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