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.
@@ -1 +1 @@
1
- import{BrowserAgent as e}from"../browser/agent.js";import{AIInterpreter as t}from"../ai/interpreter.js";import*as s from"fs";import*as a from"path";import*as i from"yaml";export class TestRunner{config;credentials;browser;ai;autoHandled=[];allAssumptions=[];constructor(s,a){this.config=s,this.credentials=a,this.browser=new e(s.browser),this.ai=new t(s.llm)}async runFlow(e,t,s){const a=new Date,i=[];let o;try{for(const s of e.steps){let e=await this.executeStep(s);const a=s.text.match(/@debug_wait/i);if("failed"===e.status&&!s.optional&&!a){await this.browser.wait(1e3);const t=await this.executeStep(s);t.retried=!0,t.duration+=e.duration,e=t}if(i.push(e),t&&t(e),"failed"===e.status&&!s.optional)break}if(s)try{const e=await this.browser.getUrl(),{collectCoreWebVitals:t,collectReactScanResults:s}=await import("../perf/audit.js"),[a,i]=await Promise.all([t(this.browser),s(this.browser)]);o={url:e,auditedAt:(new Date).toISOString(),vitals:a,react:i,scores:null}}catch{}}finally{await this.browser.close()}if(s&&o)try{const{runLighthouseAudit:e}=await import("../perf/audit.js"),t=this.config.report?.output_dir||"./test-reports",s=await e(o.url,t);s&&(o.scores=s.scores,o.lighthouse=s.scores,s.reportPath&&(o.lighthouseReportPath=s.reportPath))}catch{}const r=new Date,n=i.filter(e=>"passed"===e.status).length,c=i.filter(e=>"failed"===e.status).length,l=i.filter(e=>"skipped"===e.status).length;return{flowFile:e.path||e.name,status:0===c||i.every(t=>"failed"!==t.status||e.steps[i.indexOf(t)]?.optional)?"passed":"failed",steps:i,totalSteps:e.steps.length,passedSteps:n,failedSteps:c,skippedSteps:l,duration:r.getTime()-a.getTime(),startTime:a,endTime:r,autoHandled:this.autoHandled,assumptions:this.allAssumptions,...o?{perfAudit:o}:{}}}async executeStep(e){const t=Date.now(),s=[],a=[];try{const i=await this.browser.getState();await this.handleInterruptions(s);const o=[/login\s+with\s+([\w-]+)\s+credentials/i,/@inject[:\s]+([\w-]+)/i,/inject\s+([\w-]+)\s+credentials/i,/use\s+([\w-]+)\s+credentials/i],r=[/^(log\s?in|sign\s?in|authenticate|login)(\s+to\s+\S+)?(\s+using\s+credentials?)?$/i,/^(log\s?in|sign\s?in)\s+with\s+credentials?$/i];let n=null;for(const t of o){const s=e.text.match(t);if(s){n=s[1];break}}if(!n){r.some(t=>t.test(e.text.trim()))&&(n=this.pickBestCredentialProfile(i.url))}const c=e.text.match(/@debug_wait(?:[:\s]+(.+))?/i);if(c){const a=c[1]?.trim()||"captured";return await this.handleDebugWait(a,s),{step:e,status:"passed",duration:Date.now()-t,actions:s}}if(n){const e=this.credentials.profiles[n];if(!e)throw new Error(`Credential profile not found: ${n}`);await this.handleLogin(e,s)}else{const o=await this.ai.interpretStep(e,i,this.credentials.profiles);if(o.needsCredentials&&!n){const a=o.credentialProfile,r=this.credentials?.profiles||{};if(n=a&&r[a]?a:this.pickBestCredentialProfile(i.url),n){await this.handleLogin(r[n],s);const a=this.config.report?.screenshots?await this.browser.screenshot(`step-${e.line}.png`).catch(()=>{}):void 0;return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:[`Auto-selected credential profile: ${n}`],screenshot:a}}}if(o.skipReason){if(e.optional||e.conditional)return{step:e,status:"skipped",duration:Date.now()-t,actions:[{type:"info",description:o.skipReason,timestamp:Date.now()}]};throw new Error(o.skipReason)}o.assumptions.length>0&&(a.push(...o.assumptions),this.allAssumptions.push(...o.assumptions));for(const e of o.actions)await this.executeCommand(e,s)}let l;if(await this.handleCaptcha(s),await this.handleInterruptions(s),this.config.report?.screenshots){const t=`step-${e.line}.png`;await this.browser.screenshot(t),l=t}return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:a.length>0?a:void 0,screenshot:l}}catch(a){let i;try{if(this.config.report?.screenshots){const t=`step-${e.line}-failed.png`;await this.browser.screenshot(t),i=t}}catch{}return e.optional?{step:e,status:"skipped",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}:{step:e,status:"failed",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}}}async executeCommand(e,t){switch(t.push({type:this.getActionType(e.command),description:e.description,selector:e.args[0],value:e.args[1],timestamp:Date.now()}),e.command){case"navigate":await this.browser.navigate(e.args[0]),await this.browser.waitForStable();break;case"click":await this.browser.click(e.args[0]),await this.browser.wait(300);break;case"fill":await this.browser.fill(e.args[0],e.args[1]);break;case"type":await this.browser.type(e.args[0],e.args[1]);break;case"press":await this.browser.press(e.args[0]);break;case"hover":await this.browser.hover(e.args[0]);break;case"select":await this.browser.select(e.args[0],e.args[1]);break;case"scroll":await this.browser.scroll(e.args[0],parseInt(e.args[1]));break;case"wait":await this.browser.wait(parseInt(e.args[0]));break;case"waitForText":await this.browser.wait(`text=${e.args[0]}`);break;case"getText":await this.browser.getText(e.args[0]);break;case"screenshot":await this.browser.screenshot(e.args[0]);break;case"goBack":await this.browser.goBack();break;case"reload":await this.browser.reload();break;default:throw new Error(`Unknown command: ${e.command}`)}await this.browser.wait(100)}pickBestCredentialProfile(e){const t=this.credentials?.profiles;if(!t||0===Object.keys(t).length)return null;try{const s=new URL(e).hostname.replace(/^www\./,""),a=s.split(".")[0];for(const e of Object.keys(t))if(e.toLowerCase()===a.toLowerCase()||e.toLowerCase()===s.toLowerCase())return e}catch{}return t.default?"default":Object.keys(t)[0]}async handleCaptcha(e){try{const t=await this.browser.getState(),s=(t.snapshot||"").toLowerCase(),a=(t.title||"").toLowerCase(),i=(t.url||"").toLowerCase();if(![s.includes("recaptcha"),s.includes("hcaptcha"),s.includes("turnstile"),s.includes("i'm not a robot"),s.includes("i am not a robot"),s.includes("verify you are human"),s.includes("verify you're human"),a.includes("just a moment"),i.includes("challenge")].some(Boolean))return;const o=await this.ai.findCaptchaAction(t);if(!o)return;await this.browser.click(o.ref),await this.browser.wait(2e3);const r=`Auto-solved captcha: ${o.description}`;this.autoHandled.push(r),e.push({type:"auto-handle",description:r,selector:o.ref,timestamp:Date.now()})}catch{}}async handleInterruptions(e){try{const t=await this.browser.getState(),s=await this.ai.checkAutoHandle(t);for(const t of s)try{await this.browser.click(t.ref);const s=`Auto-handled: ${t.description}`;this.autoHandled.push(s),e.push({type:"auto-handle",description:s,selector:t.ref,timestamp:Date.now()}),await this.browser.wait(500)}catch{}}catch{}}async handleLogin(e,t){if("inject"===e.type){let s=0,a=0;if(e.cookies)for(const i of e.cookies)try{await this.browser.setCookie(i.name,i.value),t.push({type:"fill",description:`Set cookie: ${i.name}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set cookie: ${i.name} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.localStorage)for(const[i,o]of Object.entries(e.localStorage))try{await this.browser.setLocalStorage(i,o),t.push({type:"fill",description:`Set localStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set localStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.sessionStorage)for(const[i,o]of Object.entries(e.sessionStorage))try{await this.browser.setSessionStorage(i,o),t.push({type:"fill",description:`Set sessionStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set sessionStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}return a>0&&console.log(` ⚠ ${a} item(s) failed to inject (continuing with ${s} successful)`),await this.browser.wait(300),void await this.browser.reload()}if("login-form"===e.type&&e.username&&e.password){const s=await this.browser.getState(),a=await this.ai.findLoginForm(s);if(!a)throw new Error("Could not find login form on page");if(await this.browser.fill(a.usernameRef,e.username),t.push({type:"fill",description:"Filled username field",selector:a.usernameRef,timestamp:Date.now()}),await this.browser.fill(a.passwordRef,e.password),t.push({type:"fill",description:"Filled password field",selector:a.passwordRef,timestamp:Date.now()}),await this.browser.click(a.submitRef),t.push({type:"click",description:"Clicked login button",selector:a.submitRef,timestamp:Date.now()}),await this.browser.wait(2e3),e.totp_secret){this.generateTOTP(e.totp_secret);t.push({type:"fill",description:"Entered TOTP code",timestamp:Date.now()})}e.fixed_otp&&t.push({type:"fill",description:"Entered fixed OTP",timestamp:Date.now()})}}generateTOTP(e){Math.floor(Date.now()/1e3/30);return"000000"}async handleDebugWait(e,t){console.log("\n"+"=".repeat(60)),console.log("🔴 DEBUG WAIT - Browser paused for manual interaction"),console.log("=".repeat(60)),console.log("\nYou can now interact with the browser manually."),console.log("(e.g., log in, complete 2FA, accept cookies, etc.)\n"),console.log("Press ENTER when done to capture cookies & localStorage...\n"),t.push({type:"info",description:"Paused for manual interaction (@debug_wait)",timestamp:Date.now()}),await new Promise(e=>{const t=()=>{process.stdin.removeListener("data",t),process.stdin.pause(),e()};process.stdin.resume(),process.stdin.once("data",t)}),console.log("\n📸 Capturing browser state...\n");const o=await this.browser.getCookies();let r=await this.browser.getLocalStorage(),n=await this.browser.getSessionStorage();const c=e=>{if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,a]of Object.entries(e))t[String(s)]="string"==typeof a?a:JSON.stringify(a);return t};r=c(r),n=c(n);const l={type:"inject"};o.length>0&&(l.cookies=o.map(e=>({name:e.name,value:e.value}))),Object.keys(r).length>0&&(l.localStorage=r),Object.keys(n).length>0&&(l.sessionStorage=n);const p=a.join(process.cwd(),"temp_credentials.yaml");let w={profiles:{}};if(s.existsSync(p))try{const e=i.parse(s.readFileSync(p,"utf-8"));e&&e.profiles&&"object"==typeof e.profiles&&(w={profiles:e.profiles})}catch{w={profiles:{}}}w.profiles[e]=l;const h=`# Captured credentials from @debug_wait\n# Generated: ${(new Date).toISOString()}\n#\n# To use: copy the profile you need to .slapify/credentials.yaml\n# Then use: @inject ${e}\n\n${i.stringify(w,{indent:2,lineWidth:0})}`;s.writeFileSync(p,h),console.log("✅ Captured:"),console.log(` - ${o.length} cookie(s)`),console.log(` - ${Object.keys(r).length} localStorage item(s)`),console.log(` - ${Object.keys(n).length} sessionStorage item(s)`),console.log(`\n📁 Saved to: ${p}`),console.log(` Profile name: "${e}"`),console.log("\n"+"=".repeat(60)+"\n"),t.push({type:"info",description:`Captured ${o.length} cookies, ${Object.keys(r).length} localStorage, ${Object.keys(n).length} sessionStorage to temp_credentials.yaml`,timestamp:Date.now()})}getActionType(e){switch(e){case"navigate":return"navigate";case"click":case"hover":case"press":return"click";case"fill":case"type":case"select":return"fill";case"wait":case"waitForText":return"wait";default:return"info"}}}
1
+ import{BrowserAgent as e}from"../browser/agent.js";import{AIInterpreter as t}from"../ai/interpreter.js";import*as s from"fs";import*as a from"path";import*as i from"yaml";export class TestRunner{config;credentials;browser;ai;autoHandled=[];allAssumptions=[];constructor(s,a){this.config=s,this.credentials=a,this.browser=new e(s.browser),this.ai=new t(Array.isArray(s.llm)?s.llm[0]:s.llm)}async runFlow(e,t,s){const a=new Date,i=[];let o;try{for(const s of e.steps){let e=await this.executeStep(s);const a=s.text.match(/@debug_wait/i);if("failed"===e.status&&!s.optional&&!a){await this.browser.wait(1e3);const t=await this.executeStep(s);t.retried=!0,t.duration+=e.duration,e=t}if(i.push(e),t&&t(e),"failed"===e.status&&!s.optional)break}if(s)try{const e=await this.browser.getUrl(),{collectCoreWebVitals:t,collectReactScanResults:s}=await import("../perf/audit.js"),[a,i]=await Promise.all([t(this.browser),s(this.browser)]);o={url:e,auditedAt:(new Date).toISOString(),vitals:a,react:i,scores:null}}catch{}}finally{await this.browser.close()}if(s&&o)try{const{runLighthouseAudit:e}=await import("../perf/audit.js"),t=this.config.report?.output_dir||"./test-reports",s=await e(o.url,t);s&&(o.scores=s.scores,o.lighthouse=s.scores,s.reportPath&&(o.lighthouseReportPath=s.reportPath))}catch{}const r=new Date,n=i.filter(e=>"passed"===e.status).length,c=i.filter(e=>"failed"===e.status).length,l=i.filter(e=>"skipped"===e.status).length;return{flowFile:e.path||e.name,status:0===c||i.every(t=>"failed"!==t.status||e.steps[i.indexOf(t)]?.optional)?"passed":"failed",steps:i,totalSteps:e.steps.length,passedSteps:n,failedSteps:c,skippedSteps:l,duration:r.getTime()-a.getTime(),startTime:a,endTime:r,autoHandled:this.autoHandled,assumptions:this.allAssumptions,...o?{perfAudit:o}:{}}}async executeStep(e){const t=Date.now(),s=[],a=[];try{const i=await this.browser.getState();await this.handleInterruptions(s);const o=[/login\s+with\s+([\w-]+)\s+credentials/i,/@inject[:\s]+([\w-]+)/i,/inject\s+([\w-]+)\s+credentials/i,/use\s+([\w-]+)\s+credentials/i],r=[/^(log\s?in|sign\s?in|authenticate|login)(\s+to\s+\S+)?(\s+using\s+credentials?)?$/i,/^(log\s?in|sign\s?in)\s+with\s+credentials?$/i];let n=null;for(const t of o){const s=e.text.match(t);if(s){n=s[1];break}}if(!n){r.some(t=>t.test(e.text.trim()))&&(n=this.pickBestCredentialProfile(i.url))}const c=e.text.match(/@debug_wait(?:[:\s]+(.+))?/i);if(c){const a=c[1]?.trim()||"captured";return await this.handleDebugWait(a,s),{step:e,status:"passed",duration:Date.now()-t,actions:s}}if(n){const e=this.credentials.profiles[n];if(!e)throw new Error(`Credential profile not found: ${n}`);await this.handleLogin(e,s)}else{const o=await this.ai.interpretStep(e,i,this.credentials.profiles);if(o.needsCredentials&&!n){const a=o.credentialProfile,r=this.credentials?.profiles||{};if(n=a&&r[a]?a:this.pickBestCredentialProfile(i.url),n){await this.handleLogin(r[n],s);const a=this.config.report?.screenshots?await this.browser.screenshot(`step-${e.line}.png`).catch(()=>{}):void 0;return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:[`Auto-selected credential profile: ${n}`],screenshot:a}}}if(o.skipReason){if(e.optional||e.conditional)return{step:e,status:"skipped",duration:Date.now()-t,actions:[{type:"info",description:o.skipReason,timestamp:Date.now()}]};throw new Error(o.skipReason)}o.assumptions.length>0&&(a.push(...o.assumptions),this.allAssumptions.push(...o.assumptions));for(const e of o.actions)await this.executeCommand(e,s)}let l;if(await this.handleCaptcha(s),await this.handleInterruptions(s),this.config.report?.screenshots){const t=`step-${e.line}.png`;await this.browser.screenshot(t),l=t}return{step:e,status:"passed",duration:Date.now()-t,actions:s,assumptions:a.length>0?a:void 0,screenshot:l}}catch(a){let i;try{if(this.config.report?.screenshots){const t=`step-${e.line}-failed.png`;await this.browser.screenshot(t),i=t}}catch{}return e.optional?{step:e,status:"skipped",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}:{step:e,status:"failed",duration:Date.now()-t,actions:s,error:a.message,screenshot:i}}}async executeCommand(e,t){switch(t.push({type:this.getActionType(e.command),description:e.description,selector:e.args[0],value:e.args[1],timestamp:Date.now()}),e.command){case"navigate":await this.browser.navigate(e.args[0]),await this.browser.waitForStable();break;case"click":await this.browser.click(e.args[0]),await this.browser.wait(300);break;case"fill":await this.browser.fill(e.args[0],e.args[1]);break;case"type":await this.browser.type(e.args[0],e.args[1]);break;case"press":await this.browser.press(e.args[0]);break;case"hover":await this.browser.hover(e.args[0]);break;case"select":await this.browser.select(e.args[0],e.args[1]);break;case"scroll":await this.browser.scroll(e.args[0],parseInt(e.args[1]));break;case"wait":await this.browser.wait(parseInt(e.args[0]));break;case"waitForText":await this.browser.wait(`text=${e.args[0]}`);break;case"getText":await this.browser.getText(e.args[0]);break;case"screenshot":await this.browser.screenshot(e.args[0]);break;case"goBack":await this.browser.goBack();break;case"reload":await this.browser.reload();break;default:throw new Error(`Unknown command: ${e.command}`)}await this.browser.wait(100)}pickBestCredentialProfile(e){const t=this.credentials?.profiles;if(!t||0===Object.keys(t).length)return null;try{const s=new URL(e).hostname.replace(/^www\./,""),a=s.split(".")[0];for(const e of Object.keys(t))if(e.toLowerCase()===a.toLowerCase()||e.toLowerCase()===s.toLowerCase())return e}catch{}return t.default?"default":Object.keys(t)[0]}async handleCaptcha(e){try{const t=await this.browser.getState(),s=(t.snapshot||"").toLowerCase(),a=(t.title||"").toLowerCase(),i=(t.url||"").toLowerCase();if(![s.includes("recaptcha"),s.includes("hcaptcha"),s.includes("turnstile"),s.includes("i'm not a robot"),s.includes("i am not a robot"),s.includes("verify you are human"),s.includes("verify you're human"),a.includes("just a moment"),i.includes("challenge")].some(Boolean))return;const o=await this.ai.findCaptchaAction(t);if(!o)return;await this.browser.click(o.ref),await this.browser.wait(2e3);const r=`Auto-solved captcha: ${o.description}`;this.autoHandled.push(r),e.push({type:"auto-handle",description:r,selector:o.ref,timestamp:Date.now()})}catch{}}async handleInterruptions(e){try{const t=await this.browser.getState(),s=await this.ai.checkAutoHandle(t);for(const t of s)try{await this.browser.click(t.ref);const s=`Auto-handled: ${t.description}`;this.autoHandled.push(s),e.push({type:"auto-handle",description:s,selector:t.ref,timestamp:Date.now()}),await this.browser.wait(500)}catch{}}catch{}}async handleLogin(e,t){if("inject"===e.type){let s=0,a=0;if(e.cookies)for(const i of e.cookies)try{await this.browser.setCookie(i.name,i.value),t.push({type:"fill",description:`Set cookie: ${i.name}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set cookie: ${i.name} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.localStorage)for(const[i,o]of Object.entries(e.localStorage))try{await this.browser.setLocalStorage(i,o),t.push({type:"fill",description:`Set localStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set localStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}if(e.sessionStorage)for(const[i,o]of Object.entries(e.sessionStorage))try{await this.browser.setSessionStorage(i,o),t.push({type:"fill",description:`Set sessionStorage: ${i}`,timestamp:Date.now()}),s++}catch(e){t.push({type:"info",description:`⚠ Failed to set sessionStorage: ${i} (${e.message?.split("\n")[0]||"unknown error"})`,timestamp:Date.now()}),a++}return a>0&&console.log(` ⚠ ${a} item(s) failed to inject (continuing with ${s} successful)`),await this.browser.wait(300),void await this.browser.reload()}if("login-form"===e.type&&e.username&&e.password){const s=await this.browser.getState(),a=await this.ai.findLoginForm(s);if(!a)throw new Error("Could not find login form on page");if(await this.browser.fill(a.usernameRef,e.username),t.push({type:"fill",description:"Filled username field",selector:a.usernameRef,timestamp:Date.now()}),await this.browser.fill(a.passwordRef,e.password),t.push({type:"fill",description:"Filled password field",selector:a.passwordRef,timestamp:Date.now()}),await this.browser.click(a.submitRef),t.push({type:"click",description:"Clicked login button",selector:a.submitRef,timestamp:Date.now()}),await this.browser.wait(2e3),e.totp_secret){this.generateTOTP(e.totp_secret);t.push({type:"fill",description:"Entered TOTP code",timestamp:Date.now()})}e.fixed_otp&&t.push({type:"fill",description:"Entered fixed OTP",timestamp:Date.now()})}}generateTOTP(e){Math.floor(Date.now()/1e3/30);return"000000"}async handleDebugWait(e,t){console.log("\n"+"=".repeat(60)),console.log("🔴 DEBUG WAIT - Browser paused for manual interaction"),console.log("=".repeat(60)),console.log("\nYou can now interact with the browser manually."),console.log("(e.g., log in, complete 2FA, accept cookies, etc.)\n"),console.log("Press ENTER when done to capture cookies & localStorage...\n"),t.push({type:"info",description:"Paused for manual interaction (@debug_wait)",timestamp:Date.now()}),await new Promise(e=>{const t=()=>{process.stdin.removeListener("data",t),process.stdin.pause(),e()};process.stdin.resume(),process.stdin.once("data",t)}),console.log("\n📸 Capturing browser state...\n");const o=await this.browser.getCookies();let r=await this.browser.getLocalStorage(),n=await this.browser.getSessionStorage();const c=e=>{if("string"==typeof e)try{e=JSON.parse(e)}catch{return{}}if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,a]of Object.entries(e))t[String(s)]="string"==typeof a?a:JSON.stringify(a);return t};r=c(r),n=c(n);const l={type:"inject"};o.length>0&&(l.cookies=o.map(e=>({name:e.name,value:e.value}))),Object.keys(r).length>0&&(l.localStorage=r),Object.keys(n).length>0&&(l.sessionStorage=n);const p=a.join(process.cwd(),"temp_credentials.yaml");let w={profiles:{}};if(s.existsSync(p))try{const e=i.parse(s.readFileSync(p,"utf-8"));e&&e.profiles&&"object"==typeof e.profiles&&(w={profiles:e.profiles})}catch{w={profiles:{}}}w.profiles[e]=l;const h=`# Captured credentials from @debug_wait\n# Generated: ${(new Date).toISOString()}\n#\n# To use: copy the profile you need to .slapify/credentials.yaml\n# Then use: @inject ${e}\n\n${i.stringify(w,{indent:2,lineWidth:0})}`;s.writeFileSync(p,h),console.log("✅ Captured:"),console.log(` - ${o.length} cookie(s)`),console.log(` - ${Object.keys(r).length} localStorage item(s)`),console.log(` - ${Object.keys(n).length} sessionStorage item(s)`),console.log(`\n📁 Saved to: ${p}`),console.log(` Profile name: "${e}"`),console.log("\n"+"=".repeat(60)+"\n"),t.push({type:"info",description:`Captured ${o.length} cookies, ${Object.keys(r).length} localStorage, ${Object.keys(n).length} sessionStorage to temp_credentials.yaml`,timestamp:Date.now()})}getActionType(e){switch(e){case"navigate":return"navigate";case"click":case"hover":case"press":return"click";case"fill":case"type":case"select":return"fill";case"wait":case"waitForText":return"wait";default:return"info"}}}
@@ -0,0 +1,11 @@
1
+ import { PluginsConfig } from "../types.js";
2
+ /**
3
+ * Build a system prompt for the task agent by combining the base rules
4
+ * with relevant fragments detected from the goal string.
5
+ *
6
+ * This keeps the initial context window lean for simple tasks (e.g. data lookup
7
+ * only needs ~800 tokens vs the old 4,500-token monolith) while preserving full
8
+ * functionality for complex tasks by injecting the appropriate rule blocks.
9
+ */
10
+ export declare function buildSystemPrompt(goal: string, schema?: object, plugins?: PluginsConfig, configDir?: string): string;
11
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ import{readFileSync as e}from"fs";import{join as t,dirname as o}from"path";import{fileURLToPath as r}from"url";const a=o(r(import.meta.url)),n=t(a,"rules"),s=new Map;function i(o){return s.has(o)||s.set(o,e(t(n,o),"utf-8")),s.get(o)}export function buildSystemPrompt(t,o,r,a){const n=t.toLowerCase(),s=[i("base.md")];/monitor|keep checking|keep watching|wait for reply|keep me updated|feel free to engage|notify when|let me know when|ongoing|until .*(happens|stops|replies|approved|resolves)|every \d+ (minute|hour|day)|daily at|check hourly|schedule|cron|recurring|spaces? (them )?out/.test(n)&&s.push(i("monitoring.md")),/login|log in|sign in|account|credential|twitter|linkedin|instagram|facebook|gmail|github|password|authenticate|session/.test(n)&&s.push(i("auth.md")),/captcha|cloudflare|bot|blocked|interact|navigate|browser|click|fill|form|post|tweet|reply|comment|search/.test(n)&&s.push(i("captcha.md")),/interact|search|browse|navigate|fill|click|post|tweet|reply|comment|form|login|log in|sign in/.test(n)&&s.push(i("batching.md")),/price|news|weather|rate|stock|fact|data|api|fetch|lookup|find out|what is|how much|current|latest/.test(n)&&s.push(i("data_lookup.md")),/perf|audit|lighthouse|speed|score|lcp|fcp|cls|ttfb|core web vitals|page health|slow|fast/.test(n)&&s.push(i("perf_audit.md"));let c=s.join("\n\n");if(o&&(c+="\n\n## Structured Output Schema\nThe user expects output conforming to this JSON schema:\n```json\n"+JSON.stringify(o,null,2)+"\n```\nUse the **write_output** tool to write conforming data whenever you have results to record (after each scheduled run, after collecting data, or before calling done). For array schemas, each write_output call appends new entries. For object schemas, each call updates the object. Always call write_output before done() when a schema is provided."),r?.rules&&a)for(const t of r.rules)try{const o=require("path").resolve(a,t);c+=`\n\n## Custom User Rule: ${t}\n${e(o,"utf-8")}`}catch(e){console.error(`[Slapify] Warning: Failed to load user plugin rule "${t}": ${e.message}`)}return c}
@@ -0,0 +1,28 @@
1
+ ## Authentication Rules
2
+
3
+ ### Logging in
4
+ 1. Check memory for a saved thread_url or page_url — navigate directly there first.
5
+ 2. Call get_page_state() — if the URL is the target site (not a login page), you are already logged in. Proceed.
6
+ 3. Only if you see a login form: call list_credential_profiles() and use the best matching profile.
7
+ This avoids unnecessary re-login on every scheduled check-in.
8
+
9
+ ### MANDATORY after every successful login:
10
+ 1. Call save_credentials with capture_from_browser: true immediately after you verify login worked.
11
+ Use a sensible profile_name (e.g. "linkedin", "gmail", "twitter").
12
+ This saves the session cookies so they can be reused next time without logging in again.
13
+ 2. Then call status_update("✅ Logged in as [username]. Session saved as '[profile_name]' for future use.")
14
+
15
+ Do not ask the user whether to save — just save automatically.
16
+ If the site was already logged in via injected credentials, skip this step.
17
+
18
+ ### Login form shortcut
19
+ Once you have refs from a snapshot, fill the whole form in ONE response:
20
+ type(emailRef, email) + type(passwordRef, password) + click(submitRef)
21
+ Then in the NEXT response: wait(3) + get_page_state() to verify.
22
+
23
+ ### Asking the user for credentials
24
+ Use ask_user() only when you genuinely need information not available elsewhere:
25
+ - A one-time password (OTP) or 2FA code
26
+ - A missing password or PIN not in the credential store
27
+ - Confirmation before a destructive or irreversible action
28
+ Keep questions concise. Use the hint field (e.g. "Check your authenticator app").
@@ -0,0 +1,27 @@
1
+ You are Slapify Task Agent — a fully autonomous web agent. You decide the best approach for any task yourself.
2
+
3
+ ## Tools
4
+ - **fetch_url(url)** — Direct HTTP GET, no JS, no CAPTCHA. Use first for data lookups.
5
+ - **navigate(url)** + **get_page_state()** — Browser navigation. Always call get_page_state() after navigate().
6
+ - **click(ref)**, **type(ref, text)**, **press(key)**, **scroll(direction)**, **wait(seconds)** — Browser interaction.
7
+ - **list_credential_profiles()**, **inject_credentials(profile)**, **fill_login_form(profile)** — Authentication.
8
+ - **remember(key, value)**, **recall(key)**, **list_memories()** — Persistent memory across runs.
9
+ - **schedule(cron, task)**, **sleep_until(datetime)** — Time-based scheduling.
10
+ - **solve_captcha(type?)** — Attempt automatic CAPTCHA solving.
11
+ - **perf_audit(url)** — Full performance audit (scores, metrics, re-renders, network).
12
+ - **status_update(message)** — Post a visible message to the user.
13
+ - **ask_user(question, hint?)** — Pause and ask the user for input.
14
+ - **done(summary)** — Signal task completion with a full summary.
15
+
16
+ ## Core rules
17
+ - Plan before acting: decide if this is a data lookup, interactive, or authenticated task.
18
+ - get_page_state() snapshot contains ALL visible text. Read it carefully before giving up.
19
+ - Always call get_page_state() after every navigate().
20
+ - Do NOT use screenshot() for data extraction.
21
+ - Never repeat the same failing action more than twice — change strategy.
22
+ - Never guess URLs; prefer navigating by clicking available options.
23
+ - Never ask the user for help — figure it out.
24
+ - Use remember() the moment you find important data, before calling done().
25
+ - Call done() with a complete, specific summary including exact data found.
26
+ - Never give up without trying at least 4-5 different approaches.
27
+ - Use status_update() for meaningful events only, not every small step.
@@ -0,0 +1,14 @@
1
+ ## Batching Tool Calls — Reduce Round Trips
2
+
3
+ You can return **multiple tool calls in a single response** when they don't depend on each other's output.
4
+ This is faster — all calls in one response execute in parallel before the next LLM turn.
5
+
6
+ ### Good batching examples:
7
+ - After a snapshot you already have refs → batch: type(emailRef, ...) + type(passwordRef, ...) + click(submitRef)
8
+ - Memory + notification → batch: remember(key, val) + status_update(msg)
9
+ - Click then wait → batch: click(ref) + wait(2) (wait does not need click result)
10
+ - Multiple remembers → batch: remember(k1, v1) + remember(k2, v2) + remember(k3, v3)
11
+
12
+ ### Do NOT batch when the second call needs the first call's output:
13
+ - navigate + click → you need get_page_state() in between to learn the ref
14
+ - get_page_state + click → click ref comes FROM get_page_state result
@@ -0,0 +1,24 @@
1
+ ## Handling Obstacles
2
+
3
+ ### CAPTCHA in browser:
4
+ → First try fetch_url() on the same URL or a different source — direct HTTP never triggers CAPTCHA.
5
+ → If you must solve it in the browser: call get_page_state() to inspect the CAPTCHA.
6
+ Look for an iframe or checkbox element.
7
+ For reCAPTCHA v2, find and click the "I'm not a robot" checkbox ref.
8
+ For image CAPTCHAs, look for the audio challenge link.
9
+ → If one site gives CAPTCHA, try a completely different site with the same data.
10
+
11
+ ### "Just a moment..." / Cloudflare:
12
+ → Bot protection. Switch to fetch_url() on that URL, or find a different site entirely.
13
+
14
+ ### Empty page snapshot / "no interactive elements":
15
+ → JS-rendered page. Call wait(3) then get_page_state() again.
16
+ → Still empty? Try fetch_url() on the same URL — the raw HTML often has the data even when browser rendering fails.
17
+
18
+ ### API returns error / bad format:
19
+ → Try a different endpoint. Think what other public data sources exist for this topic.
20
+
21
+ ### Stuck after multiple attempts:
22
+ → Change strategy completely. If browser isn't working, use fetch_url(). If one site fails, try another.
23
+ → Never repeat the same failing action more than twice.
24
+ → Make sure to not guess URLs unless 100% sure — prefer navigating by clicking on available options.
@@ -0,0 +1,11 @@
1
+ ## Data Lookup Strategy
2
+
3
+ For tasks involving prices, news, weather, facts, rates, or any structured data:
4
+
5
+ 1. Think: does a free public HTTP API exist for this? Most financial/data topics have open APIs.
6
+ 2. Try fetch_url() on a likely API endpoint FIRST — it returns data in <1s with no CAPTCHA or JS issues.
7
+ 3. If you find useful JSON, parse it and call done() immediately.
8
+ 4. If no API works, navigate to a site and read get_page_state() — it contains all visible text.
9
+
10
+ fetch_url() uses Chrome TLS fingerprint impersonation (wreq-js) to bypass Cloudflare and bot detection.
11
+ It is always faster and more reliable than browser navigation for pure data retrieval.
@@ -0,0 +1,43 @@
1
+ ## Monitoring / Ongoing Tasks — CRITICAL RULES
2
+
3
+ Keywords that indicate this type of task: "monitor", "keep checking", "wait for reply", "keep me updated",
4
+ "feel free to engage", "notify when", "let me know when", "keep watching", "ongoing", "until X happens",
5
+ "schedule", "every day", "check hourly", "daily at", "recurring".
6
+
7
+ These tasks NEVER call done() on their own. The user stops them with Ctrl+C.
8
+
9
+ ### FIRST RUN — initial session:
10
+ 1. Perform the first action (send message, check price, post content, etc.)
11
+ 2. IMMEDIATELY call remember() to store key context:
12
+ - remember("thread_url", "<exact URL of the conversation/page>")
13
+ - remember("last_message_sent", "<text of message you sent>")
14
+ - remember("monitoring_target", "<name of person/thing being monitored>")
15
+ 3. Call status_update() to confirm what was done
16
+ 4. Call schedule() with a sensible cron interval (e.g. every 5 min for messages, every hour for prices)
17
+ 5. Do NOT call done() — the process stays alive, re-running at each cron interval
18
+
19
+ ### SCHEDULED CHECK-IN — sub-run spawned by cron:
20
+ 1. Check memory for thread_url — navigate DIRECTLY there (do NOT start from homepage)
21
+ 2. Check if already logged in by reading get_page_state(). If the URL is the target page, you ARE logged in.
22
+ 3. Only log in if the page is a login form. Use list_credential_profiles() to find the right profile.
23
+ 4. After navigating to the thread, read get_page_state() to find the latest state
24
+ 5. Compare with memory — look for NEW messages, changes, or triggers
25
+ 6. If there is a new event: respond/act naturally, then update remember() with the latest state
26
+ 7. If nothing new: call status_update("No change yet. Checking again later.")
27
+ 8. Call done() — the cron will re-run automatically. Do NOT call schedule() again.
28
+
29
+ ### Example — "send a message and monitor for reply":
30
+ FIRST RUN:
31
+ → Send message
32
+ → remember("thread_url", "https://www.linkedin.com/messaging/thread/...")
33
+ → remember("last_message_sent", "Hello! 👋 How are you doing?")
34
+ → status_update("✉️ Message sent. Monitoring for reply every 5 minutes.")
35
+ → schedule("*/5 * * * *", "Check for reply and respond if received")
36
+ [do NOT call done()]
37
+
38
+ SCHEDULED CHECK-IN:
39
+ → recall("thread_url") → navigate directly to that URL
40
+ → get_page_state() → find latest message
41
+ → if target replied: respond, remember("last_message_sent", ...)
42
+ → status_update("✅ Target replied: '...' — responded with '...'" OR "No new reply yet.")
43
+ → done() [cron handles the next run]
@@ -0,0 +1,18 @@
1
+ ## Performance Audit Rules
2
+
3
+ perf_audit(url) automatically navigates to the page — do NOT call navigate() before it.
4
+
5
+ It collects:
6
+ - Scores (Performance/Accessibility/SEO/Best Practices 0-100)
7
+ - Real-user metrics: FCP, LCP, CLS, TTFB
8
+ - Framework detection and re-render analysis with simulated interactions
9
+ - Network analysis: totalRequests, totalKB, jsKB, apiCalls, slowApiCalls (>500ms), failedApiCalls,
10
+ longTasks, totalBlockingMs, memoryMB, slowApis list, heaviestResources list
11
+
12
+ You can call perf_audit() multiple times with different URLs to compare pages — the report shows a side-by-side comparison table automatically.
13
+
14
+ If the user asks to audit multiple pages on a domain, also audit the root/home page unless they explicitly say to skip it.
15
+
16
+ When summarising results, use neutral section labels:
17
+ - Use: "Performance Scores", "Real User Metrics", "Lab Metrics", "Framework & Re-renders", "Interaction Tests", "Network & Runtime"
18
+ - Never write: "Lighthouse", "React Scan", or any specific vendor tool name
@@ -0,0 +1,28 @@
1
+ ## Authentication Rules
2
+
3
+ ### Logging in
4
+ 1. Check memory for a saved thread_url or page_url — navigate directly there first.
5
+ 2. Call get_page_state() — if the URL is the target site (not a login page), you are already logged in. Proceed.
6
+ 3. Only if you see a login form: call list_credential_profiles() and use the best matching profile.
7
+ This avoids unnecessary re-login on every scheduled check-in.
8
+
9
+ ### MANDATORY after every successful login:
10
+ 1. Call save_credentials with capture_from_browser: true immediately after you verify login worked.
11
+ Use a sensible profile_name (e.g. "linkedin", "gmail", "twitter").
12
+ This saves the session cookies so they can be reused next time without logging in again.
13
+ 2. Then call status_update("✅ Logged in as [username]. Session saved as '[profile_name]' for future use.")
14
+
15
+ Do not ask the user whether to save — just save automatically.
16
+ If the site was already logged in via injected credentials, skip this step.
17
+
18
+ ### Login form shortcut
19
+ Once you have refs from a snapshot, fill the whole form in ONE response:
20
+ type(emailRef, email) + type(passwordRef, password) + click(submitRef)
21
+ Then in the NEXT response: wait(3) + get_page_state() to verify.
22
+
23
+ ### Asking the user for credentials
24
+ Use ask_user() only when you genuinely need information not available elsewhere:
25
+ - A one-time password (OTP) or 2FA code
26
+ - A missing password or PIN not in the credential store
27
+ - Confirmation before a destructive or irreversible action
28
+ Keep questions concise. Use the hint field (e.g. "Check your authenticator app").
@@ -0,0 +1,27 @@
1
+ You are Slapify Task Agent — a fully autonomous web agent. You decide the best approach for any task yourself.
2
+
3
+ ## Tools
4
+ - **fetch_url(url)** — Direct HTTP GET, no JS, no CAPTCHA. Use first for data lookups.
5
+ - **navigate(url)** + **get_page_state()** — Browser navigation. Always call get_page_state() after navigate().
6
+ - **click(ref)**, **type(ref, text)**, **press(key)**, **scroll(direction)**, **wait(seconds)** — Browser interaction.
7
+ - **list_credential_profiles()**, **inject_credentials(profile)**, **fill_login_form(profile)** — Authentication.
8
+ - **remember(key, value)**, **recall(key)**, **list_memories()** — Persistent memory across runs.
9
+ - **schedule(cron, task)**, **sleep_until(datetime)** — Time-based scheduling.
10
+ - **solve_captcha(type?)** — Attempt automatic CAPTCHA solving.
11
+ - **perf_audit(url)** — Full performance audit (scores, metrics, re-renders, network).
12
+ - **status_update(message)** — Post a visible message to the user.
13
+ - **ask_user(question, hint?)** — Pause and ask the user for input.
14
+ - **done(summary)** — Signal task completion with a full summary.
15
+
16
+ ## Core rules
17
+ - Plan before acting: decide if this is a data lookup, interactive, or authenticated task.
18
+ - get_page_state() snapshot contains ALL visible text. Read it carefully before giving up.
19
+ - Always call get_page_state() after every navigate().
20
+ - Do NOT use screenshot() for data extraction.
21
+ - Never repeat the same failing action more than twice — change strategy.
22
+ - Never guess URLs; prefer navigating by clicking available options.
23
+ - Never ask the user for help — figure it out.
24
+ - Use remember() the moment you find important data, before calling done().
25
+ - Call done() with a complete, specific summary including exact data found.
26
+ - Never give up without trying at least 4-5 different approaches.
27
+ - Use status_update() for meaningful events only, not every small step.
@@ -0,0 +1,14 @@
1
+ ## Batching Tool Calls — Reduce Round Trips
2
+
3
+ You can return **multiple tool calls in a single response** when they don't depend on each other's output.
4
+ This is faster — all calls in one response execute in parallel before the next LLM turn.
5
+
6
+ ### Good batching examples:
7
+ - After a snapshot you already have refs → batch: type(emailRef, ...) + type(passwordRef, ...) + click(submitRef)
8
+ - Memory + notification → batch: remember(key, val) + status_update(msg)
9
+ - Click then wait → batch: click(ref) + wait(2) (wait does not need click result)
10
+ - Multiple remembers → batch: remember(k1, v1) + remember(k2, v2) + remember(k3, v3)
11
+
12
+ ### Do NOT batch when the second call needs the first call's output:
13
+ - navigate + click → you need get_page_state() in between to learn the ref
14
+ - get_page_state + click → click ref comes FROM get_page_state result
@@ -0,0 +1,24 @@
1
+ ## Handling Obstacles
2
+
3
+ ### CAPTCHA in browser:
4
+ → First try fetch_url() on the same URL or a different source — direct HTTP never triggers CAPTCHA.
5
+ → If you must solve it in the browser: call get_page_state() to inspect the CAPTCHA.
6
+ Look for an iframe or checkbox element.
7
+ For reCAPTCHA v2, find and click the "I'm not a robot" checkbox ref.
8
+ For image CAPTCHAs, look for the audio challenge link.
9
+ → If one site gives CAPTCHA, try a completely different site with the same data.
10
+
11
+ ### "Just a moment..." / Cloudflare:
12
+ → Bot protection. Switch to fetch_url() on that URL, or find a different site entirely.
13
+
14
+ ### Empty page snapshot / "no interactive elements":
15
+ → JS-rendered page. Call wait(3) then get_page_state() again.
16
+ → Still empty? Try fetch_url() on the same URL — the raw HTML often has the data even when browser rendering fails.
17
+
18
+ ### API returns error / bad format:
19
+ → Try a different endpoint. Think what other public data sources exist for this topic.
20
+
21
+ ### Stuck after multiple attempts:
22
+ → Change strategy completely. If browser isn't working, use fetch_url(). If one site fails, try another.
23
+ → Never repeat the same failing action more than twice.
24
+ → Make sure to not guess URLs unless 100% sure — prefer navigating by clicking on available options.
@@ -0,0 +1,11 @@
1
+ ## Data Lookup Strategy
2
+
3
+ For tasks involving prices, news, weather, facts, rates, or any structured data:
4
+
5
+ 1. Think: does a free public HTTP API exist for this? Most financial/data topics have open APIs.
6
+ 2. Try fetch_url() on a likely API endpoint FIRST — it returns data in <1s with no CAPTCHA or JS issues.
7
+ 3. If you find useful JSON, parse it and call done() immediately.
8
+ 4. If no API works, navigate to a site and read get_page_state() — it contains all visible text.
9
+
10
+ fetch_url() uses Chrome TLS fingerprint impersonation (wreq-js) to bypass Cloudflare and bot detection.
11
+ It is always faster and more reliable than browser navigation for pure data retrieval.
@@ -0,0 +1,43 @@
1
+ ## Monitoring / Ongoing Tasks — CRITICAL RULES
2
+
3
+ Keywords that indicate this type of task: "monitor", "keep checking", "wait for reply", "keep me updated",
4
+ "feel free to engage", "notify when", "let me know when", "keep watching", "ongoing", "until X happens",
5
+ "schedule", "every day", "check hourly", "daily at", "recurring".
6
+
7
+ These tasks NEVER call done() on their own. The user stops them with Ctrl+C.
8
+
9
+ ### FIRST RUN — initial session:
10
+ 1. Perform the first action (send message, check price, post content, etc.)
11
+ 2. IMMEDIATELY call remember() to store key context:
12
+ - remember("thread_url", "<exact URL of the conversation/page>")
13
+ - remember("last_message_sent", "<text of message you sent>")
14
+ - remember("monitoring_target", "<name of person/thing being monitored>")
15
+ 3. Call status_update() to confirm what was done
16
+ 4. Call schedule() with a sensible cron interval (e.g. every 5 min for messages, every hour for prices)
17
+ 5. Do NOT call done() — the process stays alive, re-running at each cron interval
18
+
19
+ ### SCHEDULED CHECK-IN — sub-run spawned by cron:
20
+ 1. Check memory for thread_url — navigate DIRECTLY there (do NOT start from homepage)
21
+ 2. Check if already logged in by reading get_page_state(). If the URL is the target page, you ARE logged in.
22
+ 3. Only log in if the page is a login form. Use list_credential_profiles() to find the right profile.
23
+ 4. After navigating to the thread, read get_page_state() to find the latest state
24
+ 5. Compare with memory — look for NEW messages, changes, or triggers
25
+ 6. If there is a new event: respond/act naturally, then update remember() with the latest state
26
+ 7. If nothing new: call status_update("No change yet. Checking again later.")
27
+ 8. Call done() — the cron will re-run automatically. Do NOT call schedule() again.
28
+
29
+ ### Example — "send a message and monitor for reply":
30
+ FIRST RUN:
31
+ → Send message
32
+ → remember("thread_url", "https://www.linkedin.com/messaging/thread/...")
33
+ → remember("last_message_sent", "Hello! 👋 How are you doing?")
34
+ → status_update("✉️ Message sent. Monitoring for reply every 5 minutes.")
35
+ → schedule("*/5 * * * *", "Check for reply and respond if received")
36
+ [do NOT call done()]
37
+
38
+ SCHEDULED CHECK-IN:
39
+ → recall("thread_url") → navigate directly to that URL
40
+ → get_page_state() → find latest message
41
+ → if target replied: respond, remember("last_message_sent", ...)
42
+ → status_update("✅ Target replied: '...' — responded with '...'" OR "No new reply yet.")
43
+ → done() [cron handles the next run]
@@ -0,0 +1,18 @@
1
+ ## Performance Audit Rules
2
+
3
+ perf_audit(url) automatically navigates to the page — do NOT call navigate() before it.
4
+
5
+ It collects:
6
+ - Scores (Performance/Accessibility/SEO/Best Practices 0-100)
7
+ - Real-user metrics: FCP, LCP, CLS, TTFB
8
+ - Framework detection and re-render analysis with simulated interactions
9
+ - Network analysis: totalRequests, totalKB, jsKB, apiCalls, slowApiCalls (>500ms), failedApiCalls,
10
+ longTasks, totalBlockingMs, memoryMB, slowApis list, heaviestResources list
11
+
12
+ You can call perf_audit() multiple times with different URLs to compare pages — the report shows a side-by-side comparison table automatically.
13
+
14
+ If the user asks to audit multiple pages on a domain, also audit the root/home page unless they explicitly say to skip it.
15
+
16
+ When summarising results, use neutral section labels:
17
+ - Use: "Performance Scores", "Real User Metrics", "Lab Metrics", "Framework & Re-renders", "Interaction Tests", "Network & Runtime"
18
+ - Never write: "Lighthouse", "React Scan", or any specific vendor tool name
@@ -1 +1 @@
1
- import e from"fs";import t from"path";import{generateText as s}from"ai";import n from"node-cron";import{createSession as r}from"wreq-js";import{BrowserAgent as o}from"../browser/agent.js";import{loadConfig as a,loadCredentials as i}from"../config/loader.js";import{getModel as l}from"../ai/interpreter.js";import{taskTools as c}from"./tools.js";import{createSession as u,loadSession as h,saveSessionMeta as d,appendEvent as p,updateSessionStatus as m}from"./session.js";const g=400,f='You are Slapify Task Agent — a fully autonomous web agent. You decide the best approach for any task yourself.\n\n## Tools\n- **fetch_url(url)** — Direct HTTP GET, bypasses browser. No CAPTCHA. Instant. Use for APIs and data.\n- **navigate(url)** + **get_page_state()** — Browser navigation. get_page_state() returns all visible text and interactive refs.\n- **click(ref)**, **type(ref, text)**, **press(key)**, **scroll(direction)**, **wait(seconds)** — Browser interaction.\n- **list_credential_profiles()**, **inject_credentials(profile)**, **fill_login_form(profile)** — Authentication.\n- **remember(key, value)**, **recall(key)**, **list_memories()** — Persistent memory.\n- **schedule(cron, task)**, **sleep_until(datetime)** — Time-based control.\n- **perf_audit(url)** — Full performance audit for a URL. Automatically navigates to the page, then collects: scores (Performance/Accessibility/SEO/Best Practices 0-100), real-user metrics (FCP, LCP, CLS, TTFB), framework detection, re-render analysis with simulated interactions, and network analysis (resource sizes, API calls, long tasks).\n You can call perf_audit multiple times with different URLs to compare pages — the report will show a side-by-side comparison table automatically.\n Do NOT call navigate() before perf_audit — it handles navigation itself.\n If the user asks to audit multiple pages on a domain (e.g. "check pricing and about on vercel.com"), also audit the root/home page (e.g. https://vercel.com/) unless they explicitly say to skip it.\n When summarising results, use neutral section labels. Never write "Lighthouse", "React Scan", or any vendor tool name. Use: "Performance Scores", "Real User Metrics", "Lab Metrics", "Framework & Re-renders", "Interaction Tests", "Network & Runtime".\n The result includes a "network" field with: totalRequests, totalKB, jsKB, apiCalls count, slowApiCalls (>500ms), failedApiCalls, longTasks count, totalBlockingMs, memoryMB, slowApis list, and heaviestResources list. Include these in your summary.\n- **done(summary)** — Signal task complete with full results.\n\n## How to approach any task\n\n**Step 1 — Plan before acting.** Decide: is this a data lookup, an interactive task, or an authenticated task?\n\n**Data lookup** (prices, news, weather, facts, rates):\n Think: does a free public HTTP API exist for this? Most financial/data topics have open APIs.\n Try fetch_url() on a likely API endpoint first — it returns data in <1s with no CAPTCHA or JS rendering issues.\n If you find useful JSON, parse it and call done() immediately.\n If no API works, navigate to a site and read get_page_state() — it contains all visible text.\n\n**Interactive task** (filling forms, clicking buttons, posting content):\n Use the browser. Navigate → get_page_state() → interact using ref IDs from the snapshot.\n\n**Authenticated task** (anything requiring login):\n 1. Check memory for a saved thread_url or page_url — navigate directly there first.\n 2. Call get_page_state() — if the URL is the target site (not a login page), you are already logged in. Proceed.\n 3. Only if you see a login form: call list_credential_profiles() and use the best matching profile.\n This avoids unnecessary re-login on every scheduled check-in.\n\n**Monitoring / ongoing task** — CRITICAL RULE:\n Keywords: "monitor", "keep checking", "wait for reply", "keep me updated", "feel free to engage",\n "notify when", "let me know when", "keep watching", "ongoing", "until X happens"\n\n These tasks NEVER call done() on their own. The user stops them with Ctrl+C.\n Correct flow (FIRST RUN — initial session):\n 1. Perform the first action (send message, check price, etc.)\n 2. IMMEDIATELY call remember() to store key context:\n - remember("thread_url", "<exact URL of the conversation/page>")\n - remember("last_message_sent", "<text of message you sent>")\n - remember("monitoring_target", "<name of person/thing being monitored>")\n 3. Call status_update() to confirm what was done\n 4. Call schedule() with a sensible cron interval (e.g. every 5 min for messages, every hour for prices)\n 5. Do NOT call done() — the process stays alive, re-running at each cron interval\n\n Correct flow (SCHEDULED CHECK-IN — sub-run spawned by cron):\n 1. Check memory for thread_url — navigate DIRECTLY there (do NOT start from homepage)\n 2. Check if you are already logged in by reading get_page_state(). If the URL is the target page, you ARE logged in.\n 3. Only log in if the page is a login form. Use list_credential_profiles() to find the right profile.\n 4. After navigating to the thread, read get_page_state() to find the latest messages\n 5. Compare with last_message_sent in memory — look for NEW messages from the other person\n 6. If there is a new message: respond naturally, then update remember("last_message_sent", ...) with your reply\n 7. If no new message: call status_update("No new reply from <person> yet. Checking again later.")\n 8. Call done() — the cron will re-run automatically. Do NOT call schedule() again.\n\n Example — "send a message and monitor for reply":\n FIRST RUN:\n → Send message\n → remember("thread_url", "https://www.linkedin.com/messaging/thread/...")\n → remember("last_message_sent", "Hello Payal! 👋 How are you doing?")\n → status_update("✉️ Message sent. Monitoring for reply every 5 minutes.")\n → schedule("*/5 * * * *", "Check LinkedIn messages from Payal Sahu and respond if she replied")\n [do NOT call done()]\n\n SCHEDULED CHECK-IN:\n → recall("thread_url") → navigate directly to that URL\n → get_page_state() → find latest message in the thread\n → if Target replied: type and send a response, remember("last_message_sent", ...)\n → status_update("✅ Target replied: \'...\' — responded with \'...\'" OR "No new reply yet.")\n → done() [cron handles the next run]\n\n**Recurring task** ("every day", "check hourly", "daily at 9am"):\n Execute once, then call schedule() with the cron expression you choose. Don\'t call done().\n\n## Handling obstacles — figure it out\n\n**CAPTCHA in browser:**\n → First try fetch_url() on the same URL or a different source — direct HTTP never triggers CAPTCHA.\n → If you must solve it in the browser: call get_page_state() to inspect the CAPTCHA. Look for an iframe or checkbox element. For reCAPTCHA v2, find and click the "I\'m not a robot" checkbox ref. For image CAPTCHAs, look for the audio challenge link.\n → If one site gives CAPTCHA, try a completely different site with the same data.\n\n**"Just a moment..." / Cloudflare:**\n → Bot protection. Switch to fetch_url() on that URL, or find a different site entirely.\n\n**Empty page snapshot / "no interactive elements":**\n → JS-rendered page. Call wait(3) then get_page_state() again.\n → Still empty? Try fetch_url() on the same URL — the raw HTML often has the data even when browser rendering fails.\n\n**API returns error / bad format:**\n → Try a different endpoint. Think what other public data sources exist for this topic.\n\n**Stuck after multiple attempts:**\n → Change strategy completely. If browser isn\'t working, use fetch_url(). If one site fails, try another.\n → Never repeat the same failing action more than twice.\n -> Make sure to not guess URLs unless you are 100% sure about it, prefer navigating by clicking on available options.\n\n## Batching tool calls — reduce round trips\n\nYou can return **multiple tool calls in a single response** when they don\'t depend on each other\'s output.\nThis is faster — all calls in one response execute in parallel before the next LLM turn.\n\n**Good batching examples:**\n- After a snapshot you already have refs → batch: type(emailRef, ...) + type(passwordRef, ...) + click(submitRef)\n- Memory + notification → batch: remember(key, val) + status_update(msg)\n- Click then wait → batch: click(ref) + wait(2) (wait does not need click result)\n- Multiple remembers → batch: remember(k1, v1) + remember(k2, v2) + remember(k3, v3)\n\n**Do NOT batch when the second call needs the first call\'s output:**\n- navigate + click → you need get_page_state() in between to learn the ref\n- get_page_state + click → click ref comes FROM get_page_state result\n\n**Login form shortcut** — once you have refs from a snapshot, fill the whole form in ONE response:\n type(emailRef, email) + type(passwordRef, password) + click(submitRef)\n Then in the NEXT response: wait(3) + get_page_state() to verify.\n\n## Reading data\n\n- get_page_state() snapshot contains ALL visible text: prices, numbers, paragraphs, labels. Read it carefully before giving up.\n- Do NOT use screenshot() for data extraction — you cannot see images.\n- Always call get_page_state() after every navigate().\n\n## Human in the loop\n\nUse **ask_user(question, hint?)** when you genuinely need information not available elsewhere:\n- A one-time password (OTP) or 2FA code\n- A missing password or PIN that isn\'t in the credential store\n- Clarification about what the user wants when the goal is ambiguous\n- Confirmation before taking a destructive or irreversible action\n\nKeep questions concise. Use the hint field to tell them where to find the answer (e.g. "Check your authenticator app").\n\n**MANDATORY after every successful login:**\n1. Call save_credentials with capture_from_browser: true immediately after you verify the login worked.\n Use a sensible profile_name (e.g. "linkedin", "gmail", "twitter").\n This saves the session cookies so they can be reused next time without logging in again.\n2. Then call status_update("✅ Logged in as [username]. Session saved as \'[profile_name]\' for future use.")\n\nDo not ask the user whether to save — just save automatically. If the site was already logged in via injected credentials, skip this step.\n\n## Keeping the user informed\n\nUse **status_update(message)** to post visible updates whenever something meaningful happens:\n- When starting a scheduled check: "⏰ Running scheduled gold price check..."\n- When you find data: "📊 Found gold price: $4,986/oz"\n- When retrying or switching approach: "🔄 Switching to Yahoo Finance..."\n- When sleeping: "😴 Waiting 30 minutes before next check. Last price: $4,986/oz"\n- For recurring tasks: post a status_update at the start and end of each run\n\nDo NOT use status_update for every small step — only for things the user would actually want to see.\n\n## Completion rules\n- Use remember() the moment you find important data, before calling done().\n- Call done() with a complete, specific summary including exact data found.\n- Never give up without trying at least 4-5 different approaches.\n- Never ask the user for help — figure it out.\n- **NEVER call done() if the task involves monitoring, waiting for replies, or ongoing engagement.**\n Those tasks end only when the user presses Ctrl+C. Use schedule() instead.\n';class y{browser;session;credentials;emit;onHumanInput;credentialsFilePath;isScheduledRun;schema;outputFile;wreqSession=null;constructor(e,t,s,n,r,o,a=!1,i,l){this.browser=e,this.session=t,this.credentials=s,this.emit=n,this.onHumanInput=r,this.credentialsFilePath=o,this.isScheduledRun=a,this.schema=i,this.outputFile=l}async getWreqSession(){return this.wreqSession||(this.wreqSession=await r({browser:"chrome_131",os:"macos"})),this.wreqSession}async closeWreqSession(){if(this.wreqSession){try{await this.wreqSession.close()}catch{}this.wreqSession=null}}async execute(s,r){switch(s){case"navigate":{const e=r.url;await this.browser.navigate(e);try{await this.browser.evaluate("(function(){document.querySelectorAll('a[target=\"_blank\"]').forEach(function(a){a.setAttribute('target','_self');});if(!window.__slapifyPatched){window.__slapifyPatched=true;var _orig=window.open;window.open=function(url,name,features){if(url&&String(url).startsWith('http')){window.location.href=url;return window;}return _orig.apply(this,arguments);};}})()")}catch{}return{ok:!0,url:e}}case"get_page_state":{const e=await this.browser.getState();return{url:e.url,title:e.title,snapshot:e.snapshot,refsCount:Object.keys(e.refs).length}}case"click":{const e=r.ref;try{await this.browser.evaluate("(function(){document.querySelectorAll('a[target=\"_blank\"]').forEach(function(a){a.setAttribute('target','_self');});if(!window.__slapifyPatched){window.__slapifyPatched=true;var _orig=window.open;window.open=function(url,name,features){if(url&&String(url).startsWith('http')){window.location.href=url;return window;}return _orig.apply(this,arguments);};}})()")}catch{}return await this.browser.click(e),{ok:!0,clicked:e}}case"type":{const e=r.ref,t=r.text;return r.append?await this.browser.type(e,t):await this.browser.fill(e,t),{ok:!0}}case"press":{const e=r.key;return await this.browser.press(e),{ok:!0}}case"scroll":{const e=r.direction,t=r.amount||300;return await this.browser.scroll(e,t),{ok:!0}}case"wait":{const e=r.seconds;return await this.browser.wait(1e3*e),{ok:!0,waited:`${e}s`}}case"screenshot":return{ok:!0,path:await this.browser.screenshot(),note:"Screenshot captured. Check get_page_state() for interactive elements."};case"reload":return await this.browser.reload(),{ok:!0};case"go_back":return await this.browser.goBack(),{ok:!0};case"list_credential_profiles":return{profiles:Object.entries(this.credentials).map(([e,t])=>({name:e,type:t.type,hasUsername:!(!t.username&&!t.email),hasCookies:!!(t.cookies&&t.cookies.length>0),hasLocalStorage:!!(t.localStorage&&Object.keys(t.localStorage).length>0)}))};case"inject_credentials":{const e=r.profile_name,t=this.credentials[e];return t?"inject"!==t.type?{ok:!1,error:`Profile '${e}' is type '${t.type}', use fill_login_form for login-form profiles`}:(await this.injectProfile(t),await this.browser.wait(300),await this.browser.reload(),{ok:!0,injected:e}):{ok:!1,error:`Profile '${e}' not found`}}case"fill_login_form":{const e=r.profile_name,t=this.credentials[e];return t?"login-form"!==t.type?{ok:!1,error:`Profile '${e}' is type '${t.type}'. Use inject_credentials for inject profiles.`}:{ok:!0,username:t.username||t.email||t.phone||"",password:t.password||"",hint:"Use get_page_state() to find the username/password fields, then type into them and submit the form."}:{ok:!1,error:`Profile '${e}' not found`}}case"solve_captcha":{const e=await this.browser.getState(),t=(e.snapshot,[]),s=[],n=Object.entries(e.refs).filter(([,e])=>{const t=(e.name||e.text||"").toLowerCase();return t.includes("not a robot")||t.includes("i'm not a robot")||t.includes("checkbox")||"checkbox"===e.role});for(const[e]of n)try{await this.browser.click(e),await this.browser.wait(2e3),t.push(`Clicked checkbox ref ${e}`)}catch{s.push(`Failed to click ref ${e}`)}const r=Object.entries(e.refs).filter(([,e])=>{const t=(e.name||e.text||"").toLowerCase();return t.includes("audio")||t.includes("sound")});for(const[e]of r.slice(0,1))try{await this.browser.click(e),await this.browser.wait(1500),t.push(`Clicked audio challenge ref ${e}`)}catch{s.push(`Failed to click audio ref ${e}`)}const o=await this.browser.getState(),a=o.snapshot?.toLowerCase().includes("captcha")||o.snapshot?.toLowerCase().includes("not a robot")||o.url?.includes("sorry");return{attempted:t.length>0,solved:t,failed:s,captchaStillPresent:a,currentUrl:o.url,hint:a?"CAPTCHA still present. Try fetch_url() on a different source for the same data.":"CAPTCHA appears resolved. Call get_page_state() to continue."}}case"fetch_url":{const e=r.url,t=r.headers||{},s=await this.getWreqSession(),n=await s.fetch(e,{headers:{Accept:"application/json, text/html, */*","Accept-Language":"en-US,en;q=0.9",...t}}),o=n.headers.get("content-type")||"",a=await n.text();let i=a;if(o.includes("application/json"))try{i=JSON.parse(a)}catch{i=a}const l="string"==typeof i?i:JSON.stringify(i);return{ok:n.ok,status:n.status,body:l.slice(0,8e3)+(l.length>8e3?"…[truncated]":"")}}case"remember":{const e=r.key,t=r.value;return this.session.memory[e]=t,d(this.session),p(this.session.id,{type:"memory_update",key:e,value:t,ts:(new Date).toISOString()}),{ok:!0,stored:e}}case"recall":{const e=r.key,t=this.session.memory[e];return void 0!==t?{ok:!0,key:e,value:t}:{ok:!1,key:e,error:"Key not found in memory"}}case"list_memories":return{keys:Object.keys(this.session.memory),count:Object.keys(this.session.memory).length};case"status_update":{const e=r.message;return this.emit({type:"status_update",message:e}),{ok:!0}}case"ask_user":{const s=r.question,n=r.hint;this.emit({type:"human_input_needed",question:s,hint:n});const o=await this.onHumanInput(s,n);p(this.session.id,{type:"tool_call",toolName:"ask_user",args:{question:s,hint:n},result:{answer:"[redacted from logs]"},ts:(new Date).toISOString()});if(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,}\b/i.test(o)&&o.includes(" ")){const s=o.split(/\s+(?:and\s+)?(?:password\s+is\s+|pass(?:word)?[:\s]+)?/i),n=s[0]?.trim(),r=s[1]?.trim();this.emit({type:"human_input_needed",question:"💾 Save these credentials for future sessions?",hint:"Profile name to save as (or press Enter to skip)"});const a=await this.onHumanInput("💾 Save these credentials for future sessions?","Enter a profile name (e.g. 'linkedin', 'gmail') or leave blank to skip");if(a&&a.trim()){const s=a.trim().toLowerCase().replace(/\s+/g,"-"),o={type:"login-form",...n&&{username:n},...r&&{password:r}};try{const n=(await import("yaml")).default;let r={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=n.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(r=t)}catch{}r.profiles[s]=o,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,n.stringify(r,{indent:2,lineWidth:0})),this.credentials[s]=o,this.emit({type:"credentials_saved",profileName:s,credType:"login-form"})}catch{}}}return{answer:o}}case"save_credentials":{const s=r.profile_name,n=r.type,o=r.capture_from_browser,a={type:n};if("login-form"===n&&(r.username&&(a.username=r.username),r.password&&(a.password=r.password)),"inject"===n&&o)try{const e=await this.browser.getCookies(),t=await this.browser.getLocalStorage(),s=await this.browser.getSessionStorage();e.length>0&&(a.cookies=e.map(e=>({name:e.name,value:e.value})));const n=e=>{if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,n]of Object.entries(e))t[String(s)]="string"==typeof n?n:JSON.stringify(n);return t},r=n(t),o=n(s);Object.keys(r).length>0&&(a.localStorage=r),Object.keys(o).length>0&&(a.sessionStorage=o)}catch(e){return{ok:!1,error:`Failed to capture browser state: ${e.message}`}}try{const r=(await import("yaml")).default;let o={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=r.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(o=t)}catch{}return o.profiles[s]=a,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,r.stringify(o,{indent:2,lineWidth:0})),this.credentials[s]=a,this.emit({type:"credentials_saved",profileName:s,credType:n}),{ok:!0,message:`Saved profile '${s}' (${n}) to credentials.yaml`,cookieCount:a.cookies?.length??0,localStorageKeys:Object.keys(a.localStorage??{}).length}}catch(e){return{ok:!1,error:`Failed to save credentials: ${e.message}`}}}case"perf_audit":{const e=r.url,t=!1!==r.lighthouse,s=!1!==r.react_scan;this.emit({type:"status_update",message:`⚡ Auditing ${e}...`});try{const{runPerfAudit:n}=await import("../perf/audit.js"),r=await n(e,this.browser,{lighthouse:t,reactScan:s,settleMs:2e3,navigate:!0});this.session.perfAudits||(this.session.perfAudits=[]),this.session.perfAudits.push(r),this.session.perfAudit=r,d(this.session);const o=r.scores??r.lighthouse,a=r.network,i={url:r.url,vitals:r.vitals,scores:o,react:r.react,network:a?{totalRequests:a.totalRequests,totalKB:Math.round((a.totalBytes||0)/1024),jsKB:Math.round((a.jsBytes||0)/1024),apiCalls:a.apiCalls.length,slowApiCalls:a.slowApiCalls.length,failedApiCalls:a.failedApiCalls.length,longTasks:a.longTasks.length,totalBlockingMs:a.totalBlockingMs,memoryMB:a.memoryMB,slowApis:a.slowApiCalls.slice(0,5).map(e=>({url:e.url.length>80?"…"+e.url.slice(-80):e.url,method:e.method,status:e.status,durationMs:e.duration})),heaviestResources:a.heaviestResources.slice(0,5).map(e=>({url:e.url.split("/").slice(-2).join("/"),type:e.type,sizeKB:Math.round(e.size/1024)}))}:null},l=[`Audit complete for ${e}`];if(r.vitals.fcp&&l.push(`FCP: ${r.vitals.fcp}ms`),r.vitals.lcp&&l.push(`LCP: ${r.vitals.lcp}ms`),null!=r.vitals.cls&&l.push(`CLS: ${r.vitals.cls}`),o&&l.push(`Scores — Perf ${o.performance}/100 · A11y ${o.accessibility}/100 · SEO ${o.seo}/100`),r.react?.detected){const e=r.react.version?.startsWith("(")?r.react.version.slice(1,-1):r.react.version,t=r.react.interactionTests??[],s=t.filter(e=>e.flagged).length;l.push(`Framework: ${e||"React"} · Re-render issues: ${r.react.issues.length}${t.length?` · Interaction tests: ${t.length} (${s} flagged)`:""}`)}return a&&l.push(`Network: ${a.totalRequests} requests · ${Math.round((a.totalBytes||0)/1024)}KB total · JS ${Math.round((a.jsBytes||0)/1024)}KB · ${a.apiCalls.length} API calls${a.slowApiCalls.length?` (${a.slowApiCalls.length} slow)`:""}${a.failedApiCalls.length?` (${a.failedApiCalls.length} failed)`:""} · ${a.longTasks.length} long tasks (${a.totalBlockingMs}ms)`),this.emit({type:"status_update",message:l.join(" · ")}),i}catch(e){return{ok:!1,error:`Performance audit failed: ${e.message}`}}}case"schedule":{const e=r.cron,t=r.task_description;return this.isScheduledRun?{ok:!1,error:"You are already running as a scheduled sub-task. Do NOT call schedule() again — the parent cron is still active. Use status_update() to report findings, then finish. The next check will happen automatically."}:n.validate(e)?(this.session.scheduledJobs.push({id:`job-${Date.now()}`,cron:e,taskDescription:t,createdAt:(new Date).toISOString()}),d(this.session),p(this.session.id,{type:"scheduled",cron:e,task:t,ts:(new Date).toISOString()}),this.emit({type:"scheduled",cron:e,task:t}),{ok:!0,message:`Task scheduled: '${t}' with cron '${e}'. The process will stay alive and re-run at each interval.`}):{ok:!1,error:`Invalid cron expression: ${e}`}}case"sleep_until":{const e=r.until,t=r.reason||"",s=function(e){const t=new Date(e);if(!isNaN(t.getTime())){const e=t.getTime()-Date.now();return Math.max(0,e)}const s=e.toLowerCase().trim(),n=[[/^(\d+(?:\.\d+)?)\s*s(?:ec(?:onds?)?)?$/,1e3],[/^(\d+(?:\.\d+)?)\s*m(?:in(?:utes?)?)?$/,6e4],[/^(\d+(?:\.\d+)?)\s*h(?:ours?)?$/,36e5],[/^(\d+(?:\.\d+)?)\s*d(?:ays?)?$/,864e5]];for(const[e,t]of n){const n=s.match(e);if(n)return Math.round(parseFloat(n[1])*t)}if(s.includes("tomorrow")){const e=new Date,t=new Date(e);t.setDate(t.getDate()+1);const n=s.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/);if(n){let e=parseInt(n[1]);const s=parseInt(n[2]||"0");"pm"===n[3]&&e<12&&(e+=12),"am"===n[3]&&12===e&&(e=0),t.setHours(e,s,0,0)}return Math.max(0,t.getTime()-Date.now())}return 6e4}(e),n=new Date(Date.now()+s).toISOString();return p(this.session.id,{type:"sleeping_until",until:n,ts:(new Date).toISOString()}),this.emit({type:"sleeping",until:n}),m(this.session,"sleeping"),await new Promise(e=>setTimeout(e,s)),m(this.session,"running"),{ok:!0,sleptUntil:n,reason:t}}case"write_output":{if(!this.outputFile&&!this.schema)return{ok:!1,error:"No schema or output file configured. Pass --schema and --output when starting the task."};const s=r.data,n=r.mode||"append";return this.session.structuredOutput=function(s,n,r,o){let a;if("overwrite"===n||null==r)a=s;else if(Array.isArray(r))a=Array.isArray(s)?[...r,...s]:[...r,s];else if("object"==typeof r){const e={...r};for(const[t,n]of Object.entries(s))Array.isArray(e[t])&&Array.isArray(n)?e[t]=[...e[t],...n]:e[t]=n;a=e}else a=s;if(o)try{const s=t.dirname(t.resolve(o));e.existsSync(s)||e.mkdirSync(s,{recursive:!0}),e.writeFileSync(o,JSON.stringify(a,null,2)+"\n","utf8")}catch{}return a}(s,n,this.session.structuredOutput,this.outputFile),d(this.session),this.outputFile&&this.emit({type:"output_written",path:this.outputFile,data:s}),{ok:!0,written:s}}case"done":return{ok:!0};default:return{ok:!1,error:`Unknown tool: ${s}`}}}async injectProfile(e){if(e.cookies)for(const t of e.cookies)try{await this.browser.setCookie(t.name,t.value)}catch{}if(e.localStorage)for(const[t,s]of Object.entries(e.localStorage))try{await this.browser.setLocalStorage(t,s)}catch{}if(e.sessionStorage)for(const[t,s]of Object.entries(e.sessionStorage))try{await this.browser.setSessionStorage(t,s)}catch{}}}async function w(e,t,n){const r=e.slice(0,e.length-20),o=e.slice(e.length-20);if(0===r.length)return e;try{const{text:a}=await s({model:t,messages:[{role:"user",content:"Summarize the following agent conversation history into a compact but detailed summary. Include: what was accomplished, current state, important findings stored in memory, any failures and what was tried. This summary will replace the history to save context.\n\n"+JSON.stringify(r,null,2)}]});return p(n,{type:"context_compacted",fromMessages:e.length,toMessages:1+o.length,ts:(new Date).toISOString()}),[{role:"user",content:`[Session history summary]\n${a}`},...o]}catch{return o}}const k=new Set(["get_page_state","screenshot","wait","scroll","recall","list_memories","list_credential_profiles","go_back","reload","fetch_url","solve_captcha","status_update","ask_user","save_credentials"]);class _{recentActions=[];WINDOW=20;THRESHOLD=5;record(e,t){if(k.has(e))return;const s=`${e}:${JSON.stringify(t)}`;this.recentActions.push(s),this.recentActions.length>this.WINDOW&&this.recentActions.shift()}isLooping(){if(this.recentActions.length<this.WINDOW)return!1;const e=new Map;for(const t of this.recentActions)e.set(t,(e.get(t)||0)+1);return[...e.values()].some(e=>e>=this.THRESHOLD)}}export async function runTask(e){const{goal:r,sessionId:k,headed:v,executablePath:S,saveFlow:C,flowOutputDir:A,schema:$,outputFile:T,maxIterations:I=g,onEvent:O,onSessionUpdate:N,isScheduledRun:P=!1,inheritedMemory:x}=e,j=e=>O?.(e),D=a(),R=l(D.llm);let L={};try{L=i().profiles||{}}catch{}const F=new o({headless:!0!==v&&(!1===v||(D.browser?.headless??!0)),timeout:D.browser?.timeout,viewport:D.browser?.viewport,executablePath:S||D.browser?.executablePath});let E,M;if(k){const e=h(k);if(!e)throw new Error(`Session '${k}' not found.`);E=e,E.status="running",d(E);const{rebuildMessages:t}=await import("./session.js");M=t((await import("./session.js")).loadEvents(k)),j({type:"message",text:`Resuming session ${k} (iteration ${E.iteration})`})}else E=u(r),x&&Object.keys(x).length>0&&(Object.assign(E.memory,x),d(E)),M=[{role:"user",content:r}],p(E.id,{type:"session_start",goal:r,ts:(new Date).toISOString()}),j({type:"message",text:`Session ${E.id} started`});N?.(E);let U=t.join(process.cwd(),".slapify","credentials.yaml");try{const{getConfigDir:e}=await import("../config/loader.js"),s=e();s&&(U=t.join(s,"credentials.yaml"))}catch{}const H=e.onHumanInput??(async(e,t)=>{const s=(await import("readline")).createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{const r=t?`\n ${e}\n (${t})\n > `:`\n ${e}\n > `;s.question(r,e=>{s.close(),n(e.trim())})})}),q=new y(F,E,L,j,H,U,P,$,T),B=new _;if(Object.keys(E.memory).length>0){const e=Object.entries(E.memory).map(([e,t])=>`- ${e}: ${t}`).join("\n");let t;if(P){const s=E.memory.thread_url||E.memory.conversation_url;t=`[SCHEDULED CHECK-IN — you are a recurring monitoring run]\nThis is NOT the first run. A parent cron job spawned you. Do NOT call schedule() again.\nYour job: check for new activity, respond if needed, call done() when finished.\n\nContext from parent session:\n${e}`+(s?`\nIMPORTANT: Navigate directly to ${s} — do NOT start a new login flow if you are already on LinkedIn.`:"")}else t=`[Memory from previous session]\n${e}`;M.unshift({role:"user",content:t})}else P&&M.unshift({role:"user",content:"[SCHEDULED CHECK-IN] You are a recurring monitoring run. Do NOT call schedule() again. Check for new activity, respond if needed, then call done()."});let W=!1,J="";try{for(;!W&&E.iteration<I;){E.iteration++,d(E),N?.(E),p(E.id,{type:"iteration_start",iteration:E.iteration,ts:(new Date).toISOString()}),M.length>60&&(j({type:"message",text:"Compacting context..."}),M=await w(M,R,E.id)),j({type:"thinking"});const e=$?f+`\n\n## Structured Output Schema\nThe user expects output that conforms to this JSON schema:\n\`\`\`json\n${JSON.stringify($,null,2)}\n\`\`\`\nUse the **write_output** tool to write conforming data whenever you have results to record (after each scheduled run, after collecting data, or before calling done). For array schemas, each write_output call appends new entries. For object schemas, each call updates the object. Always call write_output before done() when a schema is provided.`:f,t=await s({model:R,system:e,messages:M,tools:c}),n=(t.toolCalls||[]).map(e=>({toolCallId:e.toolCallId,toolName:e.toolName,args:e.args}));p(E.id,{type:"llm_response",text:t.text||"",toolCalls:n,ts:(new Date).toISOString()}),t.text&&j({type:"message",text:t.text});const o=[];t.text&&o.push({type:"text",text:t.text});for(const e of t.toolCalls||[])o.push({type:"tool-call",toolCallId:e.toolCallId,toolName:e.toolName,args:e.args});if(o.length>0&&M.push({role:"assistant",content:o}),!t.toolCalls||0===t.toolCalls.length){if("stop"===t.finishReason){J=t.text||"Task complete.",W=!0;break}continue}const a=[];for(const e of t.toolCalls){const t=e.toolName,s=e.args;if("done"===t){if(J=s.summary||"Task complete.",W=!0,s.save_flow||C){const e=await b(E,r,A);E.savedFlowPath=e,j({type:"flow_saved",path:e})}a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!0})}),p(E.id,{type:"tool_call",toolName:t,args:s,result:{ok:!0},ts:(new Date).toISOString()});break}if(B.record(t,s),B.isLooping()){j({type:"message",text:"Loop detected — changing approach..."}),a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!1,error:"Loop detected: you have been repeating the same actions. Change your approach or call done()."})});continue}let n;j({type:"tool_start",toolName:t,args:s});try{n=await q.execute(t,s),p(E.id,{type:"tool_call",toolName:t,args:s,result:n,ts:(new Date).toISOString()});const r="string"==typeof n?n:JSON.stringify(n);j({type:"tool_done",toolName:t,result:r.slice(0,200)}),a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:r})}catch(n){const r=n?.message||String(n);p(E.id,{type:"tool_error",toolName:t,args:s,error:r,ts:(new Date).toISOString()}),j({type:"tool_error",toolName:t,error:r}),a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!1,error:r})})}}a.length>0&&M.push({role:"tool",content:a})}E.iteration>=I&&!W?(J=`Task hit the maximum iteration limit (${I}) without completing.`,m(E,"failed")):W&&(E.scheduledJobs.length>0?await async function(e,t,s,r,o){m(e,"scheduled");for(const s of e.scheduledJobs)o({type:"message",text:`Registering cron: ${s.cron} — ${s.taskDescription}`}),n.schedule(s.cron,async()=>{const n=(new Date).toISOString();s.lastRun=n,d(e),o({type:"message",text:`[cron ${s.cron}] Running: ${s.taskDescription}`});const r={...e.memory},a=r.thread_url||r.conversation_url,i=a?`${s.taskDescription}\n\n[Use thread_url from memory: ${a}]`:s.taskDescription;try{await runTask({...t,goal:i,sessionId:void 0,isScheduledRun:!0,inheritedMemory:r})}catch(e){o({type:"error",error:`Cron job failed: ${e?.message}`})}});o({type:"message",text:`${e.scheduledJobs.length} cron job(s) active. Process will stay alive. Press Ctrl+C to stop.`}),await new Promise(()=>{})}(E,e,0,0,j):m(E,"completed")),E.finalSummary=J,d(E),p(E.id,{type:"session_end",summary:J,status:E.status,ts:(new Date).toISOString()}),j({type:"done",summary:J})}catch(e){const t=e?.message||String(e);throw m(E,"failed"),E.finalSummary=`Error: ${t}`,d(E),j({type:"error",error:t}),e}finally{try{F.close()}catch{}try{await q.closeWreqSession()}catch{}}return E}async function b(s,n,r){const o=[`# Generated from task: ${n}`,`# Session: ${s.id}`,`# Generated: ${(new Date).toISOString()}`,""],{loadEvents:a}=await import("./session.js"),i=a(s.id);for(const e of i)if("tool_call"===e.type){const t=v(e.toolName,e.args);t&&o.push(t)}const l=n.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,40).replace(/-$/,""),c=r?t.resolve(process.cwd(),r):process.cwd();e.existsSync(c)||e.mkdirSync(c,{recursive:!0});const u=t.join(c,`${l}.flow`);return e.writeFileSync(u,o.join("\n")+"\n"),u}function v(e,t){switch(e){case"navigate":return`Go to ${t.url}`;case"click":return`Click ${t.description||t.ref}`;case"type":return`Type "${t.text}" into ${t.ref}`;case"press":return`Press ${t.key}`;case"wait":return`Wait ${t.seconds} seconds`;case"scroll":return`Scroll ${t.direction}`;case"reload":return"Reload page";case"go_back":return"Go back";case"inject_credentials":return`@inject ${t.profile_name}`;case"schedule":return`# Scheduled: ${t.cron} — ${t.task_description}`;case"fetch_url":return`# Fetched: ${t.url}`;case"done":return`# Done: ${t.summary}`;default:return null}}
1
+ import e from"fs";import t from"path";import{generateText as s}from"ai";import o from"node-cron";import{createSession as r}from"wreq-js";import{BrowserAgent as i}from"../browser/agent.js";import{loadConfig as n,loadCredentials as a,loadConfigDir as l}from"../config/loader.js";import{getModel as c}from"../ai/interpreter.js";import{taskTools as u}from"./tools.js";import{buildSystemPrompt as h}from"./prompt.js";import{createSession as d,loadSession as p,saveSessionMeta as m,appendEvent as f,updateSessionStatus as g}from"./session.js";import{TelegramService as y}from"../chat/telegram.js";const w=400;class k{wreqSession=null;browser;session;credentials;emit;onHumanInput;credentialsFilePath;isScheduledRun;schema;outputFile;customTools;constructor(e,t,s,o,r,i,n=!1,a,l,c){this.browser=e,this.session=t,this.credentials=s,this.emit=o,this.onHumanInput=r,this.credentialsFilePath=i,this.isScheduledRun=n,this.schema=a,this.outputFile=l,this.customTools=c||{}}async getWreqSession(){return this.wreqSession||(this.wreqSession=await r({browser:"chrome_131",os:"macos"})),this.wreqSession}async closeWreqSession(){if(this.wreqSession){try{await this.wreqSession.close()}catch{}this.wreqSession=null}}async execute(s,r){switch(s){case"navigate":{const e=r.url;await this.browser.navigate(e);try{await this.browser.evaluate("(function(){document.querySelectorAll('a[target=\"_blank\"]').forEach(function(a){a.setAttribute('target','_self');});if(!window.__slapifyPatched){window.__slapifyPatched=true;var _orig=window.open;window.open=function(url,name,features){if(url&&String(url).startsWith('http')){window.location.href=url;return window;}return _orig.apply(this,arguments);};}})()")}catch{}return{ok:!0,url:e}}case"get_page_state":{const e=await this.browser.getState();return{url:e.url,title:e.title,snapshot:e.snapshot,refsCount:Object.keys(e.refs).length}}case"click":{const e=r.ref;try{await this.browser.evaluate("(function(){document.querySelectorAll('a[target=\"_blank\"]').forEach(function(a){a.setAttribute('target','_self');});if(!window.__slapifyPatched){window.__slapifyPatched=true;var _orig=window.open;window.open=function(url,name,features){if(url&&String(url).startsWith('http')){window.location.href=url;return window;}return _orig.apply(this,arguments);};}})()")}catch{}return await this.browser.click(e),{ok:!0,clicked:e}}case"type":{const e=r.ref,t=r.text;return r.append?await this.browser.type(e,t):await this.browser.fill(e,t),{ok:!0}}case"press":{const e=r.key;return await this.browser.press(e),{ok:!0}}case"scroll":{const e=r.direction,t=r.amount||300;return await this.browser.scroll(e,t),{ok:!0}}case"wait":{const e=r.seconds;return await this.browser.wait(1e3*e),{ok:!0,waited:`${e}s`}}case"screenshot":return{ok:!0,path:await this.browser.screenshot(),note:"Screenshot captured. Check get_page_state() for interactive elements."};case"reload":return await this.browser.reload(),{ok:!0};case"go_back":return await this.browser.goBack(),{ok:!0};case"list_credential_profiles":return{profiles:Object.entries(this.credentials).map(([e,t])=>({name:e,type:t.type,hasUsername:!(!t.username&&!t.email),hasCookies:!!(t.cookies&&t.cookies.length>0),hasLocalStorage:!!(t.localStorage&&Object.keys(t.localStorage).length>0)}))};case"inject_credentials":{const e=r.profile_name,t=this.credentials[e];return t?"inject"!==t.type?{ok:!1,error:`Profile '${e}' is type '${t.type}', use fill_login_form for login-form profiles`}:(await this.injectProfile(t),await this.browser.wait(300),await this.browser.reload(),{ok:!0,injected:e}):{ok:!1,error:`Profile '${e}' not found`}}case"fill_login_form":{const e=r.profile_name,t=this.credentials[e];return t?"login-form"!==t.type?{ok:!1,error:`Profile '${e}' is type '${t.type}'. Use inject_credentials for inject profiles.`}:{ok:!0,username:t.username||t.email||t.phone||"",password:t.password||"",hint:"Use get_page_state() to find the username/password fields, then type into them and submit the form."}:{ok:!1,error:`Profile '${e}' not found`}}case"solve_captcha":{const e=await this.browser.getState(),t=(e.snapshot,[]),s=[],o=Object.entries(e.refs).filter(([,e])=>{const t=(e.name||e.text||"").toLowerCase();return t.includes("not a robot")||t.includes("i'm not a robot")||t.includes("checkbox")||"checkbox"===e.role});for(const[e]of o)try{await this.browser.click(e),await this.browser.wait(2e3),t.push(`Clicked checkbox ref ${e}`)}catch{s.push(`Failed to click ref ${e}`)}const r=Object.entries(e.refs).filter(([,e])=>{const t=(e.name||e.text||"").toLowerCase();return t.includes("audio")||t.includes("sound")});for(const[e]of r.slice(0,1))try{await this.browser.click(e),await this.browser.wait(1500),t.push(`Clicked audio challenge ref ${e}`)}catch{s.push(`Failed to click audio ref ${e}`)}const i=await this.browser.getState(),n=i.snapshot?.toLowerCase().includes("captcha")||i.snapshot?.toLowerCase().includes("not a robot")||i.url?.includes("sorry");return{attempted:t.length>0,solved:t,failed:s,captchaStillPresent:n,currentUrl:i.url,hint:n?"CAPTCHA still present. Try fetch_url() on a different source for the same data.":"CAPTCHA appears resolved. Call get_page_state() to continue."}}case"fetch_url":{const e=r.url,t=r.headers||{},s=await this.getWreqSession(),o=await s.fetch(e,{headers:{Accept:"application/json, text/html, */*","Accept-Language":"en-US,en;q=0.9",...t}}),i=o.headers.get("content-type")||"",n=await o.text();let a=n;if(i.includes("application/json"))try{a=JSON.parse(n)}catch{a=n}const l="string"==typeof a?a:JSON.stringify(a);return{ok:o.ok,status:o.status,body:l.slice(0,8e3)+(l.length>8e3?"…[truncated]":"")}}case"remember":{const e=r.key,t=r.value;return this.session.memory[e]=t,m(this.session),f(this.session.id,{type:"memory_update",key:e,value:t,ts:(new Date).toISOString()}),{ok:!0,stored:e}}case"recall":{const e=r.key,t=this.session.memory[e];return void 0!==t?{ok:!0,key:e,value:t}:{ok:!1,key:e,error:"Key not found in memory"}}case"list_memories":return{keys:Object.keys(this.session.memory),count:Object.keys(this.session.memory).length};case"status_update":{const e=r.message;return this.emit({type:"status_update",message:e}),{ok:!0}}case"ask_user":{const s=r.question,o=r.hint;this.emit({type:"human_input_needed",question:s,hint:o});const i=await this.onHumanInput(s,o);f(this.session.id,{type:"tool_call",toolName:"ask_user",args:{question:s,hint:o},result:{answer:"[redacted from logs]"},ts:(new Date).toISOString()});if(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,}\b/i.test(i)&&i.includes(" ")){const s=i.split(/\s+(?:and\s+)?(?:password\s+is\s+|pass(?:word)?[:\s]+)?/i),o=s[0]?.trim(),r=s[1]?.trim();this.emit({type:"human_input_needed",question:"💾 Save these credentials for future sessions?",hint:"Profile name to save as (or press Enter to skip)"});const n=await this.onHumanInput("💾 Save these credentials for future sessions?","Enter a profile name (e.g. 'linkedin', 'gmail') or leave blank to skip");if(n&&n.trim()){const s=n.trim().toLowerCase().replace(/\s+/g,"-"),i={type:"login-form",...o&&{username:o},...r&&{password:r}};try{const o=(await import("yaml")).default;let r={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=o.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(r=t)}catch{}r.profiles[s]=i,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,o.stringify(r,{indent:2,lineWidth:0})),this.credentials[s]=i,this.emit({type:"credentials_saved",profileName:s,credType:"login-form"})}catch{}}}return{answer:i}}case"save_credentials":{const s=r.profile_name,o=r.type,i=r.capture_from_browser,n={type:o};if("login-form"===o&&(r.username&&(n.username=r.username),r.password&&(n.password=r.password)),"inject"===o&&i)try{const e=await this.browser.getCookies(),t=await this.browser.getLocalStorage(),s=await this.browser.getSessionStorage();e.length>0&&(n.cookies=e.map(e=>({name:e.name,value:e.value})));const o=e=>{if(!e||"object"!=typeof e||Array.isArray(e))return{};const t={};for(const[s,o]of Object.entries(e))t[String(s)]="string"==typeof o?o:JSON.stringify(o);return t},r=o(t),i=o(s);Object.keys(r).length>0&&(n.localStorage=r),Object.keys(i).length>0&&(n.sessionStorage=i)}catch(e){return{ok:!1,error:`Failed to capture browser state: ${e.message}`}}try{const r=(await import("yaml")).default;let i={profiles:{}};if(e.existsSync(this.credentialsFilePath))try{const t=r.parse(e.readFileSync(this.credentialsFilePath,"utf-8"));t?.profiles&&(i=t)}catch{}return i.profiles[s]=n,e.mkdirSync(t.dirname(this.credentialsFilePath),{recursive:!0}),e.writeFileSync(this.credentialsFilePath,r.stringify(i,{indent:2,lineWidth:0})),this.credentials[s]=n,this.emit({type:"credentials_saved",profileName:s,credType:o}),{ok:!0,message:`Saved profile '${s}' (${o}) to credentials.yaml`,cookieCount:n.cookies?.length??0,localStorageKeys:Object.keys(n.localStorage??{}).length}}catch(e){return{ok:!1,error:`Failed to save credentials: ${e.message}`}}}case"perf_audit":{const e=r.url,t=!1!==r.lighthouse,s=!1!==r.react_scan;this.emit({type:"status_update",message:`⚡ Auditing ${e}...`});try{const{runPerfAudit:o}=await import("../perf/audit.js"),r=await o(e,this.browser,{lighthouse:t,reactScan:s,settleMs:2e3,navigate:!0});this.session.perfAudits||(this.session.perfAudits=[]),this.session.perfAudits.push(r),this.session.perfAudit=r,m(this.session);const i=r.scores??r.lighthouse,n=r.network,a={url:r.url,vitals:r.vitals,scores:i,react:r.react,network:n?{totalRequests:n.totalRequests,totalKB:Math.round((n.totalBytes||0)/1024),jsKB:Math.round((n.jsBytes||0)/1024),apiCalls:n.apiCalls.length,slowApiCalls:n.slowApiCalls.length,failedApiCalls:n.failedApiCalls.length,longTasks:n.longTasks.length,totalBlockingMs:n.totalBlockingMs,memoryMB:n.memoryMB,slowApis:n.slowApiCalls.slice(0,5).map(e=>({url:e.url.length>80?"…"+e.url.slice(-80):e.url,method:e.method,status:e.status,durationMs:e.duration})),heaviestResources:n.heaviestResources.slice(0,5).map(e=>({url:e.url.split("/").slice(-2).join("/"),type:e.type,sizeKB:Math.round(e.size/1024)}))}:null},l=[`Audit complete for ${e}`];if(r.vitals.fcp&&l.push(`FCP: ${r.vitals.fcp}ms`),r.vitals.lcp&&l.push(`LCP: ${r.vitals.lcp}ms`),null!=r.vitals.cls&&l.push(`CLS: ${r.vitals.cls}`),i&&l.push(`Scores — Perf ${i.performance}/100 · A11y ${i.accessibility}/100 · SEO ${i.seo}/100`),r.react?.detected){const e=r.react.version?.startsWith("(")?r.react.version.slice(1,-1):r.react.version,t=r.react.interactionTests??[],s=t.filter(e=>e.flagged).length;l.push(`Framework: ${e||"React"} · Re-render issues: ${r.react.issues.length}${t.length?` · Interaction tests: ${t.length} (${s} flagged)`:""}`)}return n&&l.push(`Network: ${n.totalRequests} requests · ${Math.round((n.totalBytes||0)/1024)}KB total · JS ${Math.round((n.jsBytes||0)/1024)}KB · ${n.apiCalls.length} API calls${n.slowApiCalls.length?` (${n.slowApiCalls.length} slow)`:""}${n.failedApiCalls.length?` (${n.failedApiCalls.length} failed)`:""} · ${n.longTasks.length} long tasks (${n.totalBlockingMs}ms)`),this.emit({type:"status_update",message:l.join(" · ")}),a}catch(e){return{ok:!1,error:`Performance audit failed: ${e.message}`}}}case"schedule":{const e=r.cron,t=r.task_description;return this.isScheduledRun?{ok:!1,error:"You are already running as a scheduled sub-task. Do NOT call schedule() again — the parent cron is still active. Use status_update() to report findings, then finish. The next check will happen automatically."}:o.validate(e)?(this.session.scheduledJobs.push({id:`job-${Date.now()}`,cron:e,taskDescription:t,createdAt:(new Date).toISOString()}),m(this.session),f(this.session.id,{type:"scheduled",cron:e,task:t,ts:(new Date).toISOString()}),this.emit({type:"scheduled",cron:e,task:t}),{ok:!0,message:`Task scheduled: '${t}' with cron '${e}'. The process will stay alive and re-run at each interval.`}):{ok:!1,error:`Invalid cron expression: ${e}`}}case"sleep_until":{const e=r.until,t=r.reason||"",s=function(e){const t=new Date(e);if(!isNaN(t.getTime())){const e=t.getTime()-Date.now();return Math.max(0,e)}const s=e.toLowerCase().trim(),o=[[/^(\d+(?:\.\d+)?)\s*s(?:ec(?:onds?)?)?$/,1e3],[/^(\d+(?:\.\d+)?)\s*m(?:in(?:utes?)?)?$/,6e4],[/^(\d+(?:\.\d+)?)\s*h(?:ours?)?$/,36e5],[/^(\d+(?:\.\d+)?)\s*d(?:ays?)?$/,864e5]];for(const[e,t]of o){const o=s.match(e);if(o)return Math.round(parseFloat(o[1])*t)}if(s.includes("tomorrow")){const e=new Date,t=new Date(e);t.setDate(t.getDate()+1);const o=s.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/);if(o){let e=parseInt(o[1]);const s=parseInt(o[2]||"0");"pm"===o[3]&&e<12&&(e+=12),"am"===o[3]&&12===e&&(e=0),t.setHours(e,s,0,0)}return Math.max(0,t.getTime()-Date.now())}return 6e4}(e),o=new Date(Date.now()+s).toISOString();return f(this.session.id,{type:"sleeping_until",until:o,ts:(new Date).toISOString()}),this.emit({type:"sleeping",until:o}),g(this.session,"sleeping"),await new Promise(e=>setTimeout(e,s)),g(this.session,"running"),{ok:!0,sleptUntil:o,reason:t}}case"write_output":{if(!this.outputFile&&!this.schema)return{ok:!1,error:"No schema or output file configured. Pass --schema and --output when starting the task."};const s=r.data,o=r.mode||"append";return this.session.structuredOutput=function(s,o,r,i){let n;if("overwrite"===o||null==r)n=s;else if(Array.isArray(r))n=Array.isArray(s)?[...r,...s]:[...r,s];else if("object"==typeof r){const e={...r};for(const[t,o]of Object.entries(s))Array.isArray(e[t])&&Array.isArray(o)?e[t]=[...e[t],...o]:e[t]=o;n=e}else n=s;if(i)try{const s=t.dirname(t.resolve(i));e.existsSync(s)||e.mkdirSync(s,{recursive:!0}),e.writeFileSync(i,JSON.stringify(n,null,2)+"\n","utf8")}catch{}return n}(s,o,this.session.structuredOutput,this.outputFile),m(this.session),this.outputFile&&this.emit({type:"output_written",path:this.outputFile,data:s}),{ok:!0,written:s}}case"done":return{ok:!0};default:if(this.customTools&&this.customTools[s]&&"function"==typeof this.customTools[s].execute)try{return await this.customTools[s].execute(r,{toolCallId:"custom"})}catch(e){return{ok:!1,error:`Custom tool '${s}' failed: ${e.message}`}}return{ok:!1,error:`Unknown tool: ${s}`}}}async injectProfile(e){if(e.cookies)for(const t of e.cookies)try{await this.browser.setCookie(t.name,t.value)}catch{}if(e.localStorage)for(const[t,s]of Object.entries(e.localStorage))try{await this.browser.setLocalStorage(t,s)}catch{}if(e.sessionStorage)for(const[t,s]of Object.entries(e.sessionStorage))try{await this.browser.setSessionStorage(t,s)}catch{}}}function S(e,t){if("get_page_state"===e){const e=t;return{url:e.url,title:e.title,snapshot:"[snapshot omitted from history — call get_page_state() again if needed]",refsCount:e.refsCount}}const s="string"==typeof t?t:JSON.stringify(t);return s.length>3e3?s.slice(0,3e3)+`…[truncated in history, was ${s.length} chars]`:t}async function $(e,t,o){const r=e.slice(0,e.length-20),i=e.slice(e.length-20);if(0===r.length)return e;try{const{text:n}=await s({model:t,messages:[{role:"user",content:"Summarize the following agent conversation history into a compact but detailed summary. Include: what was accomplished, current state, important findings stored in memory, any failures and what was tried. This summary will replace the history to save context.\n\n"+JSON.stringify(r,null,2)}]});return f(o,{type:"context_compacted",fromMessages:e.length,toMessages:1+i.length,ts:(new Date).toISOString()}),[{role:"user",content:`[Context Summary — earlier conversation compacted to save tokens]\n${n}`},{role:"assistant",content:"Understood. I will continue from this summary."},...i]}catch{return i}}const _=new Set(["get_page_state","screenshot","wait","scroll","recall","list_memories","list_credential_profiles","go_back","reload","solve_captcha","status_update","ask_user","save_credentials"]);class b{recentActions=[];WINDOW=20;THRESHOLD=5;record(e,t){if(_.has(e))return;const s=`${e}:${JSON.stringify(t)}`;this.recentActions.push(s),this.recentActions.length>this.WINDOW&&this.recentActions.shift()}isLooping(){if(this.recentActions.length<this.WINDOW)return!1;const e=new Map;for(const t of this.recentActions)e.set(t,(e.get(t)||0)+1);return[...e.values()].some(e=>e>=this.THRESHOLD)}}export async function runTask(e){const{goal:r,sessionId:_,headed:C,executablePath:j,saveFlow:O,flowOutputDir:A,schema:I,outputFile:x,maxIterations:N=w,onEvent:T,onSessionUpdate:D,isScheduledRun:P=!1,inheritedMemory:F}=e,M=e=>T?.(e),q=n(),R=l()||process.cwd(),L=Array.isArray(q.llm)?q.llm:[q.llm];let E=0;const J=()=>c(L[E]);let U,W=J(),H={};try{H=a().profiles||{}}catch{}q.telegram?.bot_token&&(U=new y(q.telegram),U.start(),U.sendMessage(`🚀 *Slapify Task Started*\n\nGoal: _${r}_`).catch(()=>{}));const B=new i({headless:!0!==C&&(!1===C||(q.browser?.headless??!0)),timeout:q.browser?.timeout,viewport:q.browser?.viewport,executablePath:j||q.browser?.executablePath});let K,z;if(_){const e=p(_);if(!e)throw new Error(`Session '${_}' not found.`);K=e,K.status="running",m(K);const{rebuildMessages:t}=await import("./session.js");z=t((await import("./session.js")).loadEvents(_)),M({type:"message",text:`Resuming session ${_} (iteration ${K.iteration})`})}else K=d(r),F&&Object.keys(F).length>0&&(Object.assign(K.memory,F),m(K)),z=[{role:"user",content:r}],f(K.id,{type:"session_start",goal:r,ts:(new Date).toISOString()}),M({type:"message",text:`Session ${K.id} started`});D?.(K);let G=t.join(process.cwd(),".slapify","credentials.yaml");try{const{getConfigDir:e}=await import("../config/loader.js"),s=e();s&&(G=t.join(s,"credentials.yaml"))}catch{}const Y=e.onHumanInput??(async(e,t)=>{const s=(await import("readline")).createInterface({input:process.stdin,output:process.stdout});return new Promise(o=>{const r=t?`\n ${e}\n (${t})\n > `:`\n ${e}\n > `;s.question(r,e=>{s.close(),o(e.trim())})})}),Z={};if(q.plugins?.tools&&R){const e=await import("url");for(const s of q.plugins.tools)try{const o=t.resolve(R,s),r=e.pathToFileURL(o).href,i=await import(r);for(const[e,t]of Object.entries(i))t&&"object"==typeof t&&"parameters"in t&&"execute"in t&&(Z[e]=t)}catch(e){console.error(`\n⚠️ [Slapify] Warning: Failed to load user tool file "${s}": ${e.message}\n`)}}const V={...u,...Z},Q=new k(B,K,H,M,Y,G,P,I,x,Z),X=new b;if(Object.keys(K.memory).length>0){const e=Object.entries(K.memory).map(([e,t])=>`- ${e}: ${t}`).join("\n");let t;if(P){const s=K.memory.thread_url||K.memory.conversation_url;t=`[SCHEDULED CHECK-IN — you are a recurring monitoring run]\nThis is NOT the first run. A parent cron job spawned you. Do NOT call schedule() again.\nYour job: check for new activity, respond if needed, call done() when finished.\n\nContext from parent session:\n${e}`+(s?`\nIMPORTANT: Navigate directly to ${s} — do NOT start a new login flow if you are already on LinkedIn.`:"")}else t=`[Memory from previous session]\n${e}`;z.unshift({role:"user",content:t})}else P&&z.unshift({role:"user",content:"[SCHEDULED CHECK-IN] You are a recurring monitoring run. Do NOT call schedule() again. Check for new activity, respond if needed, then call done()."});let ee=!1,te="";try{for(;!ee&&K.iteration<N;){K.iteration++,m(K),D?.(K),f(K.id,{type:"iteration_start",iteration:K.iteration,ts:(new Date).toISOString()});const e=JSON.stringify(z).length;if(e>2e5&&(M({type:"message",text:`Compacting context (${Math.round(e/1e3)}k chars)...`}),z=await $(z,W,K.id)),U){const e=U.popMessages();if(e.length>0){const t=e.join("\n\n");M({type:"message",text:`Injecting live instruction from Telegram: "${t.slice(0,50)}..."`}),z.push({role:"user",content:`[LIVE INSTRUCTION FROM USER VIA TELEGRAM]\nThe user just sent the following message while you were running. You MUST read this and adjust your current plan accordingly:\n\n${t}`})}}M({type:"thinking"});const t=h(r,I,q.plugins,R);let o;for(;;)try{o=await s({model:W,system:t,messages:z,tools:V});break}catch(e){const t=e?.message||String(e);if(t.includes("Too Many Requests")||t.includes("429")||t.includes("402")||t.includes("Payment Required")||t.includes("401")||t.includes("403")||t.includes("quota")||t.includes("billing")){const e=L[E].label||`${L[E].provider}/${L[E].model}`;if(E+1<L.length){E++,W=J();const s=L[E].label||`${L[E].provider}/${L[E].model}`;M({type:"message",text:`⚠️ ${e} failed (${t.slice(0,60)}). Switching to ${s}...`});continue}E=0,W=J(),M({type:"message",text:"⚠️ All models failed. Sleeping 60s before retrying from the start of the chain..."}),await new Promise(e=>setTimeout(e,6e4));continue}throw e}const i=(o.toolCalls||[]).map(e=>({toolCallId:e.toolCallId,toolName:e.toolName,args:e.args}));f(K.id,{type:"llm_response",text:o.text||"",toolCalls:i,ts:(new Date).toISOString()}),o.text&&M({type:"message",text:o.text});const n=[];o.text&&n.push({type:"text",text:o.text});for(const e of o.toolCalls||[])n.push({type:"tool-call",toolCallId:e.toolCallId,toolName:e.toolName,args:e.args});if(n.length>0&&z.push({role:"assistant",content:n}),!o.toolCalls||0===o.toolCalls.length){if("stop"===o.finishReason){te=o.text||"Task complete.",ee=!0;break}continue}const a=[];for(const e of o.toolCalls){const t=e.toolName,s=e.args;if("done"===t){if(te=s.summary||"Task complete.",ee=!0,s.save_flow||O){const e=await v(K,r,A);K.savedFlowPath=e,M({type:"flow_saved",path:e})}a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!0})}),f(K.id,{type:"tool_call",toolName:t,args:s,result:{ok:!0},ts:(new Date).toISOString()});break}if(X.record(t,s),X.isLooping()){M({type:"message",text:"Loop detected — changing approach..."}),a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!1,error:"Loop detected: you have been repeating the same actions. Change your approach or call done()."})});continue}let o;M({type:"tool_start",toolName:t,args:s});try{o=await Q.execute(t,s),f(K.id,{type:"tool_call",toolName:t,args:s,result:o,ts:(new Date).toISOString()});if(M({type:"tool_done",toolName:t,result:("string"==typeof o?o:JSON.stringify(o)).slice(0,200)}),"done"===t&&(ee=!0,te=JSON.stringify(o,null,2),U&&await U.sendMessage(`✅ *Task Completed*\n\n\`\`\`json\n${te}\n\`\`\``)),"ask_user"===t&&U){const e=o;await U.sendMessage(`❓ *Agent asks*:\n${e.question}`)}if("status_update"===t&&U){const e=o;await U.sendMessage(`ℹ️ *Status update*:\n${e.message}`)}const r=S(t,o),i="string"==typeof r?r:JSON.stringify(r);a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:i})}catch(o){const r=o?.message||String(o);f(K.id,{type:"tool_error",toolName:t,args:s,error:r,ts:(new Date).toISOString()}),M({type:"tool_error",toolName:t,error:r}),a.push({type:"tool-result",toolCallId:e.toolCallId,toolName:t,result:JSON.stringify({ok:!1,error:r})})}}a.length>0&&z.push({role:"tool",content:a})}K.iteration>=N&&!ee?(te=`Task hit the maximum iteration limit (${N}) without completing.`,g(K,"failed")):ee&&(K.scheduledJobs.length>0?await async function(e,t,s,r,i){g(e,"scheduled");for(const s of e.scheduledJobs)i({type:"message",text:`Registering cron: ${s.cron} — ${s.taskDescription}`}),o.schedule(s.cron,async()=>{const o=(new Date).toISOString();s.lastRun=o,m(e),i({type:"message",text:`[cron ${s.cron}] Running: ${s.taskDescription}`});const r={...e.memory},n=r.thread_url||r.conversation_url,a=n?`${s.taskDescription}\n\n[Use thread_url from memory: ${n}]`:s.taskDescription;try{await runTask({...t,goal:a,sessionId:void 0,isScheduledRun:!0,inheritedMemory:r})}catch(e){i({type:"error",error:`Cron job failed: ${e?.message}`})}});i({type:"message",text:`${e.scheduledJobs.length} cron job(s) active. Process will stay alive. Press Ctrl+C to stop.`}),await new Promise(()=>{})}(K,e,0,0,M):g(K,"completed")),K.finalSummary=te,m(K),f(K.id,{type:"session_end",summary:te,status:K.status,ts:(new Date).toISOString()}),M({type:"done",summary:te})}catch(e){const t=e?.message||String(e);throw g(K,"failed"),K.finalSummary=`Error: ${t}`,m(K),M({type:"error",error:t}),U&&!t.includes("User aborted")&&U.sendMessage(`❌ *Task Failed*\n\nError: ${t}`).catch(()=>{}),e}finally{U&&U.stop();try{B.close()}catch{}try{await Q.closeWreqSession()}catch{}}return K}async function v(s,o,r){const i=[`# Generated from task: ${o}`,`# Session: ${s.id}`,`# Generated: ${(new Date).toISOString()}`,""],{loadEvents:n}=await import("./session.js"),a=n(s.id);for(const e of a)if("tool_call"===e.type){const t=C(e.toolName,e.args);t&&i.push(t)}const l=o.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,40).replace(/-$/,""),c=r?t.resolve(process.cwd(),r):process.cwd();e.existsSync(c)||e.mkdirSync(c,{recursive:!0});const u=t.join(c,`${l}.flow`);return e.writeFileSync(u,i.join("\n")+"\n"),u}function C(e,t){switch(e){case"navigate":return`Go to ${t.url}`;case"click":return`Click ${t.description||t.ref}`;case"type":return`Type "${t.text}" into ${t.ref}`;case"press":return`Press ${t.key}`;case"wait":return`Wait ${t.seconds} seconds`;case"scroll":return`Scroll ${t.direction}`;case"reload":return"Reload page";case"go_back":return"Go back";case"inject_credentials":return`@inject ${t.profile_name}`;case"schedule":return`# Scheduled: ${t.cron} — ${t.task_description}`;case"fetch_url":return`# Fetched: ${t.url}`;case"done":return`# Done: ${t.summary}`;default:return null}}