slapify 0.0.20 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -7
- 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 +40 -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
|
|
|
@@ -10,8 +12,9 @@ By [slaps.dev](https://slaps.dev)
|
|
|
10
12
|
|
|
11
13
|
| Mode | What it does |
|
|
12
14
|
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
-
| **`task`**
|
|
14
|
-
| **`
|
|
15
|
+
| **`task`** | Autonomous AI agent — give it any goal, it figures out how to achieve it. Browses, logs in, schedules itself, audits performance, remembers state across runs. |
|
|
16
|
+
| **`server`** | Persistent server mode — stays online and waits for instructions via Telegram. Start tasks remotely from your phone. |
|
|
17
|
+
| **`run`** | Execute `.flow` test files — plain-English E2E tests with screenshots and HTML reports |
|
|
15
18
|
|
|
16
19
|
---
|
|
17
20
|
|
|
@@ -78,6 +81,18 @@ npx slapify task "..." --max-iterations N # cap agent loop iterations (default
|
|
|
78
81
|
npx slapify task "..." --schema <json> --output <file> # structured JSON output (see below)
|
|
79
82
|
```
|
|
80
83
|
|
|
84
|
+
### Server Mode (`slapify server`)
|
|
85
|
+
|
|
86
|
+
Run Slapify as a persistent background process that you control remotely via Telegram.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Start the server (waits for Telegram commands)
|
|
90
|
+
npx slapify server
|
|
91
|
+
|
|
92
|
+
# With visible browser and debug logs
|
|
93
|
+
npx slapify server --headed --debug
|
|
94
|
+
```
|
|
95
|
+
|
|
81
96
|
**Structured output (JSON schema)** — Have the agent write data that matches a schema to a file. Use `--schema` (inline JSON or path to a `.json` file) and `--output` (file path). The agent uses a `write_output` tool to append or update the file whenever it has new data — ideal for recurring tasks that keep updating a report.
|
|
82
97
|
|
|
83
98
|
```bash
|
|
@@ -102,10 +117,11 @@ npx slapify task "Every day at 9am, collect top tech headlines and add to report
|
|
|
102
117
|
| **Login automatically** | Detects login forms, uses saved credential profiles, asks you to save new ones |
|
|
103
118
|
| **Persistent memory** | Stores key facts between runs (thread URLs, last-seen items, etc.) |
|
|
104
119
|
| **Schedule itself** | Creates its own cron jobs for recurring subtasks |
|
|
105
|
-
| **Ask for input** | Pauses and prompts you when it needs
|
|
120
|
+
| **Ask for input** | Pauses and prompts you via CLI or **Live Telegram Chat** when it needs info (e.g. OTP, conf) |
|
|
106
121
|
| **Performance audit** | Scores, web vitals, network analysis, framework detection, re-render testing |
|
|
107
122
|
| **Structured output** | Writes JSON conforming to a schema to a file (append/update per run) |
|
|
108
123
|
| **HTML report** | Full session report with tool timeline, summaries, and perf data |
|
|
124
|
+
| **Custom Plugins** | Load your own markdown rules or `.ts` tools directly into the agent's brain |
|
|
109
125
|
|
|
110
126
|
### Programmatic (JS/TS)
|
|
111
127
|
|
|
@@ -453,6 +469,12 @@ In **task mode**, credentials are fully automatic:
|
|
|
453
469
|
- If no profile matches, it asks you to enter credentials and offers to save them
|
|
454
470
|
- Saved sessions are reused on future runs — no re-login needed
|
|
455
471
|
|
|
472
|
+
### Persistent Browser Profiles (Anti-Bot)
|
|
473
|
+
|
|
474
|
+
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.
|
|
475
|
+
|
|
476
|
+
See the `browser.profile` setting in the Configuration section below.
|
|
477
|
+
|
|
456
478
|
---
|
|
457
479
|
|
|
458
480
|
## Configuration
|
|
@@ -460,10 +482,15 @@ In **task mode**, credentials are fully automatic:
|
|
|
460
482
|
### `.slapify/config.yaml`
|
|
461
483
|
|
|
462
484
|
```yaml
|
|
485
|
+
# You can define an array of models! If the primary API drops/rate-limits,
|
|
486
|
+
# Slapify instantly falls back to the next model in the list.
|
|
463
487
|
llm:
|
|
464
|
-
provider: anthropic
|
|
465
|
-
|
|
466
|
-
|
|
488
|
+
- provider: anthropic
|
|
489
|
+
model: claude-3-7-sonnet-20250219
|
|
490
|
+
api_key: ${ANTHROPIC_API_KEY}
|
|
491
|
+
- provider: openai
|
|
492
|
+
model: o3-mini
|
|
493
|
+
api_key: ${OPENAI_API_KEY}
|
|
467
494
|
|
|
468
495
|
browser:
|
|
469
496
|
headless: true
|
|
@@ -471,13 +498,62 @@ browser:
|
|
|
471
498
|
viewport:
|
|
472
499
|
width: 1280
|
|
473
500
|
height: 720
|
|
501
|
+
# Anti-Bot Stealth & Persistence
|
|
502
|
+
profile: ".slapify/browser-profile"
|
|
503
|
+
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"
|
|
504
|
+
args:
|
|
505
|
+
- "--disable-blink-features=AutomationControlled"
|
|
506
|
+
- "--no-sandbox"
|
|
474
507
|
|
|
475
508
|
report:
|
|
476
509
|
format: html
|
|
477
510
|
screenshots: true
|
|
478
511
|
output_dir: ./test-reports
|
|
512
|
+
|
|
513
|
+
# Extend the agent with your own rules and Vercel AI SDK tools
|
|
514
|
+
plugins:
|
|
515
|
+
rules:
|
|
516
|
+
- "rules/my-persona.md"
|
|
517
|
+
tools:
|
|
518
|
+
- "tools/query_database.ts"
|
|
519
|
+
|
|
520
|
+
# Get live status updates and send instructions to the agent mid-run via Telegram
|
|
521
|
+
telegram:
|
|
522
|
+
bot_token: ${TELEGRAM_BOT_TOKEN}
|
|
523
|
+
allowed_chat_id: 123456789
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Extensibility & Live Chat
|
|
529
|
+
|
|
530
|
+
### Custom User Tools
|
|
531
|
+
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.
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
// .slapify/tools/database.ts
|
|
535
|
+
import { tool } from "ai";
|
|
536
|
+
import { z } from "zod";
|
|
537
|
+
|
|
538
|
+
export const queryProdDB = tool({
|
|
539
|
+
description: "Execute a secure read-only SQL query.",
|
|
540
|
+
parameters: z.object({ sql: z.string() }),
|
|
541
|
+
execute: async ({ sql }) => {
|
|
542
|
+
// Custom logic here...
|
|
543
|
+
return { rows: [{ id: 1 }] };
|
|
544
|
+
}
|
|
545
|
+
});
|
|
479
546
|
```
|
|
480
547
|
|
|
548
|
+
When the Slapify agent hits an obstacle, it will seamlessly fall back to using your custom `queryProdDB` right alongside its native browser tools!
|
|
549
|
+
|
|
550
|
+
### Telegram Integration
|
|
551
|
+
Add a `telegram` block to your config. The agent will:
|
|
552
|
+
1. Message you when long-running background tasks start and finish.
|
|
553
|
+
2. Ping you via Telegram when it gets stuck or needs a 2FA code (instead of waiting at the CLI). You can reply directly in the chat!
|
|
554
|
+
3. **Listen for instructions:** You can text the bot mid-run ("Hey, don't click that button") and it will process your instruction live.
|
|
555
|
+
4. **Remote Activation:** Use the `/task` command (e.g. `/task Buy more coffee`) to start a new mission from your phone anytime while the server is running.
|
|
556
|
+
|
|
481
557
|
---
|
|
482
558
|
|
|
483
559
|
## 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 i}from"@ai-sdk/mistral";import{createGroq as s}from"@ai-sdk/groq";export function getModel(e){switch(e.provider){case"anthropic":return n({apiKey:e.api_key})(e.model);case"openai":return t({apiKey:e.api_key})(e.model);case"google":return o({apiKey:e.api_key})(e.model);case"mistral":return i({apiKey:e.api_key})(e.model);case"groq":return s({apiKey:e.api_key})(e.model);case"ollama":return t({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1"})(e.model);default:throw new Error(`Unsupported LLM provider: ${e.provider}`)}}export class AIInterpreter{config;constructor(e){this.config=e}async interpretStep(n,t,o){const i=getModel(this.config),s=`You are a browser automation assistant. Your job is to interpret natural language test steps and convert them to specific browser actions.\n\nYou have access to a browser with the following current state:\n- URL: ${t.url}\n- Title: ${t.title}\n- Page snapshot (accessibility tree with refs):\n${t.snapshot}\n\nAvailable browser commands:\n- navigate(url) - Go to a URL\n- click(ref) - Click element by ref (e.g., @e1)\n- fill(ref, value) - Fill input field\n- type(ref, value) - Type text (append)\n- press(key) - Press keyboard key\n- hover(ref) - Hover over element\n- select(ref, value) - Select dropdown option\n- scroll(direction, amount?) - Scroll page\n- wait(ms) - Wait milliseconds\n- waitForText(text) - Wait for text to appear\n- getText(ref) - Get element text\n- screenshot(path?) - Take screenshot\n- goBack() - Navigate back\n- reload() - Reload page\n\n${o&&Object.keys(o).length>0?`Available credential profiles: ${Object.keys(o).join(", ")}. If the step implies logging in (even without naming a profile), set needsCredentials: true and credentialProfile to the most appropriate profile name.`:""}\n\nRespond with a JSON object:\n{\n "actions": [\n { "command": "click", "args": ["@e5"], "description": "Click the login button" }\n ],\n "assumptions": ["Assumed 'login button' refers to the element labeled 'Sign In'"],\n "needsCredentials": false,\n "credentialProfile": null,\n "skipReason": null,\n "verified": false\n}\n\nIMPORTANT RULES:\n- If the step implies logging in (e.g. "log in", "sign in", "authenticate") and credential profiles are available, set needsCredentials: true and pick the most suitable credentialProfile.\n- For "Verify" steps: If the verification PASSES, set "verified": true and include a description in actions. Do NOT use skipReason for successful verifications.\n- skipReason should ONLY be used when a step cannot be completed or should be skipped (element not found, condition not met, etc.)\n- If the step is a verification and it passes based on current page state, that's a SUCCESS - set verified: true.\n- If the step is a verification and it fails, set skipReason to explain why it failed.\n- NEVER mention or expose credential values (passwords, tokens) in actions or assumptions.`,a=`Interpret this test step and provide browser commands:\n\nStep: "${n.text}"\n${n.optional?"(This step is optional - can be skipped if not applicable)":""}\n${n.conditional?`Condition: "${n.condition}" → Action: "${n.action}"`:""}`,r=await e({model:i,system:s,prompt:a,maxTokens:1e3});try{const e=r.text.match(/\{[\s\S]*\}/);if(!e)throw new Error("No JSON found in response");const n=JSON.parse(e[0]);return{actions:n.actions||[],assumptions:n.assumptions||[],needsCredentials:n.needsCredentials||!1,credentialProfile:n.credentialProfile||null,skipReason:n.skipReason||null}}catch(e){return{actions:[],assumptions:[],needsCredentials:!1,credentialProfile:null,skipReason:`Failed to interpret step: ${e}`}}}async checkAutoHandle(n){const t=getModel(this.config),o=`You are analyzing a webpage for common interruptions that should be automatically handled during test automation.\n\nPage snapshot:\n${n.snapshot}\n\nLook for these common interruptions:\n1. Cookie consent banners (GDPR, etc.)\n2. Newsletter signup popups\n3. "Allow notifications" prompts\n4. Chat widgets that might block content\n5. Age verification dialogs\n6. Promotional popups/modals\n7. "Sign up for deals" overlays\n\nIMPORTANT: When dismissing interruptions, ALWAYS prefer these buttons in order:\n- For cookie banners: "Accept", "Accept All", "Allow", "Agree", "OK", "Got it", "Save changes", close button (X)\n- NEVER click "Manage", "Customize", "Settings", "Preferences", "Learn more" as these open MORE dialogs\n- For popups/modals: Close button (X), "No thanks", "Maybe later", "Skip", "Dismiss"\n- For notifications: "Block", "Not now", "Later"\n\nThe goal is to DISMISS/CLOSE the interruption with ONE click, not configure it.\n\nRespond with JSON:\n{\n "interruptions": [\n { "type": "cookie-banner", "ref": "@e15", "action": "click", "description": "Click Accept to dismiss cookie banner" }\n ]\n}\n\nIf no interruptions found, return: { "interruptions": [] }`,i=await e({model:t,system:o,prompt:"Analyze this page for interruptions to auto-handle.",maxTokens:500});try{const e=i.text.match(/\{[\s\S]*\}/);if(!e)return[];return JSON.parse(e[0]).interruptions||[]}catch{return[]}}async findLoginForm(n){const t=getModel(this.config),o=`You are analyzing a webpage to find login form elements.\n\nPage snapshot:\n${n.snapshot}\n\nFind:\n1. Username/email input field (ref)\n2. Password input field (ref)\n3. Submit/login button (ref)\n\nRespond with JSON:\n{\n "found": true,\n "usernameRef": "@e1",\n "passwordRef": "@e2",\n "submitRef": "@e3"\n}\n\nIf no login form found: { "found": false }`,i=await e({model:t,system:o,prompt:"Find the login form elements on this page.",maxTokens:300});try{const e=i.text.match(/\{[\s\S]*\}/);if(!e)return null;const n=JSON.parse(e[0]);return n.found?{usernameRef:n.usernameRef,passwordRef:n.passwordRef,submitRef:n.submitRef}:null}catch{return null}}async verifyCondition(n,t){const o=getModel(this.config),i=`You are verifying a condition on a webpage.\n\nPage URL: ${t.url}\nPage Title: ${t.title}\nPage snapshot:\n${t.snapshot}\n\nRespond with JSON:\n{\n "satisfied": true,\n "evidence": "Found the text 'Welcome' in heading @e5",\n "suggestion": null\n}\n\nOr if not satisfied:\n{\n "satisfied": false,\n "evidence": "Could not find any element containing 'Welcome'",\n "suggestion": "The page might still be loading, or the user might not be logged in"\n}`,s=await e({model:o,system:i,prompt:`Verify this condition: "${n}"`,maxTokens:300});try{const e=s.text.match(/\{[\s\S]*\}/);if(!e)return{satisfied:!1,evidence:"Could not parse verification result"};const n=JSON.parse(e[0]);return{satisfied:n.satisfied,evidence:n.evidence,suggestion:n.suggestion}}catch{return{satisfied:!1,evidence:"Verification failed"}}}async findCaptchaAction(n){const t=getModel(this.config),o=`You are analyzing a webpage that contains a CAPTCHA challenge.\n\nPage snapshot:\n${n.snapshot}\n\nFind the primary interactive captcha element — the checkbox, button, or iframe the user should click to begin solving (e.g. "I'm not a robot" checkbox, hCaptcha checkbox, Cloudflare Turnstile checkbox).\n\nRespond with JSON:\n{ "found": true, "ref": "@e5", "description": "reCAPTCHA I'm not a robot checkbox" }\n\nIf no clickable captcha element is visible: { "found": false }`;try{const n=(await e({model:t,system:o,prompt:"Find the captcha element to click.",maxTokens:200})).text.match(/\{[\s\S]*\}/);if(!n)return null;const i=JSON.parse(n[0]);return i.found&&i.ref?{ref:i.ref,description:i.description||"captcha"}:null}catch{return null}}}
|
|
1
|
+
import{generateText as e}from"ai";import{createAnthropic as n}from"@ai-sdk/anthropic";import{createOpenAI as t}from"@ai-sdk/openai";import{createGoogleGenerativeAI as o}from"@ai-sdk/google";import{createMistral as s}from"@ai-sdk/mistral";import{createGroq as i}from"@ai-sdk/groq";export function getModel(e){switch(e.provider){case"anthropic":return n({apiKey:e.api_key,baseURL:e.base_url?e.base_url.endsWith("/v1")?e.base_url:`${e.base_url.replace(/\/$/,"")}/v1`:void 0,fetch:async(e,n)=>{if(process.env.SLAPIFY_DEBUG_AI&&(console.log("\n[DEBUG] === OUTGOING ANTHROPIC REQUEST ==="),console.log("[DEBUG] URL:",e),n?.body))try{const e=JSON.parse(n.body);console.log("[DEBUG] BODY:",JSON.stringify(e,null,2))}catch{console.log("[DEBUG] BODY:",n.body)}const t=await fetch(e,n),o=t.clone();if(t.headers.get("content-type")?.includes("application/json"))try{let e=await t.text();if(process.env.SLAPIFY_DEBUG_AI&&(console.log("\n[DEBUG] === INCOMING ANTHROPIC RESPONSE ==="),console.log("[DEBUG] STATUS:",t.status),console.log("[DEBUG] PAYLOAD:",e.substring(0,1e3)+(e.length>1e3?"...":""))),e.includes('"thoughtSignature"')||e.includes('"signature"')||e.includes('"thinking"')){const n=JSON.parse(e);n.content&&Array.isArray(n.content)&&(n.content=n.content.map(e=>{if(void 0!==e.thoughtSignature&&delete e.thoughtSignature,void 0!==e.signature&&delete e.signature,"thinking"===e.type){e.type="text";const n=e.thinking||"";e.text=`<thought>\n${n}\n</thought>`,delete e.thinking}return e})),e=JSON.stringify(n)}const n=new Headers(t.headers);return n.delete("content-length"),n.delete("content-encoding"),new Response(e,{status:t.status,statusText:t.statusText,headers:n})}catch(e){return process.env.SLAPIFY_DEBUG_AI&&console.log("[DEBUG] Could not read or scrub response text:",e),o}return t}})(e.model);case"openai":{const n={apiKey:e.api_key,fetch:async(e,n)=>{if(process.env.SLAPIFY_DEBUG_AI){if(console.log("\n[DEBUG] === OUTGOING AI REQUEST ==="),console.log("[DEBUG] URL:",e),n.body)try{const e=JSON.parse(n.body);console.log("[DEBUG] BODY:",JSON.stringify(e,null,2))}catch{console.log("[DEBUG] BODY:",n.body)}console.log("[DEBUG] ===================================\n")}const t=await fetch(e,n);if(process.env.SLAPIFY_DEBUG_AI){const e=t.clone();console.log("\n[DEBUG] === INCOMING AI RESPONSE ==="),console.log("[DEBUG] STATUS:",e.status);try{const n=await e.text();console.log("[DEBUG] PAYLOAD:",n)}catch(e){console.log("[DEBUG] Could not read response text")}console.log("[DEBUG] ====================================\n")}return t}};if(e.base_url){let t=e.base_url;t.endsWith("/chat/completions")&&(t=t.replace(/\/chat\/completions\/?$/,"")),n.baseURL=t,n.compatibility="compatible"}let o=e.model;o.startsWith("openai/")&&(o=o.substring(7));return t(n)(o)}case"google":return o({apiKey:e.api_key})(e.model);case"mistral":return s({apiKey:e.api_key})(e.model);case"groq":return i({apiKey:e.api_key})(e.model);case"ollama":return t({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1",fetch:async(e,n)=>{if(process.env.SLAPIFY_DEBUG_AI){if(console.log("\n[DEBUG] === OUTGOING OLLAMA REQUEST ==="),console.log("[DEBUG] URL:",e),n.body)try{const e=JSON.parse(n.body);console.log("[DEBUG] BODY:",JSON.stringify(e,null,2))}catch{console.log("[DEBUG] BODY:",n.body)}console.log("[DEBUG] =====================================\n")}const t=await fetch(e,n);if(process.env.SLAPIFY_DEBUG_AI){const e=t.clone();console.log("\n[DEBUG] === INCOMING OLLAMA RESPONSE ==="),console.log("[DEBUG] STATUS:",e.status);try{const n=await e.text();console.log("[DEBUG] PAYLOAD:",n)}catch(e){console.log("[DEBUG] Could not read response text")}console.log("[DEBUG] =====================================\n")}return t}})(e.model);default:throw new Error(`Unsupported LLM provider: ${e.provider}`)}}export class AIInterpreter{config;constructor(e){this.config=e}async interpretStep(n,t,o){const s=getModel(this.config),i=`You are a browser automation assistant. Your job is to interpret natural language test steps and convert them to specific browser actions.\n\nYou have access to a browser with the following current state:\n- URL: ${t.url}\n- Title: ${t.title}\n- Page snapshot (accessibility tree with refs):\n${t.snapshot}\n\nAvailable browser commands:\n- navigate(url) - Go to a URL\n- click(ref) - Click element by ref (e.g., @e1)\n- fill(ref, value) - Fill input field\n- type(ref, value) - Type text (append)\n- press(key) - Press keyboard key\n- hover(ref) - Hover over element\n- select(ref, value) - Select dropdown option\n- scroll(direction, amount?) - Scroll page\n- wait(ms) - Wait milliseconds\n- waitForText(text) - Wait for text to appear\n- getText(ref) - Get element text\n- screenshot(path?) - Take screenshot\n- goBack() - Navigate back\n- reload() - Reload page\n\n${o&&Object.keys(o).length>0?`Available credential profiles: ${Object.keys(o).join(", ")}. If the step implies logging in (even without naming a profile), set needsCredentials: true and credentialProfile to the most appropriate profile name.`:""}\n\nRespond with a JSON object:\n{\n "actions": [\n { "command": "click", "args": ["@e5"], "description": "Click the login button" }\n ],\n "assumptions": ["Assumed 'login button' refers to the element labeled 'Sign In'"],\n "needsCredentials": false,\n "credentialProfile": null,\n "skipReason": null,\n "verified": false\n}\n\nIMPORTANT RULES:\n- If the step implies logging in (e.g. "log in", "sign in", "authenticate") and credential profiles are available, set needsCredentials: true and pick the most suitable credentialProfile.\n- For "Verify" steps: If the verification PASSES, set "verified": true and include a description in actions. Do NOT use skipReason for successful verifications.\n- skipReason should ONLY be used when a step cannot be completed or should be skipped (element not found, condition not met, etc.)\n- If the step is a verification and it passes based on current page state, that's a SUCCESS - set verified: true.\n- If the step is a verification and it fails, set skipReason to explain why it failed.\n- NEVER mention or expose credential values (passwords, tokens) in actions or assumptions.`,a=`Interpret this test step and provide browser commands:\n\nStep: "${n.text}"\n${n.optional?"(This step is optional - can be skipped if not applicable)":""}\n${n.conditional?`Condition: "${n.condition}" → Action: "${n.action}"`:""}`,r=await e({model:s,system:i,prompt:a,maxTokens:1e3});try{const e=r.text.match(/\{[\s\S]*\}/);if(!e)throw new Error("No JSON found in response");const n=JSON.parse(e[0]);return{actions:n.actions||[],assumptions:n.assumptions||[],needsCredentials:n.needsCredentials||!1,credentialProfile:n.credentialProfile||null,skipReason:n.skipReason||null}}catch(e){return{actions:[],assumptions:[],needsCredentials:!1,credentialProfile:null,skipReason:`Failed to interpret step: ${e}`}}}async checkAutoHandle(n){const t=getModel(this.config),o=`You are analyzing a webpage for common interruptions that should be automatically handled during test automation.\n\nPage snapshot:\n${n.snapshot}\n\nLook for these common interruptions:\n1. Cookie consent banners (GDPR, etc.)\n2. Newsletter signup popups\n3. "Allow notifications" prompts\n4. Chat widgets that might block content\n5. Age verification dialogs\n6. Promotional popups/modals\n7. "Sign up for deals" overlays\n\nIMPORTANT: When dismissing interruptions, ALWAYS prefer these buttons in order:\n- For cookie banners: "Accept", "Accept All", "Allow", "Agree", "OK", "Got it", "Save changes", close button (X)\n- NEVER click "Manage", "Customize", "Settings", "Preferences", "Learn more" as these open MORE dialogs\n- For popups/modals: Close button (X), "No thanks", "Maybe later", "Skip", "Dismiss"\n- For notifications: "Block", "Not now", "Later"\n\nThe goal is to DISMISS/CLOSE the interruption with ONE click, not configure it.\n\nRespond with JSON:\n{\n "interruptions": [\n { "type": "cookie-banner", "ref": "@e15", "action": "click", "description": "Click Accept to dismiss cookie banner" }\n ]\n}\n\nIf no interruptions found, return: { "interruptions": [] }`,s=await e({model:t,system:o,prompt:"Analyze this page for interruptions to auto-handle.",maxTokens:500});try{const e=s.text.match(/\{[\s\S]*\}/);if(!e)return[];return JSON.parse(e[0]).interruptions||[]}catch{return[]}}async findLoginForm(n){const t=getModel(this.config),o=`You are analyzing a webpage to find login form elements.\n\nPage snapshot:\n${n.snapshot}\n\nFind:\n1. Username/email input field (ref)\n2. Password input field (ref)\n3. Submit/login button (ref)\n\nRespond with JSON:\n{\n "found": true,\n "usernameRef": "@e1",\n "passwordRef": "@e2",\n "submitRef": "@e3"\n}\n\nIf no login form found: { "found": false }`,s=await e({model:t,system:o,prompt:"Find the login form elements on this page.",maxTokens:300});try{const e=s.text.match(/\{[\s\S]*\}/);if(!e)return null;const n=JSON.parse(e[0]);return n.found?{usernameRef:n.usernameRef,passwordRef:n.passwordRef,submitRef:n.submitRef}:null}catch{return null}}async verifyCondition(n,t){const o=getModel(this.config),s=`You are verifying a condition on a webpage.\n\nPage URL: ${t.url}\nPage Title: ${t.title}\nPage snapshot:\n${t.snapshot}\n\nRespond with JSON:\n{\n "satisfied": true,\n "evidence": "Found the text 'Welcome' in heading @e5",\n "suggestion": null\n}\n\nOr if not satisfied:\n{\n "satisfied": false,\n "evidence": "Could not find any element containing 'Welcome'",\n "suggestion": "The page might still be loading, or the user might not be logged in"\n}`,i=await e({model:o,system:s,prompt:`Verify this condition: "${n}"`,maxTokens:300});try{const e=i.text.match(/\{[\s\S]*\}/);if(!e)return{satisfied:!1,evidence:"Could not parse verification result"};const n=JSON.parse(e[0]);return{satisfied:n.satisfied,evidence:n.evidence,suggestion:n.suggestion}}catch{return{satisfied:!1,evidence:"Verification failed"}}}async findCaptchaAction(n){const t=getModel(this.config),o=`You are analyzing a webpage that contains a CAPTCHA challenge.\n\nPage snapshot:\n${n.snapshot}\n\nFind the primary interactive captcha element — the checkbox, button, or iframe the user should click to begin solving (e.g. "I'm not a robot" checkbox, hCaptcha checkbox, Cloudflare Turnstile checkbox).\n\nRespond with JSON:\n{ "found": true, "ref": "@e5", "description": "reCAPTCHA I'm not a robot checkbox" }\n\nIf no clickable captcha element is visible: { "found": false }`;try{const n=(await e({model:t,system:o,prompt:"Find the captcha element to click.",maxTokens:200})).text.match(/\{[\s\S]*\}/);if(!n)return null;const s=JSON.parse(n[0]);return s.found&&s.ref?{ref:s.ref,description:s.description||"captcha"}:null}catch{return null}}}
|
package/dist/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,40 @@
|
|
|
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
|
+
private pendingAskResolver?;
|
|
15
|
+
constructor(config: TelegramConfig);
|
|
16
|
+
/**
|
|
17
|
+
* Start long-polling in the background.
|
|
18
|
+
*/
|
|
19
|
+
start(): void;
|
|
20
|
+
stop(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Drain any live messages received from the user.
|
|
23
|
+
*/
|
|
24
|
+
popMessages(): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Wait for a single reply from the user (used for ask_user prompts).
|
|
27
|
+
*/
|
|
28
|
+
waitForReply(): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Cancel an active wait (e.g., if CLI input won the race).
|
|
31
|
+
*/
|
|
32
|
+
cancelWaitForReply(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Send a message to the active chat (wherever the last command came from,
|
|
35
|
+
* or the specifically configured allowed_chat_id).
|
|
36
|
+
*/
|
|
37
|
+
sendMessage(text: string): Promise<void>;
|
|
38
|
+
private pollLoop;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=telegram.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class TelegramService{botToken;allowedChatId;lastUpdateId=0;isPolling=!1;messageQueue=[];activeChatId;pendingAskResolver;constructor(e){this.botToken=e.bot_token,this.allowedChatId=e.allowed_chat_id}start(){this.isPolling||(this.isPolling=!0,this.pollLoop().catch(e=>{console.error("[Telegram] Fatal polling error:",e),this.isPolling=!1}))}stop(){this.isPolling=!1}popMessages(){if(0===this.messageQueue.length)return[];const e=[...this.messageQueue];return this.messageQueue=[],e}waitForReply(){return new Promise(e=>{this.pendingAskResolver=e})}cancelWaitForReply(){this.pendingAskResolver=void 0}async sendMessage(e){const t=this.allowedChatId||this.activeChatId;if(t)try{await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:t,text:e,parse_mode:"Markdown"})})}catch(e){console.error("[Telegram] Failed to send message:",e)}}async pollLoop(){for(;this.isPolling;)try{const e=await fetch(`https://api.telegram.org/bot${this.botToken}/getUpdates?offset=${this.lastUpdateId+1}&timeout=30`,{signal:AbortSignal.timeout?.(35e3)||void 0});if(!e.ok){if(401===e.status)return console.error("\n⚠️ [Telegram] Invalid bot token. Disabling Telegram integration."),void(this.isPolling=!1);await new Promise(e=>setTimeout(e,2e3));continue}const t=await e.json();if(t.ok&&Array.isArray(t.result))for(const e of t.result){this.lastUpdateId=Math.max(this.lastUpdateId,e.update_id);const t=e.message;if(!t||!t.text)continue;const s=t.chat.id;if((!this.allowedChatId||String(s)===String(this.allowedChatId))&&(this.activeChatId=s,t.text))if(this.pendingAskResolver)console.log(`\n💬 [Telegram] Received reply: "${t.text}"`),this.pendingAskResolver(t.text.trim()),this.pendingAskResolver=void 0;else{const e=t.text.trim();console.log(`\n💬 [Telegram] Received live instruction: "${e}"`),e.toLowerCase().startsWith("/task ")?this.messageQueue.push(e.slice(6)):this.messageQueue.push(e)}}}catch(e){"AbortError"!==e.name&&"TimeoutError"!==e.name&&await new Promise(e=>setTimeout(e,2e3))}}}
|
package/dist/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as e}from"commander";import o from"chalk";import s from"ora";import t from"path";import n from"fs";import l from"dotenv";import{loadConfig as r,loadCredentials as a,getConfigDir as i}from"./config/loader.js";import{parseFlowFile as c,findFlowFiles as d,validateFlowFile as g,getFlowSummary as p}from"./parser/flow.js";import{TestRunner as f}from"./runner/index.js";import{ReportGenerator as u}from"./report/generator.js";import{BrowserAgent as m}from"./browser/agent.js";import{generateText as y}from"ai";import{createAnthropic as w}from"@ai-sdk/anthropic";import{createOpenAI as h}from"@ai-sdk/openai";import{createGoogleGenerativeAI as $}from"@ai-sdk/google";import{createMistral as b}from"@ai-sdk/mistral";import{createGroq as S}from"@ai-sdk/groq";import x from"yaml";function k(e){switch(e.provider){case"anthropic":return w({apiKey:e.api_key})(e.model);case"openai":return h({apiKey:e.api_key})(e.model);case"google":return $({apiKey:e.api_key})(e.model);case"mistral":return b({apiKey:e.api_key})(e.model);case"groq":return S({apiKey:e.api_key})(e.model);case"ollama":return h({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1"})(e.model);default:throw new Error(`Unsupported provider: ${e.provider}`)}}l.config();const v=new e;function A(e){if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const o={};for(const[s,t]of Object.entries(e))o[String(s)]="string"==typeof t?t:JSON.stringify(t);return o}function I(e,s){const l=t.resolve(e);if(!n.existsSync(l))return console.log(o.yellow(` Skip (not found): ${l}`)),!1;const r=n.readFileSync(l,"utf-8");let a;try{a=x.parse(r)}catch(e){return console.log(o.red(` Invalid YAML: ${l}`)),console.log(o.gray(` ${e.message}`)),!1}if(!a||!a.profiles||"object"!=typeof a.profiles)return console.log(o.yellow(` No profiles in: ${l}`)),!1;let i=!1;for(const[e,o]of Object.entries(a.profiles)){if("inject"!==o.type)continue;const s="string"==typeof o.localStorage||o.localStorage&&(Array.isArray(o.localStorage)||"object"!=typeof o.localStorage),t="string"==typeof o.sessionStorage||o.sessionStorage&&(Array.isArray(o.sessionStorage)||"object"!=typeof o.sessionStorage);(s||t)&&(a.profiles[e]={...o,...s&&{localStorage:A(o.localStorage)},...t&&{sessionStorage:A(o.sessionStorage)}},i=!0)}if(!i)return console.log(o.gray(` No changes needed: ${l}`)),!1;if(s)return console.log(o.cyan(` Would fix: ${l}`)),!0;const c=l+".backup";return n.copyFileSync(l,c),n.writeFileSync(l,x.stringify(a,{indent:2,lineWidth:0})),console.log(o.green(` Fixed: ${l}`)),console.log(o.gray(` Backup: ${c}`)),!0}v.name("slapify").description("AI-powered test automation using natural language flow files").version("0.0.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 u}from"./runner/index.js";import{ReportGenerator as f}from"./report/generator.js";import{BrowserAgent as m}from"./browser/agent.js";import{generateText as y}from"ai";import{createAnthropic as w}from"@ai-sdk/anthropic";import{createOpenAI as h}from"@ai-sdk/openai";import{createGoogleGenerativeAI as $}from"@ai-sdk/google";import{createMistral as b}from"@ai-sdk/mistral";import{createGroq as S}from"@ai-sdk/groq";import k from"yaml";function v(e){switch(e.provider){case"anthropic":return w({apiKey:e.api_key})(e.model);case"openai":return h({apiKey:e.api_key})(e.model);case"google":return $({apiKey:e.api_key})(e.model);case"mistral":return b({apiKey:e.api_key})(e.model);case"groq":return S({apiKey:e.api_key})(e.model);case"ollama":return h({apiKey:"ollama",baseURL:e.base_url||"http://localhost:11434/v1"})(e.model);default:throw new Error(`Unsupported provider: ${e.provider}`)}}l.config();const x=new e;function A(e){if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const o={};for(const[s,t]of Object.entries(e))o[String(s)]="string"==typeof t?t:JSON.stringify(t);return o}function I(e,s){const l=t.resolve(e);if(!n.existsSync(l))return console.log(o.yellow(` Skip (not found): ${l}`)),!1;const r=n.readFileSync(l,"utf-8");let a;try{a=k.parse(r)}catch(e){return console.log(o.red(` Invalid YAML: ${l}`)),console.log(o.gray(` ${e.message}`)),!1}if(!a||!a.profiles||"object"!=typeof a.profiles)return console.log(o.yellow(` No profiles in: ${l}`)),!1;let i=!1;for(const[e,o]of Object.entries(a.profiles)){if("inject"!==o.type)continue;const s="string"==typeof o.localStorage||o.localStorage&&(Array.isArray(o.localStorage)||"object"!=typeof o.localStorage),t="string"==typeof o.sessionStorage||o.sessionStorage&&(Array.isArray(o.sessionStorage)||"object"!=typeof o.sessionStorage);(s||t)&&(a.profiles[e]={...o,...s&&{localStorage:A(o.localStorage)},...t&&{sessionStorage:A(o.sessionStorage)}},i=!0)}if(!i)return console.log(o.gray(` No changes needed: ${l}`)),!1;if(s)return console.log(o.cyan(` Would fix: ${l}`)),!0;const c=l+".backup";return n.copyFileSync(l,c),n.writeFileSync(l,k.stringify(a,{indent:2,lineWidth:0})),console.log(o.green(` Fixed: ${l}`)),console.log(o.gray(` Backup: ${c}`)),!0}x.name("slapify").description("AI-powered browser automation that slaps — run autonomous agents, audit performance, and write E2E tests in plain English").version("0.0.22","-v, -V, --version"),x.command("init").description("Initialize Slapify in the current directory").option("-y, --yes","Skip prompts and use defaults").action(async e=>{const t=await import("readline");if(n.existsSync(".slapify"))return console.log(o.yellow("Slapify is already initialized in this directory.")),void console.log(o.gray("Delete .slapify folder to reinitialize."));console.log(o.bold.magenta("\n🖐️ slapify\n")+o.gray("AI-powered browser automation that slaps - by slaps.dev\n"));const{findSystemBrowsers:l,initConfig:r}=await import("./config/loader.js");let a,i,c,d="anthropic";const g={anthropic:{name:"Anthropic (Claude)",envVar:"ANTHROPIC_API_KEY",defaultModel:"claude-sonnet-4-6",models:[{id:"claude-sonnet-4-6",name:"Sonnet 4.6 (claude-sonnet-4-6) - highly capable & recommended",recommended:!0},{id:"claude-sonnet-4-5",name:"Sonnet 4.5 - fast & reliable"},{id:"claude-opus-4-6",name:"Opus 4.6 - extended reasoning & advanced tasks"},{id:"claude-3-7-sonnet-latest",name:"Sonnet 3.7 - legacy hybrid reasoning"},{id:"custom",name:"Enter custom model ID"}]},openai:{name:"OpenAI",envVar:"OPENAI_API_KEY",defaultModel:"gpt-5.2",models:[{id:"gpt-5.2",name:"GPT-5.2 - latest flagship model",recommended:!0},{id:"o3-mini",name:"o3-mini - fast reasoning & coding"},{id:"gpt-5.3-codex",name:"GPT-5.3 Codex - advanced coding"},{id:"custom",name:"Enter custom model ID"}]},google:{name:"Google (Gemini)",envVar:"GOOGLE_API_KEY",defaultModel:"gemini-3-flash",models:[{id:"gemini-3-flash",name:"Gemini 3 Flash - balanced & fast",recommended:!0},{id:"gemini-3.1-pro",name:"Gemini 3.1 Pro - highly capable"},{id:"gemini-2.5-flash",name:"Gemini 2.5 Flash - older stable"},{id:"custom",name:"Enter custom model ID"}]},mistral:{name:"Mistral",envVar:"MISTRAL_API_KEY",askModel:!0,defaultModel:"mistral-small-latest"},groq:{name:"Groq (Fast inference)",envVar:"GROQ_API_KEY",askModel:!0,defaultModel:"llama-3.3-70b-versatile"},ollama:{name:"Ollama (Local)",envVar:"",askModel:!0,defaultModel:"llama3"}};if(e.yes)console.log(o.gray("Using default settings...\n"));else{const e=t.createInterface({input:process.stdin,output:process.stdout}),n=o=>new Promise(s=>e.question(o,s));console.log(o.cyan("1. Choose your LLM provider:\n")),console.log(" 1) Anthropic (Claude) "+o.green("- recommended")),console.log(" 2) OpenAI (GPT-4)"),console.log(" 3) Google (Gemini)"),console.log(" 4) Mistral"),console.log(" 5) Groq "+o.gray("- fast & free tier")),console.log(" 6) Ollama "+o.gray("- local, no API key")),console.log("");d={1:"anthropic",2:"openai",3:"google",4:"mistral",5:"groq",6:"ollama"}[await n(o.white(" Select [1]: "))]||"anthropic";const r=g[d];if(console.log(o.green(` ✓ Using ${r.name}\n`)),r.models&&r.models.length>0){console.log(o.cyan(" Choose model:\n")),r.models.forEach((e,s)=>{const t=e.recommended?o.green(" ← recommended"):"";console.log(` ${s+1}) ${e.name}${t}`)}),console.log("");const e=await n(o.white(" Select [1]: ")),s=parseInt(e)-1||0,t=r.models[s];if("custom"===t?.id){a=(await n(o.white(" Enter model ID: "))).trim()||r.defaultModel}else a=t?.id||r.defaultModel;console.log(o.green(` ✓ Using model: ${a}\n`))}else if(r.askModel){console.log(o.gray(` Enter model ID (default: ${r.defaultModel})`)),"ollama"===d?console.log(o.gray(" Common models: llama3, mistral, codellama, phi3")):"groq"===d?console.log(o.gray(" Common models: llama-3.3-70b-versatile, mixtral-8x7b-32768")):"mistral"===d&&console.log(o.gray(" Common models: mistral-small-latest, mistral-large-latest")),console.log("");a=(await n(o.white(` Model [${r.defaultModel}]: `))).trim()||r.defaultModel,console.log(o.green(` ✓ Using model: ${a}\n`)),"ollama"===d&&(console.log(o.gray(" Make sure Ollama is running: ollama serve")),console.log(""))}if("ollama"!==d){console.log(o.cyan("2. API Key verification:\n"));const t=r.envVar;let l=process.env[t];if(l)console.log(o.gray(` Found ${t} in environment`));else{console.log(o.yellow(` ${t} not found in environment`)),console.log(o.gray(" You can set it now or add it to your shell config later.\n"));const e=await n(o.white(" Enter API key (or press Enter to skip): "));e.trim()&&(l=e.trim())}let i=!1;for(;!i;){if(!l){console.log(o.yellow(`\n Remember to set ${t} before running tests.`));break}if("n"===(await n(o.white(" Verify API key with test call? (Y/n): "))).toLowerCase()){console.log(o.gray(" Skipping verification\n"));break}{e.pause();const c=s(" Verifying API key...").start();try{const e=v({provider:d,model:a||r.defaultModel||"test",api_key:l});(await y({model:e,prompt:"Reply with only the word 'pong'",maxTokens:10})).text.toLowerCase().includes("pong")?c.succeed(o.green("API key verified! ✓")):c.succeed(o.green("API key works! (got response)")),i=!0}catch(e){c.fail(o.red("API key verification failed")),console.log(o.red(` Error: ${e.message}\n`))}if(e.resume(),!i){if("n"===(await n(o.white(" Try a different API key? (Y/n): "))).toLowerCase()){console.log(o.yellow(` Remember to set ${t} correctly before running tests.`));break}const e=await n(o.white(" Enter API key: "));if(!e.trim()){console.log(o.yellow(" No key entered, skipping.\n"));break}l=e.trim()}}}console.log("")}console.log(o.cyan(("ollama"===d?"2":"3")+". Browser setup:\n"));const p=l();if(p.length>0){console.log(" Found browsers on your system:"),p.forEach((e,s)=>{console.log(o.gray(` ${s+1}) ${e.name}`))}),console.log(o.gray(` ${p.length+1}) Download Chromium (~170MB)`)),console.log(o.gray(` ${p.length+2}) Enter custom path`)),console.log("");const e=await n(o.white(" Select [1]: ")),s=parseInt(e)||1;if(s<=p.length)i=p[s-1].path,c=!0,console.log(o.green(` ✓ Using ${p[s-1].name}\n`));else if(s===p.length+2){const e=await n(o.white(" Enter browser path: "));e.trim()&&(i=e.trim(),c=!0,console.log(o.green(" ✓ Using custom browser\n")))}else c=!1,console.log(o.green(" ✓ Will download Chromium on first run\n"))}else{console.log(" No browsers found. Options:"),console.log(o.gray(" 1) Download Chromium automatically (~170MB)")),console.log(o.gray(" 2) Enter custom browser path")),console.log("");if("2"===await n(o.white(" Select [1]: "))){const e=await n(o.white(" Enter browser path: "));e.trim()&&(i=e.trim(),c=!0,console.log(o.green(" ✓ Using custom browser\n")))}else c=!1,console.log(o.green(" ✓ Will download Chromium on first run\n"))}e.close()}const p=s("Creating configuration...").start();try{r(process.cwd(),{provider:d,model:a,browserPath:i,useSystemBrowser:c}),p.succeed("Slapify initialized!"),console.log(""),console.log(o.green("Created:")),console.log(" 📁 .slapify/config.yaml - Configuration"),console.log(" 🔐 .slapify/credentials.yaml - Credentials (gitignored)"),console.log(" 📝 tests/example.flow - Sample test"),console.log("");const e=g[d];console.log(o.yellow("Next steps:")),console.log(""),"ollama"===d?(console.log(o.white(" 1. Make sure Ollama is running:")),console.log(o.cyan(" ollama serve")),console.log(o.cyan(" ollama pull llama3"))):(console.log(o.white(" 1. Set your API key:")),console.log(o.cyan(` export ${e.envVar}=your-key-here`))),console.log(""),console.log(o.white(" 2. Try an autonomous task:")),console.log(o.cyan(' npx slapify task "Monitor Bitcoin price"')),console.log(""),console.log(o.white(" 3. Run the example test flow:")),console.log(o.cyan(" npx slapify run tests/example.flow")),console.log(""),console.log(o.white(" 4. Create your own persistent tests:")),console.log(o.cyan(" npx slapify create my-first-test")),console.log(o.cyan(' npx slapify generate "test login for myapp.com"')),console.log(""),console.log(o.gray(" Config can be modified anytime in .slapify/config.yaml")),console.log("")}catch(e){p.fail(e.message),process.exit(1)}}),x.command("install").description("Install browser dependencies").action(()=>{const e=s("Checking agent-browser...").start();if(m.isInstalled())e.succeed("agent-browser is already installed");else{e.text="Installing agent-browser...";try{m.install(),e.succeed("Browser dependencies installed!")}catch(o){e.fail(`Installation failed: ${o.message}`),process.exit(1)}}}),x.command("run [files...]").description("Run flow test files").option("--headed","Run browser in headed mode (visible)").option("--report [format]","Generate report folder (html, markdown, json)").option("--output <dir>","Output directory for reports","./test-reports").option("--credentials <profile>","Default credentials profile to use").option("-p, --parallel","Run tests in parallel").option("-w, --workers <n>","Number of parallel workers (default: 4)","4").option("--performance","Run performance audit (scores, real-user metrics, framework & re-render analysis) and include in report").action(async(e,s)=>{try{const l=i();l||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const g=r(l),m=a(l);s.headed&&(g.browser={...g.browser,headless:!1});const y=void 0!==s.report;g.report={...g.report,format:"string"==typeof s.report?s.report:"html",output_dir:s.output,screenshots:y};let w=[];if(0===e.length){const e=t.join(process.cwd(),"tests");n.existsSync(e)&&(w=await d(e))}else for(const o of e)if(n.statSync(o).isDirectory()){const e=await d(o);w.push(...e)}else w.push(o);0===w.length&&(console.log(o.yellow("No .flow files found to run.")),process.exit(0));const h=new f(g.report),$=[],b=s.parallel&&w.length>1,S=parseInt(s.workers)||4;if(b){console.log(o.blue.bold(`\n━━━ Running ${w.length} tests in parallel (${S} workers) ━━━\n`));const e=[...w],s=new Map,n=new Map;for(const e of w){const s=t.basename(e,".flow");n.set(s,o.gray("⏳ pending"))}const l=()=>{process.stdout.write("["+w.length+"A");for(const e of w){const o=t.basename(e,".flow"),s=n.get(o)||"";process.stdout.write("[2K"),console.log(` ${o}: ${s}`)}};for(const e of w){const o=t.basename(e,".flow");console.log(` ${o}: ${n.get(o)}`)}const r=async e=>{const s=c(e),t=s.name;n.set(t,o.cyan("▶ running...")),l();try{const e=new u(g,m),l=await e.runFlow(s);$.push(l),"passed"===l.status?n.set(t,o.green(`✓ passed (${l.passedSteps}/${l.totalSteps} steps, ${(l.duration/1e3).toFixed(1)}s)`)):n.set(t,o.red(`✗ failed (${l.failedSteps} failed, ${l.passedSteps} passed)`))}catch(e){n.set(t,o.red(`✗ error: ${e.message}`))}l()};for(;e.length>0||s.size>0;){for(;e.length>0&&s.size<S;){const o=e.shift(),t=r(o).then(()=>{s.delete(o)});s.set(o,t)}s.size>0&&await Promise.race(s.values())}console.log("")}else for(const e of w){const t=c(e),n=p(t);console.log(""),console.log(o.blue.bold(`━━━ ${t.name} ━━━`)),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`)),console.log("");try{const e=new u(g,m),n=await e.runFlow(t,e=>{const s=e.step,t="passed"===e.status?o.green("✓"):"failed"===e.status?o.red("✗"):o.yellow("⊘"),n=s.optional?o.gray(" [optional]"):"",l=e.retried?o.yellow(" [retried]"):"",r=o.gray(`(${(e.duration/1e3).toFixed(1)}s)`);if(console.log(` ${t} ${s.text}${n}${l} ${r}`),"failed"===e.status&&e.error&&console.log(o.red(` └─ ${e.error}`)),e.assumptions&&e.assumptions.length>0)for(const s of e.assumptions)console.log(o.gray(` └─ 💡 ${s}`))},!!s.performance);if(n.perfAudit){const e=n.perfAudit,s=[];e.vitals.fcp&&s.push(`FCP ${e.vitals.fcp}ms`),e.vitals.lcp&&s.push(`LCP ${e.vitals.lcp}ms`),null!=e.vitals.cls&&s.push(`CLS ${e.vitals.cls}`);const t=e.scores??e.lighthouse;t&&s.push(`Perf ${t.performance}/100`),console.log(o.cyan(` ⚡ Perf: ${s.join(" · ")}`))}$.push(n),console.log(""),"passed"===n.status?console.log(o.green.bold(" ✓ PASSED")+o.gray(` (${n.passedSteps}/${n.totalSteps} steps in ${(n.duration/1e3).toFixed(1)}s)`)):console.log(o.red.bold(" ✗ FAILED")+o.gray(` (${n.failedSteps} failed, ${n.passedSteps} passed)`)),n.autoHandled.length>0&&console.log(o.gray(` ℹ Auto-handled: ${n.autoHandled.join(", ")}`))}catch(e){console.log(o.red(` ✗ ERROR: ${e.message}`))}}console.log(""),console.log(o.blue.bold("━━━ Summary ━━━"));const k=$.filter(e=>"passed"===e.status).length,v=$.filter(e=>"failed"===e.status).length,x=$.reduce((e,o)=>e+o.totalSteps,0),A=$.reduce((e,o)=>e+o.passedSteps,0);if(console.log(o.gray(` ${$.length} test file(s), ${x} total steps`)),0===v?console.log(o.green.bold(` ✓ All ${k} test(s) passed! (${A}/${x} steps)`)):console.log(o.red.bold(` ✗ ${v}/${$.length} test(s) failed`)),y&&$.length>0){let e;e=1===$.length?h.saveAsFolder($[0]):h.saveSuiteAsFolder($),console.log(o.cyan(`\n 📄 Report: ${e}`))}console.log(""),v>0&&process.exit(1)}catch(e){console.error(o.red(`Error: ${e.message}`)),process.exit(1)}}),x.command("create <name>").description("Create a new flow file").option("-d, --dir <directory>","Directory to create flow in","tests").action(async(e,s)=>{const l=await import("readline"),r=s.dir;n.existsSync(r)||n.mkdirSync(r,{recursive:!0});const a=e.endsWith(".flow")?e:`${e}.flow`,i=t.join(r,a);n.existsSync(i)&&(console.log(o.red(`File already exists: ${i}`)),process.exit(1)),console.log(o.blue(`\nCreating: ${i}`)),console.log(o.gray("Enter your test steps (one per line). Empty line to finish.\n"));const c=l.createInterface({input:process.stdin,output:process.stdout}),d=[`# ${e}`,""];let g=1;const p=()=>new Promise(e=>{c.question(o.cyan(`${g}. `),o=>{e(o)})});for(;;){const e=await p();if(""===e)break;d.push(e),g++}c.close(),d.length<=2?console.log(o.yellow("\nNo steps entered. File not created.")):(n.writeFileSync(i,d.join("\n")+"\n"),console.log(o.green(`\n✓ Created: ${i}`)),console.log(o.gray(` ${g-1} steps`)),console.log(o.gray(`\nRun with: slapify run ${i}`)))}),x.command("generate <prompt>").alias("gen").description("Generate a verified .flow file by running the goal as a task and recording what worked").option("-d, --dir <directory>","Directory to save flow","tests").option("--headed","Show browser window while running").action(async(e,s)=>{const t=i();t||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));r(t);console.log(o.blue("\n🤖 Flow Generator\n")),console.log(o.gray(" Running the goal in the browser to discover the real path...\n"));const{runTask:n}=await import("./task/runner.js");let l;await n({goal:e,headed:s.headed,saveFlow:!0,flowOutputDir:s.dir,onEvent:e=>{"status_update"===e.type&&process.stdout.write(o.gray(` → ${e.message}\n`)),"message"===e.type&&console.log(o.white(`\n${e.text}`)),"flow_saved"===e.type&&(l=e.path),"done"===e.type&&console.log(o.green("\n✅ Done")),"error"===e.type&&console.log(o.red(`\n✗ ${e.error}`))}}),l?(console.log(o.green(`\n✓ Flow saved: ${l}`)),console.log(o.gray(` Run with: slapify run ${l}`))):console.log(o.yellow("\n⚠ No flow was saved. The agent may not have completed the goal."))}),x.command("fix <file>").description("Analyze a failing test and suggest/apply fixes").option("--auto","Automatically apply suggested fixes without confirmation").option("--headed","Run browser in headed mode for debugging").action(async(e,t)=>{const l=i();l||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1)),n.existsSync(e)||(console.log(o.red(`File not found: ${e}`)),process.exit(1));const d=r(l),g=a(l);t.headed&&(d.browser={...d.browser,headless:!1}),d.report={...d.report,screenshots:!0};const p=await import("readline");let f=s("Running test to identify failures...").start();try{const l=c(e),r=new u(d,g),a=[];if("passed"===(await r.runFlow(l,e=>{"failed"===e.status&&e.error&&a.push({step:e.step.text,error:e.error,line:e.step.line,screenshot:e.screenshot})})).status)return void f.succeed("Test passed! No fixes needed.");if(f.info(`Found ${a.length} failing step(s)`),0===a.length)return void console.log(o.yellow("No specific step failures to fix."));const i=n.readFileSync(e,"utf-8"),m=i.split("\n");f=s("Analyzing failures and generating fixes...").start();const w=a.map(e=>`Line ${e.line}: "${e.step}"\n Error: ${e.error}`).join("\n\n"),h=await y({model:v(Array.isArray(d.llm)?d.llm[0]:d.llm),system:`You are a test automation expert. Analyze failing test steps and suggest fixes.\n\nOriginal flow file:\n\`\`\`\n${i}\n\`\`\`\n\nFailing steps:\n${w}\n\nBased on the errors, suggest fixes for the flow file. Common issues and fixes:\n1. Element not found → Try more descriptive text, add wait, or make step optional\n2. Timeout → Add explicit wait or increase timeout\n3. Navigation error → Add wait after navigation, or split into smaller steps\n4. Element obscured → Add step to close popup/modal first\n5. Stale element → Add wait for page to stabilize\n\nRespond with JSON:\n{\n "analysis": "Brief explanation of what's wrong",\n "fixes": [\n {\n "line": 5,\n "original": "Click the submit button",\n "fixed": "Click the Submit button",\n "reason": "Button text is capitalized"\n }\n ],\n "additions": [\n {\n "afterLine": 4,\n "step": "[Optional] Wait for page to load",\n "reason": "Page might still be loading"\n }\n ]\n}`,prompt:"Analyze the failures and suggest specific fixes.",maxTokens:1500});f.succeed("Analysis complete");const $=h.text.match(/\{[\s\S]*\}/);if(!$)return void console.log(o.red("Could not parse AI response"));const b=JSON.parse($[0]);if(console.log(o.blue("\n━━━ Analysis ━━━\n")),console.log(o.white(b.analysis)),b.fixes?.length>0||b.additions?.length>0){if(console.log(o.blue("\n━━━ Suggested Fixes ━━━\n")),b.fixes?.length>0)for(const e of b.fixes)console.log(o.yellow(`Line ${e.line}:`)),console.log(o.red(` - ${e.original}`)),console.log(o.green(` + ${e.fixed}`)),console.log(o.gray(` Reason: ${e.reason}\n`));if(b.additions?.length>0){console.log(o.yellow("New steps to add:"));for(const e of b.additions)console.log(o.green(` + After line ${e.afterLine}: ${e.step}`)),console.log(o.gray(` Reason: ${e.reason}\n`))}let s=t.auto;if(!s){const e=p.createInterface({input:process.stdin,output:process.stdout}),t=await new Promise(s=>{e.question(o.cyan("Apply these fixes? (y/N): "),o=>{e.close(),s(o.trim().toLowerCase())})});s="y"===t||"yes"===t}if(s){let s=[...m];new Map;if(b.fixes)for(const e of b.fixes){const o=e.line-1;o>=0&&o<s.length&&(s[o]=e.fixed)}if(b.additions){const e=[...b.additions].sort((e,o)=>o.afterLine-e.afterLine);for(const o of e){const e=o.afterLine;s.splice(e,0,o.step)}}const t=s.join("\n"),l=e+".backup";n.writeFileSync(l,i),n.writeFileSync(e,t),console.log(o.green(`\n✓ Fixes applied to ${e}`)),console.log(o.gray(` Backup saved to ${l}`)),console.log(o.gray(`\nRun again with: slapify run ${e}`))}else console.log(o.yellow("No changes made."))}else console.log(o.yellow("\nNo automatic fixes suggested.")),console.log(o.gray("The failures may require manual investigation."))}catch(e){f.fail("Error"),console.error(o.red(`Error: ${e.message}`)),process.exit(1)}}),x.command("validate [files...]").description("Validate flow files for syntax issues").action(async e=>{let s=[];if(0===e.length){const e=t.join(process.cwd(),"tests");n.existsSync(e)&&(s=await d(e))}else s=e;if(0===s.length)return void console.log(o.yellow("No .flow files found."));let l=!1;for(const e of s)try{const s=c(e),t=g(s),n=p(s);if(t.length>0){l=!0,console.log(o.yellow(`⚠️ ${e}`));for(const e of t)console.log(o.yellow(` ${e}`))}else console.log(o.green(`✅ ${e}`)),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`))}catch(s){console.log(o.red(`❌ ${e}`)),console.log(o.red(` ${s.message}`)),l=!0}l&&process.exit(1)}),x.command("list").description("List all flow files").action(async()=>{const e=t.join(process.cwd(),"tests");if(!n.existsSync(e))return void console.log(o.yellow("No tests directory found."));const s=await d(e);if(0!==s.length){console.log(o.blue(`\nFound ${s.length} flow file(s):\n`));for(const e of s){const s=c(e),n=p(s),l=t.relative(process.cwd(),e);console.log(` ${o.white(l)}`),console.log(o.gray(` ${n.totalSteps} steps (${n.requiredSteps} required, ${n.optionalSteps} optional)`))}console.log("")}else console.log(o.yellow("No .flow files found."))}),x.command("credentials").description("List configured credential profiles").action(()=>{const e=i();e||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const s=a(e),t=Object.keys(s.profiles);if(0===t.length)return console.log(o.yellow("No credential profiles configured.")),void console.log(o.gray("Edit .slapify/credentials.yaml to add profiles."));console.log(o.blue("\nConfigured credential profiles:\n"));for(const e of t){const t=s.profiles[e];console.log(` ${o.white(e)} (${t.type})`),t.username&&console.log(o.gray(` username: ${t.username}`)),t.email&&console.log(o.gray(` email: ${t.email}`)),t.totp_secret&&console.log(o.gray(" 2FA: TOTP configured")),t.fixed_otp&&console.log(o.gray(" 2FA: Fixed OTP configured"))}console.log("")}),x.command("fix-credentials [files...]").description("Fix credential YAML files where localStorage/sessionStorage were saved as JSON strings").option("--dry-run","Only print what would be fixed").action((e,s)=>{const n=[];if(e&&e.length>0)n.push(...e.map(e=>t.resolve(e)));else{const e=process.cwd();n.push(t.join(e,"temp_credentials.yaml"));const o=i();o&&n.push(t.join(o,"credentials.yaml"))}console.log(o.blue("\n🔧 Fix credential YAML files\n")),s.dryRun&&console.log(o.gray(" (dry run – no files will be modified)\n"));let l=0;for(const e of n)I(e,!!s.dryRun)&&l++;0===l&&n.length>0&&console.log(o.gray("\n No files needed fixing.")),console.log("")}),x.command("interactive [url]").alias("i").description("Run steps interactively").option("--headed","Run browser in headed mode").action(async(e,s)=>{console.log(o.blue("\n🧪 Slapify Interactive Mode")),console.log(o.gray('Type test steps and press Enter to execute. Type "exit" to quit.\n'));const t=i();t||(console.log(o.red('No .slapify directory found. Run "slapify init" first.')),process.exit(1));const n=r(t),l=a(t);s.headed&&(n.browser={...n.browser,headless:!1});new u(n,l);e&&console.log(o.gray(`Navigating to ${e}...`));const c=(await import("readline")).createInterface({input:process.stdin,output:process.stdout}),d=()=>{c.question(o.cyan("> "),async e=>{const s=e.trim();"exit"!==s.toLowerCase()&&"quit"!==s.toLowerCase()||(console.log(o.gray("\nGoodbye!")),c.close(),process.exit(0)),s?(console.log(o.gray(`Executing: ${s}`)),console.log(o.green("✓ Done")),d()):d()})};d()}),x.command("server").description("Run Slapify in server mode. The agent stays online and waits for instructions via Telegram.\n Use this for long-running bots that you control remotely.").option("--headed","Show the browser window").option("--debug","Show all tool calls and internal steps").option("--report","Generate an HTML report after the server stops").action(async e=>{x._slapifyServerMode=!0,await x.commands.find(e=>"task"===e._name).parseAsync(["You are running in SERVER MODE. You are ALREADY connected to Telegram and this chat. Wait for the user to send you a new task or instruction. When you receive a message, treat it as a new goal. Do NOT ask for Telegram credentials or tokens; they are already active. If no command is pending, you should simply wait or use the status_update() tool to say you are ready.",...e.headed?["--headed"]:[],...e.debug?["--debug"]:[],...e.report?["--report"]:[]],{from:"user"})}),x.command("task [goal]").description('Run an autonomous AI agent task in plain English.\n The agent decides everything: what to do, when to schedule, when to sleep.\n Examples:\n slapify task "Go to linkedin.com and like the latest 3 posts"\n slapify task "Monitor my Gmail for new emails every 30 min and log subjects"\n slapify task "Order breakfast from Swiggy every day at 8am"').option("--headed","Show the browser window").option("--debug","Show all tool calls and internal steps").option("--report","Generate an HTML report after the task completes").option("--save-flow","Save agent steps as a reusable .flow file when done").option("--session <id>","Resume an existing task session").option("--list-sessions","List all task sessions").option("--logs <id>","Show logs for a task session").option("--max-iterations <n>","Safety cap on agent loop iterations (default 400)",parseInt).option("--schema <json-or-file>","JSON Schema (inline JSON string or path to a .json file) the agent should use to structure its output").option("--output <file>","File path to write structured JSON output to (used together with --schema)").action(async(e,s)=>{if(s.listSessions){const{listSessions:e}=await import("./task/index.js"),s=e();if(0===s.length)return void console.log(o.gray("\nNo task sessions found.\n"));console.log(o.blue(`\n📋 Task Sessions (${s.length})\n`));for(const e of s){const s="completed"===e.status?o.green:"failed"===e.status?o.red:"scheduled"===e.status?o.blue:o.yellow;console.log(` ${s("●")} ${o.bold(e.id)}\n Goal: ${e.goal.slice(0,70)}${e.goal.length>70?"…":""}\n Status: ${s(e.status)} Iterations: ${e.iteration}\n Updated: ${new Date(e.updatedAt).toLocaleString()}\n`)}return}if(s.logs){const{loadSession:e}=await import("./task/index.js"),{loadEvents:t}=await import("./task/session.js"),n=e(s.logs);n||(console.log(o.red(`Session '${s.logs}' not found.`)),process.exit(1)),console.log(o.blue(`\n📜 Logs: ${n.id}\n`)),console.log(o.gray(`Goal: ${n.goal}\n`));const l=t(s.logs);for(const e of l){const s=o.gray(new Date(e.ts).toLocaleTimeString());"llm_response"===e.type?e.text&&console.log(`${s} 🤔 ${o.cyan(e.text.slice(0,120))}`):"tool_call"===e.type?console.log(`${s} 🔧 ${o.yellow(e.toolName)} → ${o.gray(JSON.stringify(e.result).slice(0,80))}`):"tool_error"===e.type?console.log(`${s} ❌ ${o.red(e.toolName)} → ${o.red(e.error.slice(0,80))}`):"memory_update"===e.type?console.log(`${s} 🧠 ${o.magenta("remember")} ${e.key} = ${e.value.slice(0,60)}`):"scheduled"===e.type?console.log(`${s} ⏰ ${o.blue("schedule")} ${e.cron} — ${e.task}`):"sleeping_until"===e.type?console.log(`${s} 😴 ${o.blue("sleep")} until ${e.until}`):"session_end"===e.type&&console.log(`${s} ✅ ${o.green("done")} ${e.summary.slice(0,120)}`)}return void console.log("")}let l=e||s.session?e:void 0;if(l||s.session||(console.log(o.red('\nPlease provide a goal. Example:\n slapify task "Go to example.com and check the title"\n')),process.exit(1)),!l&&s.session){const{loadSession:e}=await import("./task/index.js"),t=e(s.session);t||(console.log(o.red(`Session '${s.session}' not found.`)),process.exit(1)),l=t.goal}i()||(console.log(o.red('\nNo .slapify directory found. Run "slapify init" first.\n')),process.exit(1));let r=null;const a=async e=>{if(s.report)try{const{loadEvents:s,saveTaskReport:t}=await import("./task/index.js"),n=t(e,s(e.id));console.log(o.cyan(`\n 📊 Report: ${n}`))}catch(e){console.log(o.yellow(` ⚠ Could not generate report: ${e?.message}`))}},c=!!s.debug,d=()=>process.stdout.write("[2K\r");console.log(o.blue("\n🤖 Slapify Task Agent\n")),console.log(o.white(` Goal: ${l}`)),s.session&&console.log(o.gray(` Resuming session: ${s.session}`)),console.log(o.gray([s.report?" --report: HTML report on exit":"",c?" --debug: verbose output":""," Ctrl+C to stop"].filter(Boolean).join(" · ")+"\n")),console.log(o.gray("─".repeat(60))+"\n");let g=null,p=!1;const u=()=>{if(c||p||g)return;const e=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];let s=0;g=setInterval(()=>{process.stdout.write(o.gray(`\r ${e[s++%e.length]} working...`))},80)};c||u();const{runTask:f}=await import("./task/index.js");let m=!1;const y=async()=>{if(!m){if(m=!0,clearInterval(g),g=null,p=!0,process.stdout.write("[2K\r"),console.log(o.yellow("\n ⚡ Interrupted"+(s.report?" — generating report...":""))),r){r.status="failed",r.finalSummary="Task interrupted by user (Ctrl+C).";const{saveSessionMeta:e}=await import("./task/session.js");e(r),await a(r)}console.log(o.gray(" Goodbye.\n")),process.exit(0)}};process.once("SIGINT",y);try{const e=()=>{g&&(clearInterval(g),g=null),process.stdout.write("[2K\r")};let i;if(s.schema)try{i=JSON.parse(s.schema)}catch{try{const e=n.readFileSync(t.resolve(s.schema),"utf8");i=JSON.parse(e)}catch{console.log(o.red("Could not parse --schema: expected inline JSON or a valid .json file path.")),process.exit(1)}}const m=await f({goal:l,sessionId:s.session,headed:s.headed,saveFlow:s.saveFlow,maxIterations:s.maxIterations,schema:i,outputFile:s.output,onHumanInput:async(s,t)=>{p=!0,e();const n=(await import("readline")).createInterface({input:process.stdin,output:process.stdout,terminal:!0}),l=new Promise(e=>{n.question(` ${o.cyan("›")} `,o=>{n.close(),e(o.trim())})});let a,i=!1;const c=r?._telegramInstance;if(c){const e=c.waitForReply().then(e=>(i=!0,e));a=await Promise.race([l,e]),i?(n.write("\n"),n.close()):c.cancelWaitForReply()}else a=await l;return console.log(o.yellow("─".repeat(60))+"\n"),p=!1,u(),a},onEvent:s=>{const t="status_update"===s.type||"human_input_needed"===s.type||"credentials_saved"===s.type||"done"===s.type||"error"===s.type||"tool_error"===s.type;t&&e(),(e=>{switch(e.type){case"thinking":c&&process.stdout.write(o.gray(" ⟳ thinking...\r"));break;case"message":c&&(d(),console.log(o.gray(` 💬 ${e.text}`)));break;case"tool_start":if(c){d();const s=JSON.stringify(e.args);console.log(o.dim(` › ${o.cyan(e.toolName)} `)+o.gray(s.slice(0,100)+(s.length>100?"…":"")))}break;case"tool_done":c&&console.log(o.dim(` ✓ ${e.result.slice(0,120)}`));break;case"tool_error":d(),console.log(o.red(` ✗ ${e.toolName}: ${e.error.slice(0,120)}`));break;case"status_update":d(),console.log(o.white(` ${e.message}`));break;case"human_input_needed":console.log("\n"+o.yellow("─".repeat(60))),console.log(o.yellow.bold(" 🙋 Agent needs your input")),console.log(o.white(`\n ${e.question}`)),e.hint&&console.log(o.gray(` ${e.hint}`));break;case"credentials_saved":d(),console.log(o.green(` 💾 Credentials saved: '${e.profileName}' (${e.credType}) → .slapify/credentials.yaml`));break;case"scheduled":c&&(d(),console.log(o.dim(` ⏰ scheduled: ${e.cron} — ${e.task}`)));break;case"sleeping":c&&(d(),console.log(o.dim(` 😴 sleeping until ${new Date(e.until).toLocaleString()}`)));break;case"done":d(),console.log("\n"+o.green("─".repeat(60))),console.log(o.green.bold(" ✅ Task complete!")),console.log(o.white(`\n ${e.summary}`)),console.log(o.green("─".repeat(60)));break;case"error":d(),console.log(o.red(`\n ✗ Error: ${e.error}`))}})(s),t&&"done"!==s.type&&"error"!==s.type&&"human_input_needed"!==s.type&&u()},onSessionUpdate:e=>{r=e}});if(e(),process.removeListener("SIGINT",y),console.log(o.gray(`\n Session: ${m.id}`)),m.savedFlowPath&&console.log(o.cyan(` Flow saved: ${m.savedFlowPath}`)),s.output&&null!=m.structuredOutput&&console.log(o.cyan(` Output: ${t.resolve(s.output)}`)),Object.keys(m.memory).length>0){console.log(o.gray(` Memory (${Object.keys(m.memory).length} items):`));for(const[e,s]of Object.entries(m.memory))console.log(o.gray(` • ${e}: ${s.slice(0,80)}`))}await a(m),console.log("")}catch(e){process.removeListener("SIGINT",y),clearInterval(g),g=null,process.stdout.write("[2K\r"),console.error(o.red(`\n Task failed: ${e?.message||e}`)),process.env.SLAPIFY_DEBUG_AI&&(console.error(o.dim(`\n Stack Trace:\n${e?.stack||""}`)),e?.cause&&console.error(o.yellow(`\n Error Cause:\n${JSON.stringify(e.cause,null,2)}`))),r&&await a(r),process.exit(1)}}),x.parse();
|
package/dist/config/loader.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 f=`llm:\n provider: ${p}\n model: ${m}`;f+="ollama"===p?"\n # Ollama runs locally - no API key needed\n base_url: http://localhost:11434/v1":`\n api_key: \${${u}}`;const h=`# Slapify Configuration\n# Docs: https://slaps.dev/slapify\n\n# LLM Settings (required)\n# You can change the provider and model anytime\n${f}\n\n# Browser Automation Settings\n${d}\n # Anti-Bot Stealth & Persistence\n profile: ".slapify/browser-profile"\n userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"\n args:\n - "--disable-blink-features=AutomationControlled"\n - "--no-sandbox"\n# To use Browserbase cloud browsers instead of local (stealth, scalable, no install):\n# browser:\n# browserbase:\n# api_key: \${BROWSERBASE_API_KEY}\n# project_id: \${BROWSERBASE_PROJECT_ID} # optional\n\n# Custom User Plugins\n# Add your own markdown rules to the agent's system prompt\n# plugins:\n# rules:\n# - "rules/my-persona.md"\n# - "rules/company-guidelines.md"\n# tools:\n# - "tools/custom-utils.js"\n\n# Live Telegram Chat Integration\n# Talk to your agent while it runs and let it ask you questions\n# telegram:\n# bot_token: \${TELEGRAM_BOT_TOKEN}\n# allowed_chat_id: \${TELEGRAM_CHAT_ID} # optional: restrict who can talk to it\n\n# Report Settings\nreport:\n format: html\n screenshots: true\n output_dir: ./test-reports\n`;e.writeFileSync(n.join(s,r),h);e.writeFileSync(n.join(s,i),'# Slapify Credentials\n# WARNING: Do not commit this file to version control!\n# This file is gitignored by default\n\nprofiles:\n # Default login credentials (for form-based login)\n default:\n type: login-form\n username: ${TEST_USERNAME}\n password: ${TEST_PASSWORD}\n\n # Example: Admin account with 2FA\n # admin:\n # type: login-form\n # username: admin@example.com\n # password: ${ADMIN_PASSWORD}\n # totp_secret: JBSWY3DPEHPK3PXP\n\n # Example: Inject auth token via localStorage\n # token-auth:\n # type: inject\n # localStorage:\n # auth_token: ${AUTH_TOKEN}\n # user_id: "12345"\n\n # Example: Inject session via sessionStorage\n # session-auth:\n # type: inject\n # sessionStorage:\n # session_token: ${SESSION_TOKEN}\n\n # Example: Inject auth cookies\n # cookie-auth:\n # type: inject\n # cookies:\n # - name: auth_token\n # value: ${AUTH_TOKEN}\n # domain: .example.com\n # - name: refresh_token\n # value: ${REFRESH_TOKEN}\n # domain: .example.com\n\n # Example: Combined - cookies + localStorage\n # full-auth:\n # type: inject\n # cookies:\n # - name: session_id\n # value: ${SESSION_ID}\n # domain: .example.com\n # localStorage:\n # user_preferences: \'{"theme":"dark","lang":"en"}\'\n # feature_flags: \'{"beta":true}\'\n');const g=n.join(o,"tests");e.existsSync(g)||e.mkdirSync(g,{recursive:!0});e.writeFileSync(n.join(g,"example.flow"),'# Example Test - Getting Started with Slapify\n# Run with: slapify run tests/example.flow\n\n# Navigate to a website\nGo to https://example.com\n\n# Verify the page loaded correctly\nVerify page title contains "Example"\n\n# Handle potential popups (won\'t fail if not present)\n[Optional] Close any cookie consent popup\n\n# Interact with the page\nClick on "More information" link\n\n# Verify navigation worked\nVerify URL contains "iana.org"\n');e.writeFileSync(n.join(s,".gitignore"),"credentials.yaml\n")}export function checkBrowserPath(n){return e.existsSync(n)}export function findSystemBrowsers(){const n=[],o=[{name:"Google Chrome",path:"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"},{name:"Chrome (Linux)",path:"/usr/bin/google-chrome"},{name:"Chrome (Linux Alt)",path:"/usr/bin/google-chrome-stable"},{name:"Chromium",path:"/usr/bin/chromium"},{name:"Chromium (Linux)",path:"/usr/bin/chromium-browser"},{name:"Chrome (Windows)",path:"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"},{name:"Chrome (Windows x86)",path:"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"},{name:"Edge",path:"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"},{name:"Edge (Windows)",path:"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"},{name:"Brave",path:"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"}];for(const t of o)e.existsSync(t.path)&&n.push(t);return n}export function getConfigDir(){return a()}
|