ripplo 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import{StdioServerTransport as li}from"@modelcontextprotocol/sdk/server/stdio.js";import{McpServer as ri}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as T}from"zod";function I({locator:e,page:t}){switch(e.by){case"css":return t.locator(e.value);case"testId":return t.getByTestId(e.value);case"role":return t.getByRole(e.role,{exact:e.exact,name:e.name});case"text":return t.getByText(e.value,{exact:e.exact});case"label":return t.getByLabel(e.value,{exact:e.exact});case"placeholder":return t.getByPlaceholder(e.value);case"altText":return t.getByAltText(e.value)}}import xd from"@anthropic-ai/sdk";import{z as Be}from"zod";import{z as Mr}from"zod";async function Q({page:e,runStartTime:t,targets:r}){let o=e.viewportSize();if(o==null)throw new Error("Page has no viewport set");let[n,s]=await Promise.all([e.screenshot({type:"png"}),Lr(r)]),l=n.toString("base64");return{annotations:s,screenshotBase64:l,snapshotTimestamp:Math.round(performance.now()-t),url:e.url(),viewportHeight:o.height,viewportWidth:o.width}}function qr(e){return"locator"in e}async function Lr(e){return(await Promise.all(e.map(r=>_r(r)))).filter(r=>r!=null)}async function _r(e){if(!qr(e))return{height:0,label:e.label,type:e.type,width:0,x:0,y:0};let t=await e.locator.boundingBox({timeout:2e3}).catch(()=>null);if(t!=null)return{height:Math.round(t.height),label:e.label,type:e.type,width:Math.round(t.width),x:Math.round(t.x),y:Math.round(t.y)}}function he({node:e,page:t}){if(e.type==="assertUrl"||e.type==="waitForUrl")return[{label:e.type,type:"urlBar"}];if(e.type==="drag"){let n=I({locator:e.source,page:t}),s=I({locator:e.target,page:t});return[{label:"drag-source",locator:n,type:"action"},{label:"drag-target",locator:s,type:"action"}]}if(!Br(e))return[];let r=e.type.startsWith("assert")?"assertion":"action",o=I({locator:e.locator,page:t});return[{label:e.type,locator:o,type:r}]}function Br(e){return"locator"in e&&e.locator!=null}function c({description:e,execute:t,name:r,schema:o}){let n=Mr.toJSONSchema(o,{target:"draft-2020-12"});return{anthropicTool:{description:e,input_schema:{...n,type:"object"},name:r},name:r,async execute(s,l){let d=o.parse(l);return{...await Promise.resolve(t(s,d)),kind:"action"}}}}async function p(e){let t=e.specNode==null?[]:he({node:e.specNode,page:e.page}),r=await Q({page:e.page,runStartTime:e.runStartTime,targets:t}),o=[...e.assertions],n={annotations:r.annotations,assertions:o,detail:e.detail,duration:Math.round(e.duration),nodeId:`agent-step-${String(e.stepIndex)}`,nodeType:e.nodeType,screenshotBase64:r.screenshotBase64,snapshotTimestamp:r.snapshotTimestamp,status:e.status,stepIndex:e.stepIndex,title:e.title,url:r.url,viewportHeight:r.viewportHeight,viewportWidth:r.viewportWidth},s=o.length>0?o.map(l=>`${l.status}: ${l.description} \u2014 ${l.detail??""}`).join(`
3
- `):`${e.status}: ${e.title}`;return{specNode:e.specNode,stepResult:n,toolOutput:s}}var Gr=Be.object({selector:Be.string().describe("CSS selector for the checkbox or radio button")}),Me=c({description:"Assert that a checkbox or radio button is checked",name:"assert_checked",schema:Gr,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).isChecked().catch(()=>!1),n=o?"passed":"failed",s=[{description:`Element ${t.selector} is checked`,detail:o?void 0:"Element is not checked",status:n}],d={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertChecked"};return p({assertions:s,detail:o?"Element is checked":"Element is not checked",duration:performance.now()-r,nodeType:"assertChecked",page:e.page,runStartTime:e.runStartTime,specNode:d,status:n,stepIndex:e.stepIndex,title:`Assert checked: ${t.selector}`})}});import{z as M}from"zod";var zr=M.object({httpOnly:M.boolean().optional().describe("Assert httpOnly flag value"),name:M.string().describe("Cookie name to check"),sameSite:M.enum(["Strict","Lax","None"]).optional().describe("Assert sameSite value"),secure:M.boolean().optional().describe("Assert secure flag value"),value:M.string().optional().describe("Expected cookie value (substring match)")}),ze=c({description:"Assert properties of a browser cookie. Check existence, value, and security flags (httpOnly, secure, sameSite).",name:"assert_cookie",schema:zr,execute:async(e,t)=>{let r=performance.now(),n=(await e.page.context().cookies()).find(h=>h.name===t.name),s=n==null?[{description:`Cookie "${t.name}" exists`,detail:"Cookie not found",status:"failed"}]:[{description:`Cookie "${t.name}" exists`,detail:void 0,status:"passed"},...Hr({cookie:n,input:t})],l=s.some(h=>h.status==="failed"),d=l?"failed":"passed",u={id:`agent-step-${String(e.stepIndex)}`,name:{type:"static",value:t.name},type:"assertCookie"};return p({assertions:s,detail:l?s.filter(h=>h.status==="failed").map(h=>h.detail).join("; "):"Cookie OK",duration:performance.now()-r,nodeType:"assertCookie",page:e.page,runStartTime:e.runStartTime,specNode:u,status:d,stepIndex:e.stepIndex,title:`Assert cookie: ${t.name}`})}});function Hr({cookie:e,input:t}){return[Jr(t.value,e.value),Ge("httpOnly",t.httpOnly,e.httpOnly),Ge("secure",t.secure,e.secure),Kr("sameSite",t.sameSite,e.sameSite)].filter(r=>r!=null)}function Jr(e,t){if(e==null)return;let r=t.includes(e);return{description:`Cookie value contains "${e}"`,detail:r?void 0:`Got: "${t}"`,status:r?"passed":"failed"}}function Ge(e,t,r){if(t!=null)return{description:`Cookie ${e} is ${String(t)}`,detail:r===t?void 0:`Got: ${String(r)}`,status:r===t?"passed":"failed"}}function Kr(e,t,r){if(t!=null)return{description:`Cookie ${e} is ${t}`,detail:r===t?void 0:`Got: ${r}`,status:r===t?"passed":"failed"}}import{z as He}from"zod";var Yr=He.object({selector:He.string().describe("CSS selector for the element to check focus on")}),Je=c({description:"Assert that an element has keyboard focus. Use for accessibility tab-order testing.",name:"assert_focused",schema:Yr,execute:async(e,t)=>{let r=performance.now(),n=!!await e.page.locator(t.selector).evaluate("(el) => document.activeElement === el").catch(()=>!1),s=n?"passed":"failed",l=[{description:`Element ${t.selector} is focused`,detail:n?void 0:"Element does not have focus",status:s}],i={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertFocused"};return p({assertions:l,detail:n?"Element is focused":"Element does not have focus",duration:performance.now()-r,nodeType:"assertFocused",page:e.page,runStartTime:e.runStartTime,specNode:i,status:s,stepIndex:e.stepIndex,title:`Assert focused: ${t.selector}`})}});import{z as G}from"zod";var Qr=G.object({bodyContains:G.string().optional().describe("Assert response body contains this string"),headerName:G.string().optional().describe("Response header name to check"),headerValue:G.string().optional().describe("Expected value the header should contain"),status:G.number().int().optional().describe("Expected HTTP status code"),urlPattern:G.string().describe("URL substring to match the response against")}),Ke=c({description:"Wait for a network response matching the URL pattern and assert on its status, body, or headers. Useful for verifying API calls and security headers (CSP, CORS).",name:"assert_response",schema:Qr,execute:async(e,t)=>{let r=performance.now(),o=await e.page.waitForResponse(u=>u.url().includes(t.urlPattern),{timeout:1e4}),n=[Xr(t.status,o.status()),await Zr(t.bodyContains,o),eo(t.headerName,t.headerValue,o.headers())].filter(u=>u!=null),s=n.length===0?[{description:`Response matching "${t.urlPattern}" received`,detail:void 0,status:"passed"}]:n,l=s.some(u=>u.status==="failed"),i={id:`agent-step-${String(e.stepIndex)}`,type:"assertResponse",urlPattern:{type:"static",value:t.urlPattern}};return p({assertions:s,detail:l?s.filter(u=>u.status==="failed").map(u=>u.detail).join("; "):"Response OK",duration:performance.now()-r,nodeType:"assertResponse",page:e.page,runStartTime:e.runStartTime,specNode:i,status:l?"failed":"passed",stepIndex:e.stepIndex,title:`Assert response: ${t.urlPattern}`})}});function Xr(e,t){if(e!=null)return{description:`Response status equals ${String(e)}`,detail:t===e?void 0:`Got: ${String(t)}`,status:t===e?"passed":"failed"}}async function Zr(e,t){if(e==null)return;let o=(await t.text()).includes(e);return{description:`Response body contains "${e}"`,detail:o?void 0:"Not found in response body",status:o?"passed":"failed"}}function eo(e,t,r){if(e==null||t==null)return;let o=r[e.toLowerCase()],n=o!=null&&o.includes(t);return{description:`Response header "${e}" contains "${t}"`,detail:n?void 0:`Got: "${o??"(missing)"}"`,status:n?"passed":"failed"}}import{z as ge}from"zod";var to=ge.object({expected:ge.string().describe("The expected text content"),selector:ge.string().describe("CSS selector for the element to check")}),Ye=c({description:"Assert that an element's text content matches the expected value",name:"assert_text",schema:to,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).textContent({timeout:5e3}).catch(()=>null),n=o!=null&&o.includes(t.expected),s=n?"passed":"failed",l=[{description:`Text contains "${t.expected}"`,detail:`Actual: "${o??"(not found)"}"`,status:s}],d=`agent-step-${String(e.stepIndex)}`,i={expected:{type:"static",value:t.expected},id:d,locator:{by:"css",value:t.selector},operator:"contains",type:"assertText"};return p({assertions:l,detail:n?`Text matches: "${t.expected}"`:`Expected "${t.expected}", got "${o??"(not found)"}"`,duration:performance.now()-r,nodeType:"assertText",page:e.page,runStartTime:e.runStartTime,specNode:i,status:s,stepIndex:e.stepIndex,title:`Assert text: ${t.selector}`})}});import{z as ye}from"zod";var ro=ye.object({expected:ye.string().describe("Expected page title or substring"),operator:ye.enum(["equals","contains","startsWith","endsWith"]).describe("Comparison operator").default("contains")}),Qe=c({description:"Assert that the page title matches an expected value",name:"assert_title",schema:ro,execute:async(e,t)=>{let r=performance.now(),o=await e.page.title(),n=oo({actual:o,expected:t.expected,operator:t.operator}),s=n?"passed":"failed",l=[{description:`Title ${t.operator} "${t.expected}"`,detail:n?void 0:`Got: "${o}"`,status:s}],d=`agent-step-${String(e.stepIndex)}`,i={expected:{type:"static",value:t.expected},id:d,operator:t.operator,type:"assertTitle"};return p({assertions:l,detail:n?"Title matches":`Expected title ${t.operator} "${t.expected}", got "${o}"`,duration:performance.now()-r,nodeType:"assertTitle",page:e.page,runStartTime:e.runStartTime,specNode:i,status:s,stepIndex:e.stepIndex,title:`Assert title ${t.operator} "${t.expected}"`})}});function oo({actual:e,expected:t,operator:r}){return r==="equals"?e===t:r==="contains"?e.includes(t):r==="startsWith"?e.startsWith(t):r==="endsWith"?e.endsWith(t):!1}import{z as Xe}from"zod";var no=Xe.object({selector:Xe.string().describe("CSS selector for the element to check")}),Ze=c({description:"Assert that an element matching the selector is visible on the page",name:"assert_visible",schema:no,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).isVisible().catch(()=>!1),n=o?"passed":"failed",s=[{description:`Element ${t.selector} is visible`,detail:o?"Element is visible":"Element is not visible",status:n}],d={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertVisible"};return p({assertions:s,detail:o?"Element is visible":"Element is not visible",duration:performance.now()-r,nodeType:"assertVisible",page:e.page,runStartTime:e.runStartTime,specNode:d,status:n,stepIndex:e.stepIndex,title:`Assert visible: ${t.selector}`})}});import{z as et}from"zod";var ao=et.object({selector:et.string().describe("CSS selector for the checkbox to check")}),tt=c({description:"Check a checkbox matching the CSS selector",name:"check",schema:ao,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).check({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"check"};return p({assertions:[],detail:`Checked ${t.selector}`,duration:performance.now()-r,nodeType:"check",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Check ${t.selector}`})}});import{z as rt}from"zod";var so=rt.object({selector:rt.string().describe("CSS selector for the input to clear")}),ot=c({description:"Clear the contents of an input field",name:"clear",schema:so,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).clear({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"clear"};return p({assertions:[],detail:`Cleared ${t.selector}`,duration:performance.now()-r,nodeType:"clear",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Clear ${t.selector}`})}});import{z as nt}from"zod";var io=nt.object({selector:nt.string().describe("CSS selector for the element to click")}),at=c({description:"Click an element matching the CSS selector",name:"click",schema:io,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"click"};return p({assertions:[],detail:`Clicked ${t.selector}`,duration:performance.now()-r,nodeType:"click",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Click ${t.selector}`})}});import{z as we}from"zod";var lo=we.object({action:we.enum(["read","write"]).describe("Whether to read from or write to the clipboard"),value:we.string().optional().describe("Text to write (required for write action)")}),st=c({description:"Read from or write to the browser clipboard. For write, provide the text value. For read, the clipboard contents are returned.",name:"clipboard",schema:lo,execute:async(e,t)=>{let r=performance.now(),o=`agent-step-${String(e.stepIndex)}`;if(t.action==="write"){if(t.value==null)throw new Error("clipboard write requires a value");let l=JSON.stringify(t.value);await e.page.evaluate(`navigator.clipboard.writeText(${l})`);let d={action:"write",id:o,type:"clipboard",value:{type:"static",value:t.value}};return p({assertions:[],detail:`Wrote to clipboard: "${t.value}"`,duration:performance.now()-r,nodeType:"clipboard",page:e.page,runStartTime:e.runStartTime,specNode:d,status:"passed",stepIndex:e.stepIndex,title:"Clipboard write"})}let n=String(await e.page.evaluate("navigator.clipboard.readText()")),s={action:"read",id:o,type:"clipboard"};return p({assertions:[],detail:`Clipboard contents: "${n}"`,duration:performance.now()-r,nodeType:"clipboard",page:e.page,runStartTime:e.runStartTime,specNode:s,status:"passed",stepIndex:e.stepIndex,title:"Clipboard read"})}});import{z as re}from"zod";var it=re.object({summary:re.string().describe("This is the ONLY output the user sees. If the agent profile specifies an Output format, follow those instructions exactly. Otherwise, provide a summary of your findings."),verdict:re.enum(["pass","fail"]).describe("Whether the test passed or failed. If the agent profile defines Success Criteria, base this verdict on whether those criteria were met.")}),lt={anthropicTool:{description:"Call this tool when you have finished your evaluation. If the agent profile defines an Output section, the summary MUST follow those output instructions exactly. Otherwise, summarize your findings.",input_schema:{...re.toJSONSchema(it,{target:"draft-2020-12"}),type:"object"},name:"complete_test"},name:"complete_test",execute(e,t){let r=it.parse(t),o={kind:"verdict",summary:r.summary,toolOutput:`Verdict: ${r.verdict}
2
+ import{StdioServerTransport as li}from"@modelcontextprotocol/sdk/server/stdio.js";import{McpServer as ri}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as T}from"zod";function I({locator:e,page:t}){switch(e.by){case"css":return t.locator(e.value);case"testId":return t.getByTestId(e.value);case"role":return t.getByRole(e.role,{exact:e.exact,name:e.name});case"text":return t.getByText(e.value,{exact:e.exact});case"label":return t.getByLabel(e.value,{exact:e.exact});case"placeholder":return t.getByPlaceholder(e.value);case"altText":return t.getByAltText(e.value)}}import xd from"@anthropic-ai/sdk";import{z as Be}from"zod";import{z as Mr}from"zod";async function Q({page:e,runStartTime:t,targets:r}){let o=e.viewportSize();if(o==null)throw new Error("Page has no viewport set");let[n,s]=await Promise.all([e.screenshot({type:"png"}),qr(r)]),l=n.toString("base64");return{annotations:s,screenshotBase64:l,snapshotTimestamp:Math.round(performance.now()-t),url:e.url(),viewportHeight:o.height,viewportWidth:o.width}}function Wr(e){return"locator"in e}async function qr(e){return(await Promise.all(e.map(r=>_r(r)))).filter(r=>r!=null)}async function _r(e){if(!Wr(e))return{height:0,label:e.label,type:e.type,width:0,x:0,y:0};let t=await e.locator.boundingBox({timeout:2e3}).catch(()=>null);if(t!=null)return{height:Math.round(t.height),label:e.label,type:e.type,width:Math.round(t.width),x:Math.round(t.x),y:Math.round(t.y)}}function he({node:e,page:t}){if(e.type==="assertUrl"||e.type==="waitForUrl")return[{label:e.type,type:"urlBar"}];if(e.type==="drag"){let n=I({locator:e.source,page:t}),s=I({locator:e.target,page:t});return[{label:"drag-source",locator:n,type:"action"},{label:"drag-target",locator:s,type:"action"}]}if(!Br(e))return[];let r=e.type.startsWith("assert")?"assertion":"action",o=I({locator:e.locator,page:t});return[{label:e.type,locator:o,type:r}]}function Br(e){return"locator"in e&&e.locator!=null}function c({description:e,execute:t,name:r,schema:o}){let n=Mr.toJSONSchema(o,{target:"draft-2020-12"});return{anthropicTool:{description:e,input_schema:{...n,type:"object"},name:r},name:r,async execute(s,l){let d=o.parse(l);return{...await Promise.resolve(t(s,d)),kind:"action"}}}}async function p(e){let t=e.specNode==null?[]:he({node:e.specNode,page:e.page}),r=await Q({page:e.page,runStartTime:e.runStartTime,targets:t}),o=[...e.assertions],n={annotations:r.annotations,assertions:o,detail:e.detail,duration:Math.round(e.duration),nodeId:`agent-step-${String(e.stepIndex)}`,nodeType:e.nodeType,screenshotBase64:r.screenshotBase64,snapshotTimestamp:r.snapshotTimestamp,status:e.status,stepIndex:e.stepIndex,title:e.title,url:r.url,viewportHeight:r.viewportHeight,viewportWidth:r.viewportWidth},s=o.length>0?o.map(l=>`${l.status}: ${l.description} \u2014 ${l.detail??""}`).join(`
3
+ `):`${e.status}: ${e.title}`;return{specNode:e.specNode,stepResult:n,toolOutput:s}}var Gr=Be.object({selector:Be.string().describe("CSS selector for the checkbox or radio button")}),Me=c({description:"Assert that a checkbox or radio button is checked",name:"assert_checked",schema:Gr,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).isChecked().catch(()=>!1),n=o?"passed":"failed",s=[{description:`Element ${t.selector} is checked`,detail:o?void 0:"Element is not checked",status:n}],d={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertChecked"};return p({assertions:s,detail:o?"Element is checked":"Element is not checked",duration:performance.now()-r,nodeType:"assertChecked",page:e.page,runStartTime:e.runStartTime,specNode:d,status:n,stepIndex:e.stepIndex,title:`Assert checked: ${t.selector}`})}});import{z as M}from"zod";var zr=M.object({httpOnly:M.boolean().optional().describe("Assert httpOnly flag value"),name:M.string().describe("Cookie name to check"),sameSite:M.enum(["Strict","Lax","None"]).optional().describe("Assert sameSite value"),secure:M.boolean().optional().describe("Assert secure flag value"),value:M.string().optional().describe("Expected cookie value (substring match)")}),ze=c({description:"Assert properties of a browser cookie. Check existence, value, and security flags (httpOnly, secure, sameSite).",name:"assert_cookie",schema:zr,execute:async(e,t)=>{let r=performance.now(),n=(await e.page.context().cookies()).find(h=>h.name===t.name),s=n==null?[{description:`Cookie "${t.name}" exists`,detail:"Cookie not found",status:"failed"}]:[{description:`Cookie "${t.name}" exists`,detail:void 0,status:"passed"},...Hr({cookie:n,input:t})],l=s.some(h=>h.status==="failed"),d=l?"failed":"passed",u={id:`agent-step-${String(e.stepIndex)}`,name:{type:"static",value:t.name},type:"assertCookie"};return p({assertions:s,detail:l?s.filter(h=>h.status==="failed").map(h=>h.detail).join("; "):"Cookie OK",duration:performance.now()-r,nodeType:"assertCookie",page:e.page,runStartTime:e.runStartTime,specNode:u,status:d,stepIndex:e.stepIndex,title:`Assert cookie: ${t.name}`})}});function Hr({cookie:e,input:t}){return[Jr(t.value,e.value),Ge("httpOnly",t.httpOnly,e.httpOnly),Ge("secure",t.secure,e.secure),Yr("sameSite",t.sameSite,e.sameSite)].filter(r=>r!=null)}function Jr(e,t){if(e==null)return;let r=t.includes(e);return{description:`Cookie value contains "${e}"`,detail:r?void 0:`Got: "${t}"`,status:r?"passed":"failed"}}function Ge(e,t,r){if(t!=null)return{description:`Cookie ${e} is ${String(t)}`,detail:r===t?void 0:`Got: ${String(r)}`,status:r===t?"passed":"failed"}}function Yr(e,t,r){if(t!=null)return{description:`Cookie ${e} is ${t}`,detail:r===t?void 0:`Got: ${r}`,status:r===t?"passed":"failed"}}import{z as He}from"zod";var Kr=He.object({selector:He.string().describe("CSS selector for the element to check focus on")}),Je=c({description:"Assert that an element has keyboard focus. Use for accessibility tab-order testing.",name:"assert_focused",schema:Kr,execute:async(e,t)=>{let r=performance.now(),n=!!await e.page.locator(t.selector).evaluate("(el) => document.activeElement === el").catch(()=>!1),s=n?"passed":"failed",l=[{description:`Element ${t.selector} is focused`,detail:n?void 0:"Element does not have focus",status:s}],i={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertFocused"};return p({assertions:l,detail:n?"Element is focused":"Element does not have focus",duration:performance.now()-r,nodeType:"assertFocused",page:e.page,runStartTime:e.runStartTime,specNode:i,status:s,stepIndex:e.stepIndex,title:`Assert focused: ${t.selector}`})}});import{z as G}from"zod";var Qr=G.object({bodyContains:G.string().optional().describe("Assert response body contains this string"),headerName:G.string().optional().describe("Response header name to check"),headerValue:G.string().optional().describe("Expected value the header should contain"),status:G.number().int().optional().describe("Expected HTTP status code"),urlPattern:G.string().describe("URL substring to match the response against")}),Ye=c({description:"Wait for a network response matching the URL pattern and assert on its status, body, or headers. Useful for verifying API calls and security headers (CSP, CORS).",name:"assert_response",schema:Qr,execute:async(e,t)=>{let r=performance.now(),o=await e.page.waitForResponse(u=>u.url().includes(t.urlPattern),{timeout:1e4}),n=[Xr(t.status,o.status()),await Zr(t.bodyContains,o),eo(t.headerName,t.headerValue,o.headers())].filter(u=>u!=null),s=n.length===0?[{description:`Response matching "${t.urlPattern}" received`,detail:void 0,status:"passed"}]:n,l=s.some(u=>u.status==="failed"),i={id:`agent-step-${String(e.stepIndex)}`,type:"assertResponse",urlPattern:{type:"static",value:t.urlPattern}};return p({assertions:s,detail:l?s.filter(u=>u.status==="failed").map(u=>u.detail).join("; "):"Response OK",duration:performance.now()-r,nodeType:"assertResponse",page:e.page,runStartTime:e.runStartTime,specNode:i,status:l?"failed":"passed",stepIndex:e.stepIndex,title:`Assert response: ${t.urlPattern}`})}});function Xr(e,t){if(e!=null)return{description:`Response status equals ${String(e)}`,detail:t===e?void 0:`Got: ${String(t)}`,status:t===e?"passed":"failed"}}async function Zr(e,t){if(e==null)return;let o=(await t.text()).includes(e);return{description:`Response body contains "${e}"`,detail:o?void 0:"Not found in response body",status:o?"passed":"failed"}}function eo(e,t,r){if(e==null||t==null)return;let o=r[e.toLowerCase()],n=o!=null&&o.includes(t);return{description:`Response header "${e}" contains "${t}"`,detail:n?void 0:`Got: "${o??"(missing)"}"`,status:n?"passed":"failed"}}import{z as ge}from"zod";var to=ge.object({expected:ge.string().describe("The expected text content"),selector:ge.string().describe("CSS selector for the element to check")}),Ke=c({description:"Assert that an element's text content matches the expected value",name:"assert_text",schema:to,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).textContent({timeout:5e3}).catch(()=>null),n=o!=null&&o.includes(t.expected),s=n?"passed":"failed",l=[{description:`Text contains "${t.expected}"`,detail:`Actual: "${o??"(not found)"}"`,status:s}],d=`agent-step-${String(e.stepIndex)}`,i={expected:{type:"static",value:t.expected},id:d,locator:{by:"css",value:t.selector},operator:"contains",type:"assertText"};return p({assertions:l,detail:n?`Text matches: "${t.expected}"`:`Expected "${t.expected}", got "${o??"(not found)"}"`,duration:performance.now()-r,nodeType:"assertText",page:e.page,runStartTime:e.runStartTime,specNode:i,status:s,stepIndex:e.stepIndex,title:`Assert text: ${t.selector}`})}});import{z as ye}from"zod";var ro=ye.object({expected:ye.string().describe("Expected page title or substring"),operator:ye.enum(["equals","contains","startsWith","endsWith"]).describe("Comparison operator").default("contains")}),Qe=c({description:"Assert that the page title matches an expected value",name:"assert_title",schema:ro,execute:async(e,t)=>{let r=performance.now(),o=await e.page.title(),n=oo({actual:o,expected:t.expected,operator:t.operator}),s=n?"passed":"failed",l=[{description:`Title ${t.operator} "${t.expected}"`,detail:n?void 0:`Got: "${o}"`,status:s}],d=`agent-step-${String(e.stepIndex)}`,i={expected:{type:"static",value:t.expected},id:d,operator:t.operator,type:"assertTitle"};return p({assertions:l,detail:n?"Title matches":`Expected title ${t.operator} "${t.expected}", got "${o}"`,duration:performance.now()-r,nodeType:"assertTitle",page:e.page,runStartTime:e.runStartTime,specNode:i,status:s,stepIndex:e.stepIndex,title:`Assert title ${t.operator} "${t.expected}"`})}});function oo({actual:e,expected:t,operator:r}){return r==="equals"?e===t:r==="contains"?e.includes(t):r==="startsWith"?e.startsWith(t):r==="endsWith"?e.endsWith(t):!1}import{z as Xe}from"zod";var no=Xe.object({selector:Xe.string().describe("CSS selector for the element to check")}),Ze=c({description:"Assert that an element matching the selector is visible on the page",name:"assert_visible",schema:no,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).isVisible().catch(()=>!1),n=o?"passed":"failed",s=[{description:`Element ${t.selector} is visible`,detail:o?"Element is visible":"Element is not visible",status:n}],d={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertVisible"};return p({assertions:s,detail:o?"Element is visible":"Element is not visible",duration:performance.now()-r,nodeType:"assertVisible",page:e.page,runStartTime:e.runStartTime,specNode:d,status:n,stepIndex:e.stepIndex,title:`Assert visible: ${t.selector}`})}});import{z as et}from"zod";var ao=et.object({selector:et.string().describe("CSS selector for the checkbox to check")}),tt=c({description:"Check a checkbox matching the CSS selector",name:"check",schema:ao,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).check({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"check"};return p({assertions:[],detail:`Checked ${t.selector}`,duration:performance.now()-r,nodeType:"check",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Check ${t.selector}`})}});import{z as rt}from"zod";var so=rt.object({selector:rt.string().describe("CSS selector for the input to clear")}),ot=c({description:"Clear the contents of an input field",name:"clear",schema:so,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).clear({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"clear"};return p({assertions:[],detail:`Cleared ${t.selector}`,duration:performance.now()-r,nodeType:"clear",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Clear ${t.selector}`})}});import{z as nt}from"zod";var io=nt.object({selector:nt.string().describe("CSS selector for the element to click")}),at=c({description:"Click an element matching the CSS selector",name:"click",schema:io,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"click"};return p({assertions:[],detail:`Clicked ${t.selector}`,duration:performance.now()-r,nodeType:"click",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Click ${t.selector}`})}});import{z as we}from"zod";var lo=we.object({action:we.enum(["read","write"]).describe("Whether to read from or write to the clipboard"),value:we.string().optional().describe("Text to write (required for write action)")}),st=c({description:"Read from or write to the browser clipboard. For write, provide the text value. For read, the clipboard contents are returned.",name:"clipboard",schema:lo,execute:async(e,t)=>{let r=performance.now(),o=`agent-step-${String(e.stepIndex)}`;if(t.action==="write"){if(t.value==null)throw new Error("clipboard write requires a value");let l=JSON.stringify(t.value);await e.page.evaluate(`navigator.clipboard.writeText(${l})`);let d={action:"write",id:o,type:"clipboard",value:{type:"static",value:t.value}};return p({assertions:[],detail:`Wrote to clipboard: "${t.value}"`,duration:performance.now()-r,nodeType:"clipboard",page:e.page,runStartTime:e.runStartTime,specNode:d,status:"passed",stepIndex:e.stepIndex,title:"Clipboard write"})}let n=String(await e.page.evaluate("navigator.clipboard.readText()")),s={action:"read",id:o,type:"clipboard"};return p({assertions:[],detail:`Clipboard contents: "${n}"`,duration:performance.now()-r,nodeType:"clipboard",page:e.page,runStartTime:e.runStartTime,specNode:s,status:"passed",stepIndex:e.stepIndex,title:"Clipboard read"})}});import{z as re}from"zod";var it=re.object({summary:re.string().describe("This is the ONLY output the user sees. If the agent profile specifies an Output format, follow those instructions exactly. Otherwise, provide a summary of your findings."),verdict:re.enum(["pass","fail"]).describe("Whether the test passed or failed. If the agent profile defines Success Criteria, base this verdict on whether those criteria were met.")}),lt={anthropicTool:{description:"Call this tool when you have finished your evaluation. If the agent profile defines an Output section, the summary MUST follow those output instructions exactly. Otherwise, summarize your findings.",input_schema:{...re.toJSONSchema(it,{target:"draft-2020-12"}),type:"object"},name:"complete_test"},name:"complete_test",execute(e,t){let r=it.parse(t),o={kind:"verdict",summary:r.summary,toolOutput:`Verdict: ${r.verdict}
4
4
  ${r.summary}`,verdict:r.verdict};return Promise.resolve(o)}};import{z as ct}from"zod";var co=ct.object({selector:ct.string().describe("CSS selector for the element to extract text from")}),dt=c({description:"Extract the text content of an element matching the CSS selector. Returns the text so you can use it in assertions or decisions.",name:"extract_text",schema:co,execute:async(e,t)=>{let r=performance.now(),n=await e.page.locator(t.selector).textContent({timeout:5e3})??"",l={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"extractText",variable:"extractedText"};return p({assertions:[],detail:`Extracted text: "${n}"`,duration:performance.now()-r,nodeType:"extractText",page:e.page,runStartTime:e.runStartTime,specNode:l,status:"passed",stepIndex:e.stepIndex,title:`Extract text from ${t.selector}`})}});import{z as be}from"zod";var uo=be.object({selector:be.string().describe("CSS selector for the input element"),value:be.string().describe("The text to type into the element")}),ut=c({description:"Clear and type text into an input element matching the CSS selector",name:"fill",schema:uo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).fill(t.value,{timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"fill",value:{type:"static",value:t.value}};return p({assertions:[],detail:`Filled ${t.selector} with "${t.value}"`,duration:performance.now()-r,nodeType:"fill",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Fill ${t.selector}`})}});import{z as pt}from"zod";var po=pt.object({level:pt.string().describe("Filter by log level: 'error', 'warning', 'log', 'info', 'debug', or 'all'").default("all")}),ft=c({description:"Get console log messages from the browser. Optionally filter by level (error, warning, log, info, debug, all).",name:"get_console_logs",schema:po,execute:(e,t)=>{let r=t.level==="all"?e.monitor.consoleEntries:e.monitor.consoleEntries.filter(n=>n.level===t.level);if(r.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No console ${t.level==="all"?"messages":t.level+" messages"} recorded.`};let o=r.map(n=>fo(n)).join(`
5
5
  `);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(r.length)} console messages:
6
6
  ${o}`}}});function fo(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as mt}from"zod";var Se=50,mo=mt.object({urlFilter:mt.string().describe("Optional regex to filter network entries by URL. Omit to see all.").default("")}),ht=c({description:"Get a summary of network requests/responses. Shows method, URL, status code, and content type. Optionally filter by URL regex.",name:"get_network_log",schema:mo,execute:(e,t)=>{let r=t.urlFilter.length>0?new RegExp(t.urlFilter,"i"):void 0,o=r==null?e.monitor.networkEntries:e.monitor.networkEntries.filter(d=>r.test(d.url));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:"No network requests recorded."};let n=o.slice(-Se),s=n.map(d=>{let i=d.statusCode==null?"pending":String(d.statusCode),u=d.responseHeaders["content-type"]??"";return`${d.method} ${i} ${d.url} [${u}]`}).join(`
@@ -27,20 +27,20 @@ ${r}`}}});import{z as Rt}from"zod";var ko=Rt.object({type:Rt.enum(["all","cookie
27
27
  (none)`;let r=t.map(o=>` ${o.name}=${o.value} (domain=${o.domain}, httpOnly=${String(o.httpOnly)}, secure=${String(o.secure)}, sameSite=${o.sameSite})`);return`## Cookies (${String(t.length)})
28
28
  ${r.join(`
29
29
  `)}`}async function vt(e,t){let r=await e.evaluate(`JSON.stringify(Object.fromEntries(Object.entries(${t})))`);return`## ${t}
30
- ${String(r)}`}import{z as ke}from"zod";var vo=ke.object({action:ke.enum(["accept","dismiss"]).describe("Whether to accept or dismiss the dialog"),promptText:ke.string().optional().describe("Text to enter for prompt dialogs")}),Pt=c({description:"Set up a handler for the next browser dialog (alert, confirm, prompt). Call this BEFORE the action that triggers the dialog.",name:"handle_dialog",schema:vo,execute:(e,t)=>{let r=performance.now();e.page.once("dialog",async s=>{t.action==="accept"?await s.accept(t.promptText??void 0):await s.dismiss()});let o=`agent-step-${String(e.stepIndex)}`,n={action:t.action,id:o,type:"handleDialog"};return p({assertions:[],detail:`Dialog handler set: ${t.action}`,duration:performance.now()-r,nodeType:"handleDialog",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Handle dialog: ${t.action}`})}});import{z as Tt}from"zod";var xo=Tt.object({selector:Tt.string().describe("CSS selector for the element to hover over")}),Nt=c({description:"Hover over an element matching the CSS selector",name:"hover",schema:xo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).hover({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"hover"};return p({assertions:[],detail:`Hovered over ${t.selector}`,duration:performance.now()-r,nodeType:"hover",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Hover ${t.selector}`})}});import{z as Re}from"zod";var Po=Re.object({permission:Re.string().describe("Permission name (e.g. geolocation, notifications, camera, clipboard-read)"),state:Re.enum(["granted","prompt"]).describe("Permission state to set")}),Ct=c({description:"Set a browser permission state (geolocation, notifications, camera, etc.)",name:"set_permission",schema:Po,execute:async(e,t)=>{let r=performance.now(),o=e.page.context();t.state==="granted"?await o.grantPermissions([t.permission]):await o.clearPermissions();let s={id:`agent-step-${String(e.stepIndex)}`,permission:t.permission,state:t.state,type:"setPermission"};return p({assertions:[],detail:`Set ${t.permission} to ${t.state}`,duration:performance.now()-r,nodeType:"setPermission",page:e.page,runStartTime:e.runStartTime,specNode:s,status:"passed",stepIndex:e.stepIndex,title:`Set permission: ${t.permission} \u2192 ${t.state}`})}});import{z as At}from"zod";var To=At.object({url:At.string().describe("The URL to navigate to")}),$t=c({description:"Navigate the browser to a URL",name:"navigate",schema:To,execute:async(e,t)=>{let r=performance.now();await e.page.goto(t.url,{waitUntil:"domcontentloaded"});let n={id:`agent-step-${String(e.stepIndex)}`,type:"goto",url:{type:"static",value:t.url}};return p({assertions:[],detail:`Navigated to ${t.url}`,duration:performance.now()-r,nodeType:"goto",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Navigate to ${t.url}`})}});import{z as It}from"zod";var No=It.object({key:It.string().describe("Key to press, e.g. 'Enter', 'Tab', 'Escape'")}),Et=c({description:"Press a keyboard key",name:"press",schema:No,execute:async(e,t)=>{let r=performance.now();await e.page.keyboard.press(t.key);let n={id:`agent-step-${String(e.stepIndex)}`,key:t.key,type:"press"};return p({assertions:[],detail:`Pressed ${t.key}`,duration:performance.now()-r,nodeType:"press",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Press ${t.key}`})}});import{z as ve}from"zod";var Co=ve.object({height:ve.number().int().positive().describe("Viewport height in pixels"),width:ve.number().int().positive().describe("Viewport width in pixels")}),jt=c({description:"Resize the browser viewport to the specified dimensions. Use this to test responsive layouts at different screen sizes (e.g. mobile: 375x812, tablet: 768x1024, desktop: 1440x900).",name:"resize_viewport",schema:Co,execute:async(e,t)=>{let r=performance.now();await e.page.setViewportSize({height:t.height,width:t.width});let o=`agent-step-${String(e.stepIndex)}`,n={height:t.height,id:o,type:"setViewport",width:t.width};return p({assertions:[],detail:`Resized viewport to ${String(t.width)}x${String(t.height)}`,duration:performance.now()-r,nodeType:"setViewport",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Resize viewport to ${String(t.width)}x${String(t.height)}`})}});import{z as Ot}from"zod";var Ao=Ot.object({selector:Ot.string().describe("CSS selector for the element to right-click")}),Vt=c({description:"Right-click an element to open the context menu",name:"right_click",schema:Ao,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({button:"right",timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"rightClick"};return p({assertions:[],detail:`Right-clicked ${t.selector}`,duration:performance.now()-r,nodeType:"rightClick",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Right-click ${t.selector}`})}});import{z as oe}from"zod";var $o=oe.object({selector:oe.string().optional().describe("CSS selector to scroll within (omit to scroll the page)"),x:oe.number().int().optional().describe("Horizontal scroll offset in pixels").default(0),y:oe.number().int().optional().describe("Vertical scroll offset in pixels").default(0)}),Ft=c({description:"Scroll the page or a specific element by pixel offset. Positive y scrolls down, negative scrolls up.",name:"scroll",schema:$o,execute:async(e,t)=>{let r=performance.now();if(t.selector==null)await e.page.mouse.wheel(t.x,t.y);else{let l=await e.page.locator(t.selector).elementHandle();if(l==null)throw new Error("Scroll target element not found");await l.evaluate(`(el) => el.scrollBy(${String(t.x)}, ${String(t.y)})`)}let n={id:`agent-step-${String(e.stepIndex)}`,type:"scroll",x:t.x,y:t.y},s=t.selector??"page";return p({assertions:[],detail:`Scrolled ${s} by (${String(t.x)}, ${String(t.y)})`,duration:performance.now()-r,nodeType:"scroll",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Scroll ${s}`})}});import{z as Io}from"zod";var Eo=Io.object({}),Ut=c({description:"Take a screenshot of the current page",name:"screenshot",schema:Eo,execute:async(e,t)=>{let r=performance.now(),n={id:`agent-step-${String(e.stepIndex)}`,type:"screenshot"};return p({assertions:[],detail:"Screenshot captured",duration:performance.now()-r,nodeType:"screenshot",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:"Screenshot"})}});import{z as Dt}from"zod";var jo=Dt.object({pattern:Dt.string().describe("Regex pattern to search console log messages")}),Wt=c({description:"Search console log messages for a regex pattern. Returns matching entries.",name:"search_console",schema:jo,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),o=e.monitor.consoleEntries.filter(s=>r.test(s.text));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No console messages matching: ${t.pattern}`};let n=o.map(s=>Oo(s)).join(`
30
+ ${String(r)}`}import{z as ke}from"zod";var vo=ke.object({action:ke.enum(["accept","dismiss"]).describe("Whether to accept or dismiss the dialog"),promptText:ke.string().optional().describe("Text to enter for prompt dialogs")}),Pt=c({description:"Set up a handler for the next browser dialog (alert, confirm, prompt). Call this BEFORE the action that triggers the dialog.",name:"handle_dialog",schema:vo,execute:(e,t)=>{let r=performance.now();e.page.once("dialog",async s=>{t.action==="accept"?await s.accept(t.promptText??void 0):await s.dismiss()});let o=`agent-step-${String(e.stepIndex)}`,n={action:t.action,id:o,type:"handleDialog"};return p({assertions:[],detail:`Dialog handler set: ${t.action}`,duration:performance.now()-r,nodeType:"handleDialog",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Handle dialog: ${t.action}`})}});import{z as Tt}from"zod";var xo=Tt.object({selector:Tt.string().describe("CSS selector for the element to hover over")}),Nt=c({description:"Hover over an element matching the CSS selector",name:"hover",schema:xo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).hover({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"hover"};return p({assertions:[],detail:`Hovered over ${t.selector}`,duration:performance.now()-r,nodeType:"hover",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Hover ${t.selector}`})}});import{z as Re}from"zod";var Po=Re.object({permission:Re.string().describe("Permission name (e.g. geolocation, notifications, camera, clipboard-read)"),state:Re.enum(["granted","prompt"]).describe("Permission state to set")}),Ct=c({description:"Set a browser permission state (geolocation, notifications, camera, etc.)",name:"set_permission",schema:Po,execute:async(e,t)=>{let r=performance.now(),o=e.page.context();t.state==="granted"?await o.grantPermissions([t.permission]):await o.clearPermissions();let s={id:`agent-step-${String(e.stepIndex)}`,permission:t.permission,state:t.state,type:"setPermission"};return p({assertions:[],detail:`Set ${t.permission} to ${t.state}`,duration:performance.now()-r,nodeType:"setPermission",page:e.page,runStartTime:e.runStartTime,specNode:s,status:"passed",stepIndex:e.stepIndex,title:`Set permission: ${t.permission} \u2192 ${t.state}`})}});import{z as At}from"zod";var To=At.object({url:At.string().describe("The URL to navigate to")}),$t=c({description:"Navigate the browser to a URL",name:"navigate",schema:To,execute:async(e,t)=>{let r=performance.now();await e.page.goto(t.url,{waitUntil:"domcontentloaded"});let n={id:`agent-step-${String(e.stepIndex)}`,type:"goto",url:{type:"static",value:t.url}};return p({assertions:[],detail:`Navigated to ${t.url}`,duration:performance.now()-r,nodeType:"goto",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Navigate to ${t.url}`})}});import{z as It}from"zod";var No=It.object({key:It.string().describe("Key to press, e.g. 'Enter', 'Tab', 'Escape'")}),Et=c({description:"Press a keyboard key",name:"press",schema:No,execute:async(e,t)=>{let r=performance.now();await e.page.keyboard.press(t.key);let n={id:`agent-step-${String(e.stepIndex)}`,key:t.key,type:"press"};return p({assertions:[],detail:`Pressed ${t.key}`,duration:performance.now()-r,nodeType:"press",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Press ${t.key}`})}});import{z as ve}from"zod";var Co=ve.object({height:ve.number().int().positive().describe("Viewport height in pixels"),width:ve.number().int().positive().describe("Viewport width in pixels")}),jt=c({description:"Resize the browser viewport to the specified dimensions. Use this to test responsive layouts at different screen sizes (e.g. mobile: 375x812, tablet: 768x1024, desktop: 1440x900).",name:"resize_viewport",schema:Co,execute:async(e,t)=>{let r=performance.now();await e.page.setViewportSize({height:t.height,width:t.width});let o=`agent-step-${String(e.stepIndex)}`,n={height:t.height,id:o,type:"setViewport",width:t.width};return p({assertions:[],detail:`Resized viewport to ${String(t.width)}x${String(t.height)}`,duration:performance.now()-r,nodeType:"setViewport",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Resize viewport to ${String(t.width)}x${String(t.height)}`})}});import{z as Ot}from"zod";var Ao=Ot.object({selector:Ot.string().describe("CSS selector for the element to right-click")}),Vt=c({description:"Right-click an element to open the context menu",name:"right_click",schema:Ao,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({button:"right",timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"rightClick"};return p({assertions:[],detail:`Right-clicked ${t.selector}`,duration:performance.now()-r,nodeType:"rightClick",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Right-click ${t.selector}`})}});import{z as oe}from"zod";var $o=oe.object({selector:oe.string().optional().describe("CSS selector to scroll within (omit to scroll the page)"),x:oe.number().int().optional().describe("Horizontal scroll offset in pixels").default(0),y:oe.number().int().optional().describe("Vertical scroll offset in pixels").default(0)}),Ft=c({description:"Scroll the page or a specific element by pixel offset. Positive y scrolls down, negative scrolls up.",name:"scroll",schema:$o,execute:async(e,t)=>{let r=performance.now();if(t.selector==null)await e.page.mouse.wheel(t.x,t.y);else{let l=await e.page.locator(t.selector).elementHandle();if(l==null)throw new Error("Scroll target element not found");await l.evaluate(`(el) => el.scrollBy(${String(t.x)}, ${String(t.y)})`)}let n={id:`agent-step-${String(e.stepIndex)}`,type:"scroll",x:t.x,y:t.y},s=t.selector??"page";return p({assertions:[],detail:`Scrolled ${s} by (${String(t.x)}, ${String(t.y)})`,duration:performance.now()-r,nodeType:"scroll",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Scroll ${s}`})}});import{z as Io}from"zod";var Eo=Io.object({}),Ut=c({description:"Take a screenshot of the current page",name:"screenshot",schema:Eo,execute:async(e,t)=>{let r=performance.now(),n={id:`agent-step-${String(e.stepIndex)}`,type:"screenshot"};return p({assertions:[],detail:"Screenshot captured",duration:performance.now()-r,nodeType:"screenshot",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:"Screenshot"})}});import{z as Dt}from"zod";var jo=Dt.object({pattern:Dt.string().describe("Regex pattern to search console log messages")}),Lt=c({description:"Search console log messages for a regex pattern. Returns matching entries.",name:"search_console",schema:jo,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),o=e.monitor.consoleEntries.filter(s=>r.test(s.text));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No console messages matching: ${t.pattern}`};let n=o.map(s=>Oo(s)).join(`
31
31
  `);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(o.length)} matching console messages:
32
- ${n}`}}});function Oo(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as qt}from"zod";var Vo=qt.object({pattern:qt.string().describe("Regex pattern to search across network request URLs and response bodies")}),Lt=c({description:"Search network requests and response bodies for a regex pattern. Useful for finding API calls, tokens, or specific data in responses.",name:"search_network",schema:Vo,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),o=e.monitor.networkEntries.filter(s=>r.test(s.url)||s.responseBody!=null&&r.test(s.responseBody));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No network entries matching: ${t.pattern}`};let n=o.map(s=>{let l=s.statusCode==null?"pending":String(s.statusCode),d=r.test(s.url)?" [URL match]":"",i=s.responseBody!=null&&r.test(s.responseBody)?" [body match]":"";return`${s.method} ${l} ${s.url}${d}${i}`}).join(`
32
+ ${n}`}}});function Oo(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as Wt}from"zod";var Vo=Wt.object({pattern:Wt.string().describe("Regex pattern to search across network request URLs and response bodies")}),qt=c({description:"Search network requests and response bodies for a regex pattern. Useful for finding API calls, tokens, or specific data in responses.",name:"search_network",schema:Vo,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),o=e.monitor.networkEntries.filter(s=>r.test(s.url)||s.responseBody!=null&&r.test(s.responseBody));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No network entries matching: ${t.pattern}`};let n=o.map(s=>{let l=s.statusCode==null?"pending":String(s.statusCode),d=r.test(s.url)?" [URL match]":"",i=s.responseBody!=null&&r.test(s.responseBody)?" [body match]":"";return`${s.method} ${l} ${s.url}${d}${i}`}).join(`
33
33
  `);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(o.length)} matching network entries:
34
34
  ${n}`}}});import{z as _t}from"zod";var Fo=20,Uo=2,Do=_t.object({pattern:_t.string().describe("Regex pattern to search for in the page HTML")}),Bt=c({description:"Search the current page's full HTML/DOM for a regex pattern. Returns matching lines with context. Useful for finding elements, attributes, text content, or hidden data without reading the entire page.",name:"search_page",schema:Do,execute:async(e,t)=>{let o=(await e.page.content()).split(`
35
- `),n=Wo({contextLines:Uo,lines:o,maxResults:Fo,pattern:t.pattern});return n.length===0?{specNode:void 0,stepResult:void 0,toolOutput:`No matches found for pattern: ${t.pattern}`}:{specNode:void 0,stepResult:void 0,toolOutput:n.join(`
35
+ `),n=Lo({contextLines:Uo,lines:o,maxResults:Fo,pattern:t.pattern});return n.length===0?{specNode:void 0,stepResult:void 0,toolOutput:`No matches found for pattern: ${t.pattern}`}:{specNode:void 0,stepResult:void 0,toolOutput:n.join(`
36
36
  ---
37
- `)}}});function Wo({contextLines:e,lines:t,maxResults:r,pattern:o}){let n=new RegExp(o,"i"),s=[];return t.forEach((l,d)=>{if(s.length>=r||!n.test(l))return;let i=Math.max(0,d-e),u=Math.min(t.length-1,d+e),h=t.slice(i,u+1).map((E,te)=>{let Wr=i+te+1;return`${i+te===d?">":" "} ${String(Wr)}: ${E}`}).join(`
38
- `);s.push(h)}),s}import{z as xe}from"zod";var qo=xe.object({selector:xe.string().describe("CSS selector for the select element"),value:xe.string().describe("The option value to select")}),Mt=c({description:"Select an option from a dropdown/select element",name:"select",schema:qo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).selectOption(t.value,{timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"select",value:{type:"static",value:t.value}};return p({assertions:[],detail:`Selected "${t.value}" in ${t.selector}`,duration:performance.now()-r,nodeType:"select",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Select ${t.value}`})}});import{z as Gt}from"zod";var Lo=Gt.object({selector:Gt.string().describe("CSS selector for the checkbox to uncheck")}),zt=c({description:"Uncheck a checkbox matching the CSS selector",name:"uncheck",schema:Lo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).uncheck({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"uncheck"};return p({assertions:[],detail:`Unchecked ${t.selector}`,duration:performance.now()-r,nodeType:"uncheck",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Uncheck ${t.selector}`})}});import{z as Ht}from"zod";var _o=Ht.object({ms:Ht.number().describe("Milliseconds to wait")}),Jt=c({description:"Wait for a specified duration in milliseconds",name:"wait",schema:_o,execute:async(e,t)=>{let r=performance.now();await e.page.waitForTimeout(t.ms);let o=`agent-step-${String(e.stepIndex)}`,n={duration:t.ms,id:o,type:"wait"};return p({assertions:[],detail:`Waited ${String(t.ms)}ms`,duration:performance.now()-r,nodeType:"wait",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Wait ${String(t.ms)}ms`})}});import{z as Pe}from"zod";var Bo=Pe.object({selector:Pe.string().describe("CSS selector for the element to wait for"),state:Pe.enum(["visible","hidden","attached","detached"]).default("visible").describe("The state to wait for (default: visible)")}),Kt=c({description:"Wait for an element matching the CSS selector to reach the specified state. More reliable than a blind wait \u2014 use this to wait for elements to appear, disappear, or attach/detach from the DOM.",name:"wait_for",schema:Bo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).waitFor({state:t.state,timeout:15e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},state:t.state,type:"waitFor"};return p({assertions:[],detail:`Waited for ${t.selector} to be ${t.state}`,duration:performance.now()-r,nodeType:"waitFor",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Wait for ${t.selector} to be ${t.state}`})}});var Te=[$t,at,ut,Et,tt,zt,Mt,Nt,Jt,Kt,Ut,St,Bt,ot,Vt,Ft,Pt,st,Ct,Ze,Ye,Qe,Me,Je,ze,Ke,ft,ht,yt,kt,bt,xt,Wt,Lt,jt,dt,lt],yd=new Map(Te.map(e=>[e.name,e])),Mo=Te.map(e=>e.anthropicTool),Go=Te.map(e=>`- **${e.name}**: ${e.anthropicTool.description??""}`).join(`
39
- `);import zo from"pino";var N=zo({transport:{options:{ignore:"pid,hostname"},target:"pino-pretty"}});import{execFileSync as Ed}from"child_process";import{createRequire as Od}from"module";import Fd from"fs";import Dd from"path";import{chromium as qd}from"playwright";import{mkdir as Md,writeFile as Gd}from"fs/promises";import Hd from"path";import{readdir as au,rm as su,stat as iu}from"fs/promises";import Zo from"path";import{graphql as en}from"gql.tada";import{print as Yo}from"graphql";async function V(e){let t=Yo(e.document),r=JSON.stringify({query:t,variables:e.variables}),n=await(await fetch(`${e.config.ripploServerUrl}/graphql`,{body:r,headers:{Authorization:`Bearer ${e.config.token}`,"Content-Type":"application/json"},method:"POST"})).json();if(!Qo(n))throw new Error("Invalid GraphQL response");if(Xo(n))throw new Error(n.errors.map(s=>s.message).join(", "));if(n.data==null)throw new Error("No data returned from server");return n.data}function Qo(e){return typeof e=="object"&&e!=null&&("data"in e||"errors"in e)}function Xo(e){return Array.isArray(e.errors)&&e.errors.length>0}var pu=Zo.join(process.cwd(),".ripplo","debug"),fu=360*60*1e3,mu=en(`
37
+ `)}}});function Lo({contextLines:e,lines:t,maxResults:r,pattern:o}){let n=new RegExp(o,"i"),s=[];return t.forEach((l,d)=>{if(s.length>=r||!n.test(l))return;let i=Math.max(0,d-e),u=Math.min(t.length-1,d+e),h=t.slice(i,u+1).map((E,te)=>{let Lr=i+te+1;return`${i+te===d?">":" "} ${String(Lr)}: ${E}`}).join(`
38
+ `);s.push(h)}),s}import{z as xe}from"zod";var Wo=xe.object({selector:xe.string().describe("CSS selector for the select element"),value:xe.string().describe("The option value to select")}),Mt=c({description:"Select an option from a dropdown/select element",name:"select",schema:Wo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).selectOption(t.value,{timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"select",value:{type:"static",value:t.value}};return p({assertions:[],detail:`Selected "${t.value}" in ${t.selector}`,duration:performance.now()-r,nodeType:"select",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Select ${t.value}`})}});import{z as Gt}from"zod";var qo=Gt.object({selector:Gt.string().describe("CSS selector for the checkbox to uncheck")}),zt=c({description:"Uncheck a checkbox matching the CSS selector",name:"uncheck",schema:qo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).uncheck({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"uncheck"};return p({assertions:[],detail:`Unchecked ${t.selector}`,duration:performance.now()-r,nodeType:"uncheck",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Uncheck ${t.selector}`})}});import{z as Ht}from"zod";var _o=Ht.object({ms:Ht.number().describe("Milliseconds to wait")}),Jt=c({description:"Wait for a specified duration in milliseconds",name:"wait",schema:_o,execute:async(e,t)=>{let r=performance.now();await e.page.waitForTimeout(t.ms);let o=`agent-step-${String(e.stepIndex)}`,n={duration:t.ms,id:o,type:"wait"};return p({assertions:[],detail:`Waited ${String(t.ms)}ms`,duration:performance.now()-r,nodeType:"wait",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Wait ${String(t.ms)}ms`})}});import{z as Pe}from"zod";var Bo=Pe.object({selector:Pe.string().describe("CSS selector for the element to wait for"),state:Pe.enum(["visible","hidden","attached","detached"]).default("visible").describe("The state to wait for (default: visible)")}),Yt=c({description:"Wait for an element matching the CSS selector to reach the specified state. More reliable than a blind wait \u2014 use this to wait for elements to appear, disappear, or attach/detach from the DOM.",name:"wait_for",schema:Bo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).waitFor({state:t.state,timeout:15e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},state:t.state,type:"waitFor"};return p({assertions:[],detail:`Waited for ${t.selector} to be ${t.state}`,duration:performance.now()-r,nodeType:"waitFor",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Wait for ${t.selector} to be ${t.state}`})}});var Te=[$t,at,ut,Et,tt,zt,Mt,Nt,Jt,Yt,Ut,St,Bt,ot,Vt,Ft,Pt,st,Ct,Ze,Ke,Qe,Me,Je,ze,Ye,ft,ht,yt,kt,bt,xt,Lt,qt,jt,dt,lt],yd=new Map(Te.map(e=>[e.name,e])),Mo=Te.map(e=>e.anthropicTool),Go=Te.map(e=>`- **${e.name}**: ${e.anthropicTool.description??""}`).join(`
39
+ `);import zo from"pino";var N=zo({transport:{options:{ignore:"pid,hostname"},target:"pino-pretty"}});import{execFileSync as Ed}from"child_process";import{createRequire as Od}from"module";import Fd from"fs";import Dd from"path";import{chromium as Wd}from"playwright";import{mkdir as Md,writeFile as Gd}from"fs/promises";import Hd from"path";import{readdir as au,rm as su,stat as iu}from"fs/promises";import Zo from"path";import{graphql as en}from"gql.tada";import{print as Ko}from"graphql";async function V(e){let t=Ko(e.document),r=JSON.stringify({query:t,variables:e.variables}),n=await(await fetch(`${e.config.ripploServerUrl}/graphql`,{body:r,headers:{Authorization:`Bearer ${e.config.token}`,"Content-Type":"application/json"},method:"POST"})).json();if(!Qo(n))throw new Error("Invalid GraphQL response");if(Xo(n))throw new Error(n.errors.map(s=>s.message).join(", "));if(n.data==null)throw new Error("No data returned from server");return n.data}function Qo(e){return typeof e=="object"&&e!=null&&("data"in e||"errors"in e)}function Xo(e){return Array.isArray(e.errors)&&e.errors.length>0}var pu=Zo.join(process.cwd(),".ripplo","debug"),fu=360*60*1e3,mu=en(`
40
40
  mutation RevokeCurrentCliToken {
41
41
  revokeCurrentCliToken
42
42
  }
43
- `);import tr from"fs";import Ne from"path";import{z as F}from"zod";var on=F.object({baseUrl:F.string().optional(),cloudBaseUrl:F.string(),preconditionApiPath:F.string().optional(),projectId:F.string()}),nn=F.object({ripploServerUrl:F.string(),token:F.string()}),an=on.extend(nn.shape),or=Ne.join(process.cwd(),".ripplo"),sn=Ne.join(or,"settings.json"),ln=Ne.join(or,"settings.local.json");function rr(e){if(!tr.existsSync(e))return null;let t=tr.readFileSync(e,"utf8");return JSON.parse(t)}function Ce(){let e=rr(sn),t=rr(ln);if(e==null||t==null)return null;let r=an.safeParse({...e,...t});return r.success?r.data:null}import{z as a}from"zod";import{z as C}from"zod";import{z as y}from"zod";var dn=y.object({by:y.literal("css"),value:y.string().min(1)}),un=y.object({by:y.literal("testId"),value:y.string().min(1)}),pn=y.object({by:y.literal("role"),exact:y.boolean().optional(),name:y.string().optional(),role:y.string().min(1)}),fn=y.object({by:y.literal("text"),exact:y.boolean().optional(),value:y.string().min(1)}),mn=y.object({by:y.literal("label"),exact:y.boolean().optional(),value:y.string().min(1)}),hn=y.object({by:y.literal("placeholder"),value:y.string().min(1)}),gn=y.object({by:y.literal("altText"),value:y.string().min(1)}),m=y.discriminatedUnion("by",[dn,un,pn,fn,mn,hn,gn]);import{z as nr}from"zod";var R=nr.enum(["equals","notEquals","contains","startsWith","endsWith","matches"]),j=nr.enum(["equals","notEquals","greaterThan","greaterThanOrEqual","lessThan","lessThanOrEqual"]);import{z as S}from"zod";var yn=S.object({type:S.literal("static"),value:S.union([S.string(),S.number(),S.boolean()])}),Ae=S.object({name:S.string().min(1),type:S.literal("variable")}),U=S.discriminatedUnion("type",[yn,Ae]),g=S.discriminatedUnion("type",[S.object({type:S.literal("static"),value:S.string()}),Ae]),W=S.discriminatedUnion("type",[S.object({type:S.literal("static"),value:S.number().int().nonnegative()}),Ae]);var q=C.discriminatedUnion("type",[C.object({locator:m,type:C.literal("elementVisible")}),C.object({locator:m,type:C.literal("elementNotVisible")}),C.object({expected:g,operator:R,type:C.literal("urlMatch")}),C.object({expected:g,locator:m,operator:R,type:C.literal("textMatch")}),C.object({expected:U,operator:j,type:C.literal("variableCompare"),variable:C.string().min(1)})]);import{z as P}from"zod";var z=P.discriminatedUnion("type",[P.object({default:P.string().optional(),type:P.literal("string")}),P.object({default:P.number().optional(),type:P.literal("number")}),P.object({default:P.boolean().optional(),type:P.literal("boolean")}),P.object({key:P.string().min(1),type:P.literal("env")})]);var wn=a.string().min(1),O=a.array(wn).min(1),f={comment:a.string().optional(),id:a.string().min(1),label:a.string().optional(),next:a.string().optional(),timeout:a.number().int().positive().optional()},bn=a.object({...f,type:a.literal("goto"),url:g}),Sn=a.object({...f,locator:m,type:a.literal("click")}),kn=a.object({...f,locator:m,type:a.literal("fill"),value:g}),Rn=a.object({...f,locator:m,type:a.literal("select"),value:g}),vn=a.object({...f,locator:m,type:a.literal("hover")}),xn=a.object({...f,key:a.string().min(1),locator:m.optional(),type:a.literal("press")}),Pn=a.object({...f,locator:m,type:a.literal("check")}),Tn=a.object({...f,locator:m,type:a.literal("uncheck")}),Nn=a.object({...f,type:a.literal("screenshot")}),Cn=a.object({...f,height:a.number().int().positive(),type:a.literal("setViewport"),width:a.number().int().positive()}),An=a.object({...f,duration:a.number().int().positive(),type:a.literal("wait")}),$n=a.object({...f,message:a.string().min(1),type:a.literal("fail")}),In=a.object({...f,type:a.literal("setVariable"),value:U,variable:a.string().min(1)}),En=a.object({...f,locator:m,type:a.literal("extractText"),variable:a.string().min(1)}),jn=a.object({...f,files:a.array(a.string()).min(1),locator:m,type:a.literal("upload")}),On=a.object({...f,locator:m,type:a.literal("dblclick")}),Vn=a.object({...f,source:m,target:m,type:a.literal("drag")}),Fn=a.object({...f,locator:m,type:a.literal("scrollIntoView")}),Un=a.object({...f,locator:m,type:a.literal("type"),value:g}),Dn=a.object({...f,locator:m,type:a.literal("focus")}),Wn=a.object({...f,locator:m,type:a.literal("clear")}),qn=a.object({...f,locator:m,type:a.literal("rightClick")}),Ln=a.object({...f,action:a.enum(["accept","dismiss"]),promptText:a.string().optional(),type:a.literal("handleDialog")}),_n=a.object({...f,locator:m.optional(),type:a.literal("scroll"),x:a.number().int().optional(),y:a.number().int().optional()}),Bn=a.object({...f,action:a.enum(["read","write"]),type:a.literal("clipboard"),value:g.optional(),variable:a.string().min(1).optional()}),Mn=a.object({...f,permission:a.string().min(1),state:a.enum(["granted","prompt"]),type:a.literal("setPermission")}),Gn=a.object({...f,locator:m,state:a.enum(["visible","hidden","attached","detached"]).optional(),type:a.literal("waitFor")}),zn=a.object({...f,expected:g,operator:R,type:a.literal("waitForUrl")}),Hn=a.object({...f,type:a.literal("waitForResponse"),urlPattern:g}),Jn=a.object({...f,type:a.literal("waitForRequest"),urlPattern:g}),Kn=a.object({...f,locator:m,type:a.literal("assertVisible")}),Yn=a.object({...f,locator:m,type:a.literal("assertNotVisible")}),Qn=a.object({...f,expected:g,locator:m,operator:R,type:a.literal("assertText")}),Xn=a.object({...f,expected:g,operator:R,type:a.literal("assertUrl")}),Zn=a.object({...f,expected:W,locator:m,operator:j,type:a.literal("assertCount")}),ea=a.object({...f,expected:g,locator:m,operator:R,type:a.literal("assertValue")}),ta=a.object({...f,attribute:a.string().min(1),expected:g,locator:m,operator:R,type:a.literal("assertAttribute")}),ra=a.object({...f,locator:m,type:a.literal("assertEnabled")}),oa=a.object({...f,locator:m,type:a.literal("assertDisabled")}),na=a.object({...f,expected:g,operator:R,type:a.literal("assertTitle")}),aa=a.object({...f,locator:m,type:a.literal("assertChecked")}),sa=a.object({...f,locator:m,type:a.literal("assertNotChecked")}),ia=a.object({...f,locator:m,type:a.literal("assertFocused")}),la=a.object({...f,httpOnly:a.boolean().optional(),name:g,operator:R.optional(),sameSite:a.enum(["Strict","Lax","None"]).optional(),secure:a.boolean().optional(),type:a.literal("assertCookie"),value:g.optional()}),ca=a.object({...f,bodyContains:g.optional(),headerContains:a.object({name:a.string().min(1),value:g}).optional(),status:a.number().int().positive().optional(),type:a.literal("assertResponse"),urlPattern:g}),da=a.object({...f,alternate:O.optional(),condition:q,consequent:O,type:a.literal("if")}),ua=a.object({...f,body:O,iteratorVar:a.string().min(1).optional(),times:a.number().int().positive(),type:a.literal("loop")}),pa=a.object({...f,body:O,collection:a.union([a.array(a.record(a.string(),a.unknown())).min(1),a.object({name:a.string().min(1),type:a.literal("variable")})]),iteratorVar:a.string().min(1),type:a.literal("forEach")}),fa=a.object({...f,branches:a.array(O).min(2),type:a.literal("parallel")}),ma=a.object({...f,body:O,catch:O.optional(),finally:O.optional(),type:a.literal("try")}).refine(e=>e.catch!=null||e.finally!=null,{message:"try node must have at least one of 'catch' or 'finally'"}),ha=a.object({...f,nodes:O,type:a.literal("group")}),X=a.discriminatedUnion("type",[bn,Sn,kn,Rn,vn,xn,Pn,Tn,Gn,zn,Hn,Jn,Kn,Yn,Qn,Xn,Zn,ea,ta,ra,oa,Nn,Cn,An,$n,In,En,jn,On,Vn,Fn,Un,Dn,Wn,qn,Ln,_n,Bn,Mn,na,aa,sa,ia,la,ca,da,ua,pa,fa,ha]),ar=a.union([X,ma]),L=a.object({entryNode:a.string().min(1),nodes:a.record(a.string(),ar),variables:a.record(a.string(),z).optional(),version:a.literal(2)});function ne(e){let t=L.safeParse(e);return t.success?{data:t.data,success:!0}:{errors:t.error.issues.map(r=>({message:r.message,path:r.path.join(".")})),success:!1}}import{graphql as ce,readFragment as bf}from"gql.tada";import{z as ae}from"zod";var $e=ae.object({from:ae.string().min(1).describe("Key of the source state in the states record"),to:ae.string().min(1).describe("Key of the target state in the states record"),workflow:ae.string().min(1).describe("Filename (without .json) of the workflow in .ripplo/workflows/ that executes this edge")}).describe("A directed edge between two states, executed by a workflow");import{z as D}from"zod";import{z as b}from"zod";var Ie=b.object({precondition:b.string().min(1).describe("Name of the precondition to check or execute, e.g. 'auth:admin'")}).describe("Request payload sent by Ripplo to both the check and execute endpoints"),se=b.object({satisfied:b.boolean().describe("Whether the precondition is already satisfied. If true, execution is skipped.")}).describe("Response from POST {preconditionApiUrl}/check"),ie=b.object({data:b.record(b.string(),b.string()).optional().describe("Key-value data returned by the precondition. Values are injected as variables that can be interpolated into route patterns ({{key}}) and workflow spec variable references. For example, a data:project precondition should return { projectId: 'clx...' } so the route /projects/{{projectId}}/settings resolves correctly."),error:b.string().optional().describe("Human-readable error message if success is false"),navigateTo:b.string().optional().describe("URL to navigate the browser to after this precondition executes. Use this to set the starting page for the test \u2014 e.g. an auth token URL or the page the workflow begins on. If multiple preconditions return navigateTo, only the last one is used."),success:b.boolean().describe("Whether the precondition was successfully satisfied")}).describe("Response from PUT {preconditionApiUrl}/execute. Set-Cookie headers are automatically captured and injected into the browser context. The data field provides values for route param interpolation and workflow variables."),Ee=b.object({preconditions:b.array(b.string().min(1)).describe("Names of all preconditions that were executed during this run")}).describe("Request payload sent after a run completes. Fire-and-forget \u2014 errors are logged but don't fail the run."),je=b.object({error:b.string().optional().describe("Human-readable error message if success is false"),success:b.boolean().describe("Whether teardown completed successfully")}).describe("Response from PUT {preconditionApiUrl}/teardown"),_=b.object({depends:b.array(b.string().min(1)).optional().describe("Names of other preconditions that must be satisfied first. Resolved via topological sort; cycles are rejected at validation time."),description:b.string().min(1).describe("Human-readable description of what this precondition ensures"),returns:b.array(b.string().min(1)).optional().describe("Keys that the execute response's data field will contain. e.g. ['projectId', 'workflowId']. These are used for route param interpolation ({{projectId}}) and workflow variables. Declared here so generated types are strongly typed per precondition.")}).describe("A named precondition declared at the graph level. States reference these by name.");import{z as le}from"zod";var Oe=le.object({preconditions:le.array(le.string().min(1)).describe("Ordered list of precondition names to satisfy before entering this state"),route:le.string().min(1).describe("URL pattern with {{placeholders}} for dynamic segments, e.g. '/projects/{{projectId}}/settings'")}).describe("A distinct application state \u2014 a unique combination of location, auth context, and data conditions");var Z=D.object({edges:D.array($e).describe("Directed edges between states, each executed by a workflow"),preconditions:D.record(D.string(),_).describe("Named preconditions keyed by name (e.g. 'auth:admin', 'data:three-projects'). States reference these by name."),resetPrecondition:D.string().optional().describe("Name of a precondition to run before every state setup as a global clean slate"),states:D.record(D.string(),Oe).describe("States keyed by stable ID (kebab-case)"),version:D.literal(3).describe("Schema version, always 3")}).describe("Ripplo State Graph v3 \u2014 models application states, edges, and executable preconditions");function Ve(e){let t=Z.safeParse(e);return t.success?{data:t.data,success:!0}:{errors:t.error.issues.map(r=>({message:r.message,path:r.path.join(".")})),success:!1}}function Aa(e){let t=e.preconditions,r=new Set,o=new Set,n=[];function s(l){if(o.has(l)){n.push(`Precondition "${l}" has a circular dependency`);return}if(r.has(l))return;r.add(l),o.add(l);let d=t[l];d?.depends!=null&&d.depends.forEach(i=>{s(i)}),o.delete(l)}return Object.keys(t).forEach(l=>{s(l)}),n}function Fe(e){let t=[],r=new Set(Object.keys(e.preconditions)),o=new Set(Object.keys(e.states));e.edges.forEach((i,u)=>{o.has(i.from)||t.push({message:`References non-existent source state "${i.from}"`,path:`edges[${String(u)}].from`}),o.has(i.to)||t.push({message:`References non-existent target state "${i.to}"`,path:`edges[${String(u)}].to`})}),Object.entries(e.states).forEach(([i,u])=>{u.preconditions.forEach(h=>{r.has(h)||t.push({message:`References non-existent precondition "${h}"`,path:`states.${i}.preconditions`})})}),Object.entries(e.preconditions).forEach(([i,u])=>{u.depends!=null&&u.depends.forEach(h=>{r.has(h)||t.push({message:`Depends on non-existent precondition "${h}"`,path:`preconditions.${i}.depends`})})}),e.resetPrecondition!=null&&!r.has(e.resetPrecondition)&&t.push({message:`References non-existent precondition "${e.resetPrecondition}"`,path:"resetPrecondition"});let n=new Set(e.edges.flatMap(i=>[i.from,i.to]));Object.keys(e.states).forEach(i=>{n.has(i)||t.push({message:"State is not referenced by any edge (orphan)",path:`states.${i}`})});let s=new Set;Object.values(e.states).forEach(i=>{i.preconditions.forEach(u=>s.add(u))}),Object.values(e.preconditions).forEach(i=>{i.depends!=null&&i.depends.forEach(u=>s.add(u))}),e.resetPrecondition!=null&&s.add(e.resetPrecondition),Object.keys(e.preconditions).forEach(i=>{s.has(i)||t.push({message:"Defined but never used by any state or dependency",path:`preconditions.${i}`})});let l=new Set;return e.edges.forEach((i,u)=>{let h=`${i.from}|${i.to}|${i.workflow}`;l.has(h)&&t.push({message:`Duplicate edge from "${i.from}" to "${i.to}" with workflow "${i.workflow}"`,path:`edges[${String(u)}]`}),l.add(h)}),Aa(e).forEach(i=>t.push({message:i,path:"preconditions"})),t}import{parseSetCookie as of}from"set-cookie-parser";import{z as ir}from"zod";var sf=ir.record(ir.string(),_);import{graphql as Ue}from"gql.tada";var pf=Ue(`
43
+ `);import tr from"fs";import Ne from"path";import{z as F}from"zod";var on=F.object({baseUrl:F.string().optional(),cloudBaseUrl:F.string(),preconditionApiPath:F.string().optional(),projectId:F.string()}),nn=F.object({ripploServerUrl:F.string(),token:F.string()}),an=on.extend(nn.shape),or=Ne.join(process.cwd(),".ripplo"),sn=Ne.join(or,"settings.json"),ln=Ne.join(or,"settings.local.json");function rr(e){if(!tr.existsSync(e))return null;let t=tr.readFileSync(e,"utf8");return JSON.parse(t)}function Ce(){let e=rr(sn),t=rr(ln);if(e==null||t==null)return null;let r=an.safeParse({...e,...t});return r.success?r.data:null}import{z as a}from"zod";import{z as C}from"zod";import{z as y}from"zod";var dn=y.object({by:y.literal("css"),value:y.string().min(1)}),un=y.object({by:y.literal("testId"),value:y.string().min(1)}),pn=y.object({by:y.literal("role"),exact:y.boolean().optional(),name:y.string().optional(),role:y.string().min(1)}),fn=y.object({by:y.literal("text"),exact:y.boolean().optional(),value:y.string().min(1)}),mn=y.object({by:y.literal("label"),exact:y.boolean().optional(),value:y.string().min(1)}),hn=y.object({by:y.literal("placeholder"),value:y.string().min(1)}),gn=y.object({by:y.literal("altText"),value:y.string().min(1)}),m=y.discriminatedUnion("by",[dn,un,pn,fn,mn,hn,gn]);import{z as nr}from"zod";var R=nr.enum(["equals","notEquals","contains","startsWith","endsWith","matches"]),j=nr.enum(["equals","notEquals","greaterThan","greaterThanOrEqual","lessThan","lessThanOrEqual"]);import{z as S}from"zod";var yn=S.object({type:S.literal("static"),value:S.union([S.string(),S.number(),S.boolean()])}),Ae=S.object({name:S.string().min(1),type:S.literal("variable")}),U=S.discriminatedUnion("type",[yn,Ae]),g=S.discriminatedUnion("type",[S.object({type:S.literal("static"),value:S.string()}),Ae]),L=S.discriminatedUnion("type",[S.object({type:S.literal("static"),value:S.number().int().nonnegative()}),Ae]);var W=C.discriminatedUnion("type",[C.object({locator:m,type:C.literal("elementVisible")}),C.object({locator:m,type:C.literal("elementNotVisible")}),C.object({expected:g,operator:R,type:C.literal("urlMatch")}),C.object({expected:g,locator:m,operator:R,type:C.literal("textMatch")}),C.object({expected:U,operator:j,type:C.literal("variableCompare"),variable:C.string().min(1)})]);import{z as P}from"zod";var z=P.discriminatedUnion("type",[P.object({default:P.string().optional(),type:P.literal("string")}),P.object({default:P.number().optional(),type:P.literal("number")}),P.object({default:P.boolean().optional(),type:P.literal("boolean")}),P.object({key:P.string().min(1),type:P.literal("env")})]);var wn=a.string().min(1),O=a.array(wn).min(1),f={comment:a.string().optional(),id:a.string().min(1),label:a.string().optional(),next:a.string().optional(),timeout:a.number().int().positive().optional()},bn=a.object({...f,type:a.literal("goto"),url:g}),Sn=a.object({...f,locator:m,type:a.literal("click")}),kn=a.object({...f,locator:m,type:a.literal("fill"),value:g}),Rn=a.object({...f,locator:m,type:a.literal("select"),value:g}),vn=a.object({...f,locator:m,type:a.literal("hover")}),xn=a.object({...f,key:a.string().min(1),locator:m.optional(),type:a.literal("press")}),Pn=a.object({...f,locator:m,type:a.literal("check")}),Tn=a.object({...f,locator:m,type:a.literal("uncheck")}),Nn=a.object({...f,type:a.literal("screenshot")}),Cn=a.object({...f,height:a.number().int().positive(),type:a.literal("setViewport"),width:a.number().int().positive()}),An=a.object({...f,duration:a.number().int().positive(),type:a.literal("wait")}),$n=a.object({...f,message:a.string().min(1),type:a.literal("fail")}),In=a.object({...f,type:a.literal("setVariable"),value:U,variable:a.string().min(1)}),En=a.object({...f,locator:m,type:a.literal("extractText"),variable:a.string().min(1)}),jn=a.object({...f,files:a.array(a.string()).min(1),locator:m,type:a.literal("upload")}),On=a.object({...f,locator:m,type:a.literal("dblclick")}),Vn=a.object({...f,source:m,target:m,type:a.literal("drag")}),Fn=a.object({...f,locator:m,type:a.literal("scrollIntoView")}),Un=a.object({...f,locator:m,type:a.literal("type"),value:g}),Dn=a.object({...f,locator:m,type:a.literal("focus")}),Ln=a.object({...f,locator:m,type:a.literal("clear")}),Wn=a.object({...f,locator:m,type:a.literal("rightClick")}),qn=a.object({...f,action:a.enum(["accept","dismiss"]),promptText:a.string().optional(),type:a.literal("handleDialog")}),_n=a.object({...f,locator:m.optional(),type:a.literal("scroll"),x:a.number().int().optional(),y:a.number().int().optional()}),Bn=a.object({...f,action:a.enum(["read","write"]),type:a.literal("clipboard"),value:g.optional(),variable:a.string().min(1).optional()}),Mn=a.object({...f,permission:a.string().min(1),state:a.enum(["granted","prompt"]),type:a.literal("setPermission")}),Gn=a.object({...f,locator:m,state:a.enum(["visible","hidden","attached","detached"]).optional(),type:a.literal("waitFor")}),zn=a.object({...f,expected:g,operator:R,type:a.literal("waitForUrl")}),Hn=a.object({...f,type:a.literal("waitForResponse"),urlPattern:g}),Jn=a.object({...f,type:a.literal("waitForRequest"),urlPattern:g}),Yn=a.object({...f,locator:m,type:a.literal("assertVisible")}),Kn=a.object({...f,locator:m,type:a.literal("assertNotVisible")}),Qn=a.object({...f,expected:g,locator:m,operator:R,type:a.literal("assertText")}),Xn=a.object({...f,expected:g,operator:R,type:a.literal("assertUrl")}),Zn=a.object({...f,expected:L,locator:m,operator:j,type:a.literal("assertCount")}),ea=a.object({...f,expected:g,locator:m,operator:R,type:a.literal("assertValue")}),ta=a.object({...f,attribute:a.string().min(1),expected:g,locator:m,operator:R,type:a.literal("assertAttribute")}),ra=a.object({...f,locator:m,type:a.literal("assertEnabled")}),oa=a.object({...f,locator:m,type:a.literal("assertDisabled")}),na=a.object({...f,expected:g,operator:R,type:a.literal("assertTitle")}),aa=a.object({...f,locator:m,type:a.literal("assertChecked")}),sa=a.object({...f,locator:m,type:a.literal("assertNotChecked")}),ia=a.object({...f,locator:m,type:a.literal("assertFocused")}),la=a.object({...f,httpOnly:a.boolean().optional(),name:g,operator:R.optional(),sameSite:a.enum(["Strict","Lax","None"]).optional(),secure:a.boolean().optional(),type:a.literal("assertCookie"),value:g.optional()}),ca=a.object({...f,bodyContains:g.optional(),headerContains:a.object({name:a.string().min(1),value:g}).optional(),status:a.number().int().positive().optional(),type:a.literal("assertResponse"),urlPattern:g}),da=a.object({...f,alternate:O.optional(),condition:W,consequent:O,type:a.literal("if")}),ua=a.object({...f,body:O,iteratorVar:a.string().min(1).optional(),times:a.number().int().positive(),type:a.literal("loop")}),pa=a.object({...f,body:O,collection:a.union([a.array(a.record(a.string(),a.unknown())).min(1),a.object({name:a.string().min(1),type:a.literal("variable")})]),iteratorVar:a.string().min(1),type:a.literal("forEach")}),fa=a.object({...f,branches:a.array(O).min(2),type:a.literal("parallel")}),ma=a.object({...f,body:O,catch:O.optional(),finally:O.optional(),type:a.literal("try")}).refine(e=>e.catch!=null||e.finally!=null,{message:"try node must have at least one of 'catch' or 'finally'"}),ha=a.object({...f,nodes:O,type:a.literal("group")}),X=a.discriminatedUnion("type",[bn,Sn,kn,Rn,vn,xn,Pn,Tn,Gn,zn,Hn,Jn,Yn,Kn,Qn,Xn,Zn,ea,ta,ra,oa,Nn,Cn,An,$n,In,En,jn,On,Vn,Fn,Un,Dn,Ln,Wn,qn,_n,Bn,Mn,na,aa,sa,ia,la,ca,da,ua,pa,fa,ha]),ar=a.union([X,ma]),q=a.object({entryNode:a.string().min(1),nodes:a.record(a.string(),ar),variables:a.record(a.string(),z).optional(),version:a.literal(2)});function ne(e){let t=q.safeParse(e);return t.success?{data:t.data,success:!0}:{errors:t.error.issues.map(r=>({message:r.message,path:r.path.join(".")})),success:!1}}import{graphql as ce,readFragment as bf}from"gql.tada";import{z as ae}from"zod";var $e=ae.object({from:ae.string().min(1).describe("Key of the source state in the states record"),to:ae.string().min(1).describe("Key of the target state in the states record"),workflow:ae.string().min(1).describe("Filename (without .json) of the workflow in .ripplo/workflows/ that executes this edge")}).describe("A directed edge between two states, executed by a workflow");import{z as D}from"zod";import{z as b}from"zod";var Ie=b.object({precondition:b.string().min(1).describe("Name of the precondition to check or execute, e.g. 'auth:admin'")}).describe("Request payload sent by Ripplo to both the check and execute endpoints"),se=b.object({satisfied:b.boolean().describe("Whether the precondition is already satisfied. If true, execution is skipped.")}).describe("Response from POST {preconditionApiUrl}/check"),ie=b.object({data:b.record(b.string(),b.string()).optional().describe("Key-value data returned by the precondition. Values are injected as variables that can be interpolated into route patterns ({{key}}) and workflow spec variable references. For example, a data:project precondition should return { projectId: 'clx...' } so the route /projects/{{projectId}}/settings resolves correctly."),error:b.string().optional().describe("Human-readable error message if success is false"),navigateTo:b.string().optional().describe("URL to navigate the browser to after this precondition executes. Use this to set the starting page for the test \u2014 e.g. an auth token URL or the page the workflow begins on. If multiple preconditions return navigateTo, only the last one is used."),success:b.boolean().describe("Whether the precondition was successfully satisfied")}).describe("Response from PUT {preconditionApiUrl}/execute. Set-Cookie headers are automatically captured and injected into the browser context. The data field provides values for route param interpolation and workflow variables."),Ee=b.object({preconditions:b.array(b.string().min(1)).describe("Names of all preconditions that were executed during this run")}).describe("Request payload sent after a run completes. Fire-and-forget \u2014 errors are logged but don't fail the run."),je=b.object({error:b.string().optional().describe("Human-readable error message if success is false"),success:b.boolean().describe("Whether teardown completed successfully")}).describe("Response from PUT {preconditionApiUrl}/teardown"),_=b.object({depends:b.array(b.string().min(1)).optional().describe("Names of other preconditions that must be satisfied first. Resolved via topological sort; cycles are rejected at validation time."),description:b.string().min(1).describe("Human-readable description of what this precondition ensures"),returns:b.array(b.string().min(1)).optional().describe("Keys that the execute response's data field will contain. e.g. ['projectId', 'workflowId']. These are used for route param interpolation ({{projectId}}) and workflow variables. Declared here so generated types are strongly typed per precondition.")}).describe("A named precondition declared at the graph level. States reference these by name.");import{z as le}from"zod";var Oe=le.object({preconditions:le.array(le.string().min(1)).describe("Ordered list of precondition names to satisfy before entering this state"),route:le.string().min(1).describe("URL pattern with {{placeholders}} for dynamic segments, e.g. '/projects/{{projectId}}/settings'")}).describe("A distinct application state \u2014 a unique combination of location, auth context, and data conditions");var Z=D.object({edges:D.array($e).describe("Directed edges between states, each executed by a workflow"),preconditions:D.record(D.string(),_).describe("Named preconditions keyed by name (e.g. 'auth:admin', 'data:three-projects'). States reference these by name."),resetPrecondition:D.string().optional().describe("Name of a precondition to run before every state setup as a global clean slate"),states:D.record(D.string(),Oe).describe("States keyed by stable ID (kebab-case)"),version:D.literal(3).describe("Schema version, always 3")}).describe("Ripplo State Graph v3 \u2014 models application states, edges, and executable preconditions");function Ve(e){let t=Z.safeParse(e);return t.success?{data:t.data,success:!0}:{errors:t.error.issues.map(r=>({message:r.message,path:r.path.join(".")})),success:!1}}function Aa(e){let t=e.preconditions,r=new Set,o=new Set,n=[];function s(l){if(o.has(l)){n.push(`Precondition "${l}" has a circular dependency`);return}if(r.has(l))return;r.add(l),o.add(l);let d=t[l];d?.depends!=null&&d.depends.forEach(i=>{s(i)}),o.delete(l)}return Object.keys(t).forEach(l=>{s(l)}),n}function Fe(e){let t=[],r=new Set(Object.keys(e.preconditions)),o=new Set(Object.keys(e.states));e.edges.forEach((i,u)=>{o.has(i.from)||t.push({message:`References non-existent source state "${i.from}"`,path:`edges[${String(u)}].from`}),o.has(i.to)||t.push({message:`References non-existent target state "${i.to}"`,path:`edges[${String(u)}].to`})}),Object.entries(e.states).forEach(([i,u])=>{u.preconditions.forEach(h=>{r.has(h)||t.push({message:`References non-existent precondition "${h}"`,path:`states.${i}.preconditions`})})}),Object.entries(e.preconditions).forEach(([i,u])=>{u.depends!=null&&u.depends.forEach(h=>{r.has(h)||t.push({message:`Depends on non-existent precondition "${h}"`,path:`preconditions.${i}.depends`})})}),e.resetPrecondition!=null&&!r.has(e.resetPrecondition)&&t.push({message:`References non-existent precondition "${e.resetPrecondition}"`,path:"resetPrecondition"});let n=new Set(e.edges.flatMap(i=>[i.from,i.to]));Object.keys(e.states).forEach(i=>{n.has(i)||t.push({message:"State is not referenced by any edge (orphan)",path:`states.${i}`})});let s=new Set;Object.values(e.states).forEach(i=>{i.preconditions.forEach(u=>s.add(u))}),Object.values(e.preconditions).forEach(i=>{i.depends!=null&&i.depends.forEach(u=>s.add(u))}),e.resetPrecondition!=null&&s.add(e.resetPrecondition),Object.keys(e.preconditions).forEach(i=>{s.has(i)||t.push({message:"Defined but never used by any state or dependency",path:`preconditions.${i}`})});let l=new Set;return e.edges.forEach((i,u)=>{let h=`${i.from}|${i.to}|${i.workflow}`;l.has(h)&&t.push({message:`Duplicate edge from "${i.from}" to "${i.to}" with workflow "${i.workflow}"`,path:`edges[${String(u)}]`}),l.add(h)}),Aa(e).forEach(i=>t.push({message:i,path:"preconditions"})),t}import{parseSetCookie as of}from"set-cookie-parser";import{z as ir}from"zod";var sf=ir.record(ir.string(),_);import{graphql as Ue}from"gql.tada";var pf=Ue(`
44
44
  mutation StartRunCLI($runId: String!, $platform: String!, $agentProfileId: String) {
45
45
  startRun(runId: $runId, platform: $platform, agentProfileId: $agentProfileId) {
46
46
  id
@@ -115,11 +115,11 @@ ${n}`}}});import{z as _t}from"zod";var Fo=20,Uo=2,Do=_t.object({pattern:_t.strin
115
115
  successCriteria
116
116
  }
117
117
  }
118
- `);function v(e){return{content:[{text:e,type:"text"}]}}async function lr(e){let t=Ce();return t==null?v("Not logged in. Run: ripplo login"):e(t)}import{z as x}from"zod";import Da from"fs";import pm from"path";import{z as H}from"zod";var ee=H.object({additionalChecks:H.array(H.string()).default([]),description:H.string(),expectedOutcome:H.string(),name:H.string(),spec:L});function de(e){let t=Da.readFileSync(e,"utf8"),r=JSON.parse(t);return ee.parse(r)}var Wa=x.object({additionalProperties:x.unknown().optional(),properties:x.record(x.string(),x.record(x.string(),x.unknown())).optional(),required:x.array(x.string()).optional(),type:x.string().optional()}),cr=x.object({oneOf:x.array(Wa).optional()});function dr(){let e=za(),t={category:"Control Flow",fields:"body, catch?, finally?",typeName:"try"},r=[...e.map(s=>Ha(s)),t],o=Ja(r);return`# Ripplo Workflow Spec v2
118
+ `);function v(e){return{content:[{text:e,type:"text"}]}}async function lr(e){let t=Ce();return t==null?v("Not logged in. Run: ripplo login"):e(t)}import{z as x}from"zod";import Da from"fs";import pm from"path";import{z as H}from"zod";var ee=H.object({additionalChecks:H.array(H.string()).default([]),description:H.string(),expectedOutcome:H.string(),name:H.string(),spec:q});function de(e){let t=Da.readFileSync(e,"utf8"),r=JSON.parse(t);return ee.parse(r)}var La=x.object({additionalProperties:x.unknown().optional(),properties:x.record(x.string(),x.record(x.string(),x.unknown())).optional(),required:x.array(x.string()).optional(),type:x.string().optional()}),cr=x.object({oneOf:x.array(La).optional()});function dr(){let e=za(),t={category:"Control Flow",fields:"body, catch?, finally?",typeName:"try"},r=[...e.map(s=>Ha(s)),t],o=Ja(r);return`# Ripplo Workflow Spec v2
119
119
 
120
120
  ## Node Types
121
121
 
122
- ${qa.filter(s=>o.has(s)).map(s=>{let d=(o.get(s)??[]).map(i=>`- ${i.typeName} { ${i.fields} }`);return`### ${s}
122
+ ${Wa.filter(s=>o.has(s)).map(s=>{let d=(o.get(s)??[]).map(i=>`- ${i.typeName} { ${i.fields} }`);return`### ${s}
123
123
  ${d.join(`
124
124
  `)}`}).join(`
125
125
 
@@ -152,11 +152,11 @@ Unknown node type "${s}".`);return}n.push(`## ${s}
152
152
 
153
153
  \`\`\`json
154
154
  ${JSON.stringify(l,null,2)}
155
- \`\`\``)}),n.push(Ya),n.join(`
155
+ \`\`\``)}),n.push(Ka),n.join(`
156
156
 
157
157
  `)}function pr(){return`# Spec Patterns & Building Blocks
158
158
 
159
- ${[{label:"Locator",schema:m},{label:"Condition",schema:q},{label:"StringValueRef",schema:g},{label:"NumericValueRef",schema:W},{label:"ValueRef",schema:U},{label:"ComparisonOperator (string)",schema:R},{label:"NumericOperator",schema:j},{label:"VariableDef",schema:z},{label:"WorkflowFile (top-level)",schema:ee},{label:"WorkflowSpec (the spec field)",schema:L}].map(({label:r,schema:o})=>`### ${r}
159
+ ${[{label:"Locator",schema:m},{label:"Condition",schema:W},{label:"StringValueRef",schema:g},{label:"NumericValueRef",schema:L},{label:"ValueRef",schema:U},{label:"ComparisonOperator (string)",schema:R},{label:"NumericOperator",schema:j},{label:"VariableDef",schema:z},{label:"WorkflowFile (top-level)",schema:ee},{label:"WorkflowSpec (the spec field)",schema:q}].map(({label:r,schema:o})=>`### ${r}
160
160
 
161
161
  \`\`\`json
162
162
  ${JSON.stringify(x.toJSONSchema(o),null,2)}
@@ -178,13 +178,13 @@ ${JSON.stringify(x.toJSONSchema(o),null,2)}
178
178
  10. **URLs in goto nodes must be relative paths** (\`/login\`), never absolute URLs \u2014 the baseUrl handles the origin
179
179
  11. **Value references use \`"type": "static"\`**, never \`"type": "literal"\`
180
180
  12. **Node IDs must match their key** in the \`nodes\` object
181
- 13. **Template variables use \`{{name}}\`**, never \`$name\` or \`\${name}\` \u2014 only double curly braces work`}var qa=["Actions","Waits","Assertions","Variables","Control Flow","Other"],La=new Set(["if","loop","forEach","parallel","try","group"]),_a=new Set(["setVariable","extractText"]),Ba=new Set(["screenshot","setViewport","fail"]),Ma=new Set(["comment","id","label","next","timeout","type"]);function Ga(e){return e.startsWith("assert")?"Assertions":e.startsWith("waitFor")||e==="wait"?"Waits":La.has(e)?"Control Flow":_a.has(e)?"Variables":Ba.has(e)?"Other":"Actions"}function za(){return cr.parse(structuredClone(x.toJSONSchema(X))).oneOf??[]}function fr(e){let t=e.properties?.type;if(t==null)return"";let r=t.const;return typeof r=="string"?r:""}function Ha(e){let t=fr(e),r=new Set(e.required??[]),o=Object.keys(e.properties??{}).filter(n=>!Ma.has(n)).map(n=>r.has(n)?n:`${n}?`).join(", ");return{category:Ga(t),fields:o,typeName:t}}function Ja(e){let t=new Map;return e.forEach(r=>{let o=t.get(r.category)??[];o.push(r),t.set(r.category,o)}),t}function Ka(){return`---
181
+ 13. **Template variables use \`{{name}}\`**, never \`$name\` or \`\${name}\` \u2014 only double curly braces work`}var Wa=["Actions","Waits","Assertions","Variables","Control Flow","Other"],qa=new Set(["if","loop","forEach","parallel","try","group"]),_a=new Set(["setVariable","extractText"]),Ba=new Set(["screenshot","setViewport","fail"]),Ma=new Set(["comment","id","label","next","timeout","type"]);function Ga(e){return e.startsWith("assert")?"Assertions":e.startsWith("waitFor")||e==="wait"?"Waits":qa.has(e)?"Control Flow":_a.has(e)?"Variables":Ba.has(e)?"Other":"Actions"}function za(){return cr.parse(structuredClone(x.toJSONSchema(X))).oneOf??[]}function fr(e){let t=e.properties?.type;if(t==null)return"";let r=t.const;return typeof r=="string"?r:""}function Ha(e){let t=fr(e),r=new Set(e.required??[]),o=Object.keys(e.properties??{}).filter(n=>!Ma.has(n)).map(n=>r.has(n)?n:`${n}?`).join(", ");return{category:Ga(t),fields:o,typeName:t}}function Ja(e){let t=new Map;return e.forEach(r=>{let o=t.get(r.category)??[];o.push(r),t.set(r.category,o)}),t}function Ya(){return`---
182
182
 
183
183
  ## Referenced Schemas
184
184
 
185
- ${[J("Locator",m),J("StringValueRef",g),J("NumericValueRef",W),J("ComparisonOperator",R),J("NumericOperator",j),J("Condition",q)].join(`
185
+ ${[J("Locator",m),J("StringValueRef",g),J("NumericValueRef",L),J("ComparisonOperator",R),J("NumericOperator",j),J("Condition",W)].join(`
186
186
 
187
- `)}`}var Ya=Ka();function J(e,t){return`### ${e}
187
+ `)}`}var Ka=Ya();function J(e,t){return`### ${e}
188
188
 
189
189
  \`\`\`json
190
190
  ${JSON.stringify(x.toJSONSchema(t),null,2)}
@@ -434,7 +434,7 @@ app.put("/api/test/preconditions/teardown", async (req, res) => {
434
434
  }
435
435
  res.json({ success: true });
436
436
  });
437
- \`\`\``}import We from"fs";import qe from"path";import hs from"fs";import Cm from"path";import{z as k}from"zod";var De=k.object({edges:k.array(k.object({from:k.string(),to:k.string(),workflow:k.string()})),preconditions:k.record(k.string(),k.unknown()).optional(),resetPrecondition:k.string().optional(),states:k.record(k.string(),k.object({preconditions:k.array(k.string()),route:k.string()})),version:k.literal(3)});function br(e){let t=hs.readFileSync(e,"utf8"),r=JSON.parse(t);return De.parse(r)}function Sr(e){return{edges:e.edges,preconditions:e.preconditions??{},resetPrecondition:e.resetPrecondition,states:e.states,version:e.version}}function kr(e){let t=gs(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=De.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(h=>({message:h.message,path:h.path.join(".")})),valid:!1};let o=Sr(r.data),n=Ve(o);if(!n.success)return{errors:n.errors,valid:!1};let s=[...Fe(n.data)],l=qe.join(qe.dirname(e),"workflows"),d=We.existsSync(l),i=new Set(n.data.edges.map(u=>u.workflow));return n.data.edges.forEach((u,h)=>{if(!d){s.push({message:`References workflow "${u.workflow}" but workflows directory does not exist`,path:`edges[${String(h)}].workflow`});return}let E=qe.join(l,`${u.workflow}.json`);We.existsSync(E)||s.push({message:`References workflow "${u.workflow}" but no file exists at workflows/${u.workflow}.json`,path:`edges[${String(h)}].workflow`})}),d&&We.readdirSync(l).filter(u=>u.endsWith(".json")).forEach(u=>{let h=u.replace(/\.json$/,"");i.has(h)||s.push({message:`Workflow file "workflows/${u}" is not referenced by any edge`,path:`workflows/${u}`})}),{errors:s,valid:s.length===0}}function gs(e){try{return{data:br(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}import pe from"fs";import Rr from"path";import{z as A}from"zod";var ys=A.object({baseUrl:A.string().optional(),cloudBaseUrl:A.string(),preconditionApiPath:A.string().optional(),projectId:A.string()});function vr(e){let t=Rr.join(e,".ripplo","settings.json");if(!pe.existsSync(t))return{errors:[{message:".ripplo/settings.json not found",path:""}],valid:!1,warnings:[]};let r=Ss(t);if(!r.success)return{errors:[{message:r.error,path:"settings.json"}],valid:!1,warnings:[]};let o=ys.safeParse(r.data);if(!o.success)return{errors:o.error.issues.map(d=>({message:d.message,path:d.path.join(".")})),valid:!1,warnings:[]};let n=[],s=Rr.join(e,".ripplo","graph.json");return pe.existsSync(s)&&bs(s)&&(o.data.preconditionApiPath==null||o.data.preconditionApiPath.length===0)&&n.push("Graph has preconditions but preconditionApiPath is not set in settings.json. Preconditions will not be executed. Set preconditionApiPath to the URL where your app serves precondition endpoints (can be a relative path appended to baseUrl, or an absolute URL)."),{errors:[],valid:!0,warnings:n}}var ws=A.object({states:A.record(A.string(),A.object({preconditions:A.array(A.string())}))});function bs(e){try{let t=pe.readFileSync(e,"utf8"),r=JSON.parse(t),o=ws.safeParse(r);return o.success?Object.values(o.data.states).some(n=>n.preconditions.length>0):!1}catch{return!1}}function Ss(e){try{let t=pe.readFileSync(e,"utf8");return{data:JSON.parse(t),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}var xr={assertAttribute:"assertion",assertChecked:"assertion",assertCookie:"assertion",assertCount:"assertion",assertDisabled:"assertion",assertEnabled:"assertion",assertFocused:"assertion",assertNotChecked:"assertion",assertNotVisible:"assertion",assertResponse:"assertion",assertText:"assertion",assertTitle:"assertion",assertUrl:"assertion",assertValue:"assertion",assertVisible:"assertion",check:"action",clear:"action",click:"action",clipboard:"other",dblclick:"action",drag:"action",extractText:"other",fail:"other",fill:"action",focus:"action",forEach:"controlFlow",goto:"other",group:"controlFlow",handleDialog:"action",hover:"action",if:"controlFlow",loop:"controlFlow",parallel:"controlFlow",press:"action",rightClick:"action",screenshot:"other",scroll:"action",scrollIntoView:"action",select:"action",setPermission:"other",setVariable:"other",setViewport:"other",try:"controlFlow",type:"action",uncheck:"action",upload:"action",wait:"other",waitFor:"other",waitForRequest:"other",waitForResponse:"other",waitForUrl:"other"},fe=new Set,Le=new Set;Object.entries(xr).forEach(([e,t])=>{t==="assertion"&&fe.add(e),t==="action"&&Le.add(e)});function Pr(e){let t=de(e),{spec:r}=t,o=Ts(r),n=Ns(r),s=ks({orderedNodes:o,spec:r,terminalNodes:n}),l=Es({flags:s,orderedNodes:o,terminalNodes:n,wf:t});return{flags:s,report:l}}function ks({orderedNodes:e,spec:t,terminalNodes:r}){let o=e.filter(d=>fe.has(d.type)).length,n=e.filter(d=>Le.has(d.type)).length,s=Object.keys(t.nodes).length,l=r.some(d=>fe.has(d.type));return[...Rs(o),...vs(l,o),...xs(n),...Ps(o,s)]}function Rs(e){return e===0?["NO_ASSERTIONS: The workflow has zero assertion nodes. It cannot verify its expectedOutcome."]:[]}function vs(e,t){return!e&&t>0?["NO_TERMINAL_ASSERTION: No terminal node (end of flow) is an assertion. The workflow may not verify the final state."]:[]}function xs(e){return e===0?["NO_INTERACTIONS: The workflow has zero interaction nodes (click, fill, select, etc.). It does not exercise any user action."]:[]}function Ps(e,t){if(e>0&&t>3){let r=e/t;if(r<.15){let o=String(Math.round(r*100));return[`LOW_ASSERTION_RATIO: Only ${String(e)}/${String(t)} nodes are assertions (${o}%). Consider adding more verification.`]}}return[]}function Ts(e){let t=new Set,r=[];function o(n){if(t.has(n))return;t.add(n);let s=e.nodes[n];s!=null&&(r.push(s),s.next!=null&&o(s.next),Tr(s).forEach(l=>{o(l)}))}return o(e.entryNode),r}function Tr(e){switch(e.type){case"if":return[...e.consequent,...e.alternate??[]];case"loop":case"forEach":return[...e.body];case"try":return[...e.body,...e.catch??[],...e.finally??[]];case"group":return[...e.nodes];case"parallel":return e.branches.flat();case"assertVisible":case"assertNotVisible":case"assertText":case"assertUrl":case"assertCount":case"assertValue":case"click":case"fill":case"select":case"check":case"uncheck":case"press":case"hover":case"goto":case"waitFor":case"screenshot":case"setViewport":case"wait":case"fail":case"setVariable":case"extractText":case"waitForUrl":case"waitForResponse":case"waitForRequest":case"upload":case"dblclick":case"drag":case"scrollIntoView":case"type":case"focus":case"assertAttribute":case"assertEnabled":case"assertDisabled":case"assertTitle":case"assertChecked":case"assertNotChecked":case"assertFocused":case"assertCookie":case"assertResponse":case"clear":case"rightClick":case"handleDialog":case"scroll":case"clipboard":case"setPermission":return[]}}function Ns(e){return Object.values(e.nodes).filter(t=>t.next!=null?!1:Tr(t).length===0)}function w(e){return e.by==="role"?e.name==null?`role=${e.role}`:`role=${e.role}[${e.name}]`:`${e.by}=${e.value}`}function $(e){return e.type==="static"?`"${e.value}"`:`{{${e.name}}}`}function Cs(e){return e.type==="static"?String(e.value):`{{${e.name}}}`}var Nr=new Set,Cr=new Set;Object.entries(xr).forEach(([e,t])=>{t==="controlFlow"&&Nr.add(e),t==="assertion"&&Cr.add(e)});function Ar(e){let t=`[${e.type}]`;return Nr.has(e.type)?Is(e,t):Cr.has(e.type)?$s(e,t):As(e,t)}function As(e,t){let r=e;switch(r.type){case"goto":return`${t} Navigate to ${$(r.url)}`;case"click":return`${t} click ${w(r.locator)}`;case"hover":return`${t} hover ${w(r.locator)}`;case"check":return`${t} check ${w(r.locator)}`;case"uncheck":return`${t} uncheck ${w(r.locator)}`;case"fill":return`${t} Fill ${w(r.locator)} with ${$(r.value)}`;case"select":return`${t} Select ${$(r.value)} in ${w(r.locator)}`;case"press":return`${t} Press "${r.key}"`;case"waitFor":{let o=r.state==null?"":` to be ${r.state}`;return`${t} Wait for ${w(r.locator)}${o}`}case"waitForUrl":return`${t} Wait for URL ${r.operator} ${$(r.expected)}`;case"waitForResponse":return`${t} Wait for response matching ${$(r.urlPattern)}`;case"waitForRequest":return`${t} Wait for request matching ${$(r.urlPattern)}`;case"extractText":return`${t} Extract text from ${w(r.locator)} into $${r.variable}`;case"screenshot":case"clipboard":return`${t} ${r.type}`;case"setViewport":return`${t} setViewport ${String(r.width)}x${String(r.height)}`;case"wait":return`${t} wait ${String(r.duration)}ms`;case"fail":return`${t} fail: ${r.message}`;case"setVariable":return`${t} setVariable ${r.variable}`;case"upload":return`${t} Upload files to ${w(r.locator)}`;case"dblclick":return`${t} Double-click ${w(r.locator)}`;case"drag":return`${t} Drag ${w(r.source)} to ${w(r.target)}`;case"scrollIntoView":return`${t} Scroll into view ${w(r.locator)}`;case"type":return`${t} Type ${$(r.value)} into ${w(r.locator)}`;case"focus":return`${t} Focus ${w(r.locator)}`;case"clear":return`${t} Clear ${w(r.locator)}`;case"rightClick":return`${t} Right-click ${w(r.locator)}`;case"handleDialog":return`${t} Handle dialog: ${r.action}`;case"scroll":return r.locator==null?`${t} Scroll page`:`${t} Scroll ${w(r.locator)}`;case"setPermission":return`${t} Set permission ${r.permission} to ${r.state}`}}function $s(e,t){let r=e;switch(r.type){case"assertVisible":return`${t} Assert visible: ${w(r.locator)}`;case"assertNotVisible":return`${t} Assert not visible: ${w(r.locator)}`;case"assertText":return`${t} Assert text of ${w(r.locator)} ${r.operator} ${$(r.expected)}`;case"assertUrl":return`${t} Assert URL ${r.operator} ${$(r.expected)}`;case"assertCount":return`${t} Assert count of ${w(r.locator)} ${r.operator} ${Cs(r.expected)}`;case"assertValue":return`${t} Assert value of ${w(r.locator)} ${r.operator} ${$(r.expected)}`;case"assertAttribute":return`${t} Assert attribute "${r.attribute}" of ${w(r.locator)} ${r.operator} ${$(r.expected)}`;case"assertEnabled":return`${t} Assert enabled: ${w(r.locator)}`;case"assertDisabled":return`${t} Assert disabled: ${w(r.locator)}`}}function Is(e,t){let r=e;switch(r.type){case"if":return`${t} if (condition)`;case"loop":return`${t} loop ${String(r.times)} times`;case"forEach":return`${t} forEach ${r.iteratorVar}`;case"parallel":return`${t} parallel (${String(r.branches.length)} branches)`;case"group":return`${t} group (${String(r.nodes.length)} nodes)`;case"try":return`${t} try/catch`}}function Es({flags:e,orderedNodes:t,terminalNodes:r,wf:o}){let n=t.filter(d=>fe.has(d.type)).length,s=t.filter(d=>Le.has(d.type)).length;return[js(o),Os(t),Vs(r),Fs({actionCount:s,assertionCount:n,orderedNodes:t,terminalNodes:r}),...Us(e),Ds].join(`
437
+ \`\`\``}import Le from"fs";import We from"path";import hs from"fs";import Cm from"path";import{z as k}from"zod";var De=k.object({edges:k.array(k.object({from:k.string(),to:k.string(),workflow:k.string()})),preconditions:k.record(k.string(),k.unknown()).optional(),resetPrecondition:k.string().optional(),states:k.record(k.string(),k.object({preconditions:k.array(k.string()),route:k.string()})),version:k.literal(3)});function br(e){let t=hs.readFileSync(e,"utf8"),r=JSON.parse(t);return De.parse(r)}function Sr(e){return{edges:e.edges,preconditions:e.preconditions??{},resetPrecondition:e.resetPrecondition,states:e.states,version:e.version}}function kr(e){let t=gs(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=De.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(h=>({message:h.message,path:h.path.join(".")})),valid:!1};let o=Sr(r.data),n=Ve(o);if(!n.success)return{errors:n.errors,valid:!1};let s=[...Fe(n.data)],l=We.join(We.dirname(e),"workflows"),d=Le.existsSync(l),i=new Set(n.data.edges.map(u=>u.workflow));return n.data.edges.forEach((u,h)=>{if(!d){s.push({message:`References workflow "${u.workflow}" but workflows directory does not exist`,path:`edges[${String(h)}].workflow`});return}let E=We.join(l,`${u.workflow}.json`);Le.existsSync(E)||s.push({message:`References workflow "${u.workflow}" but no file exists at workflows/${u.workflow}.json`,path:`edges[${String(h)}].workflow`})}),d&&Le.readdirSync(l).filter(u=>u.endsWith(".json")).forEach(u=>{let h=u.replace(/\.json$/,"");i.has(h)||s.push({message:`Workflow file "workflows/${u}" is not referenced by any edge`,path:`workflows/${u}`})}),{errors:s,valid:s.length===0}}function gs(e){try{return{data:br(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}import pe from"fs";import Rr from"path";import{z as A}from"zod";var ys=A.object({baseUrl:A.string().optional(),cloudBaseUrl:A.string(),preconditionApiPath:A.string().optional(),projectId:A.string()});function vr(e){let t=Rr.join(e,".ripplo","settings.json");if(!pe.existsSync(t))return{errors:[{message:".ripplo/settings.json not found",path:""}],valid:!1,warnings:[]};let r=Ss(t);if(!r.success)return{errors:[{message:r.error,path:"settings.json"}],valid:!1,warnings:[]};let o=ys.safeParse(r.data);if(!o.success)return{errors:o.error.issues.map(d=>({message:d.message,path:d.path.join(".")})),valid:!1,warnings:[]};let n=[],s=Rr.join(e,".ripplo","graph.json");return pe.existsSync(s)&&bs(s)&&(o.data.preconditionApiPath==null||o.data.preconditionApiPath.length===0)&&n.push("Graph has preconditions but preconditionApiPath is not set in settings.json. Preconditions will not be executed. Set preconditionApiPath to the URL where your app serves precondition endpoints (can be a relative path appended to baseUrl, or an absolute URL)."),{errors:[],valid:!0,warnings:n}}var ws=A.object({states:A.record(A.string(),A.object({preconditions:A.array(A.string())}))});function bs(e){try{let t=pe.readFileSync(e,"utf8"),r=JSON.parse(t),o=ws.safeParse(r);return o.success?Object.values(o.data.states).some(n=>n.preconditions.length>0):!1}catch{return!1}}function Ss(e){try{let t=pe.readFileSync(e,"utf8");return{data:JSON.parse(t),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}var xr={assertAttribute:"assertion",assertChecked:"assertion",assertCookie:"assertion",assertCount:"assertion",assertDisabled:"assertion",assertEnabled:"assertion",assertFocused:"assertion",assertNotChecked:"assertion",assertNotVisible:"assertion",assertResponse:"assertion",assertText:"assertion",assertTitle:"assertion",assertUrl:"assertion",assertValue:"assertion",assertVisible:"assertion",check:"action",clear:"action",click:"action",clipboard:"other",dblclick:"action",drag:"action",extractText:"other",fail:"other",fill:"action",focus:"action",forEach:"controlFlow",goto:"other",group:"controlFlow",handleDialog:"action",hover:"action",if:"controlFlow",loop:"controlFlow",parallel:"controlFlow",press:"action",rightClick:"action",screenshot:"other",scroll:"action",scrollIntoView:"action",select:"action",setPermission:"other",setVariable:"other",setViewport:"other",try:"controlFlow",type:"action",uncheck:"action",upload:"action",wait:"other",waitFor:"other",waitForRequest:"other",waitForResponse:"other",waitForUrl:"other"},fe=new Set,qe=new Set;Object.entries(xr).forEach(([e,t])=>{t==="assertion"&&fe.add(e),t==="action"&&qe.add(e)});function Pr(e){let t=de(e),{spec:r}=t,o=Ts(r),n=Ns(r),s=ks({orderedNodes:o,spec:r,terminalNodes:n}),l=Es({flags:s,orderedNodes:o,terminalNodes:n,wf:t});return{flags:s,report:l}}function ks({orderedNodes:e,spec:t,terminalNodes:r}){let o=e.filter(d=>fe.has(d.type)).length,n=e.filter(d=>qe.has(d.type)).length,s=Object.keys(t.nodes).length,l=r.some(d=>fe.has(d.type));return[...Rs(o),...vs(l,o),...xs(n),...Ps(o,s)]}function Rs(e){return e===0?["NO_ASSERTIONS: The workflow has zero assertion nodes. It cannot verify its expectedOutcome."]:[]}function vs(e,t){return!e&&t>0?["NO_TERMINAL_ASSERTION: No terminal node (end of flow) is an assertion. The workflow may not verify the final state."]:[]}function xs(e){return e===0?["NO_INTERACTIONS: The workflow has zero interaction nodes (click, fill, select, etc.). It does not exercise any user action."]:[]}function Ps(e,t){if(e>0&&t>3){let r=e/t;if(r<.15){let o=String(Math.round(r*100));return[`LOW_ASSERTION_RATIO: Only ${String(e)}/${String(t)} nodes are assertions (${o}%). Consider adding more verification.`]}}return[]}function Ts(e){let t=new Set,r=[];function o(n){if(t.has(n))return;t.add(n);let s=e.nodes[n];s!=null&&(r.push(s),s.next!=null&&o(s.next),Tr(s).forEach(l=>{o(l)}))}return o(e.entryNode),r}function Tr(e){switch(e.type){case"if":return[...e.consequent,...e.alternate??[]];case"loop":case"forEach":return[...e.body];case"try":return[...e.body,...e.catch??[],...e.finally??[]];case"group":return[...e.nodes];case"parallel":return e.branches.flat();case"assertVisible":case"assertNotVisible":case"assertText":case"assertUrl":case"assertCount":case"assertValue":case"click":case"fill":case"select":case"check":case"uncheck":case"press":case"hover":case"goto":case"waitFor":case"screenshot":case"setViewport":case"wait":case"fail":case"setVariable":case"extractText":case"waitForUrl":case"waitForResponse":case"waitForRequest":case"upload":case"dblclick":case"drag":case"scrollIntoView":case"type":case"focus":case"assertAttribute":case"assertEnabled":case"assertDisabled":case"assertTitle":case"assertChecked":case"assertNotChecked":case"assertFocused":case"assertCookie":case"assertResponse":case"clear":case"rightClick":case"handleDialog":case"scroll":case"clipboard":case"setPermission":return[]}}function Ns(e){return Object.values(e.nodes).filter(t=>t.next!=null?!1:Tr(t).length===0)}function w(e){return e.by==="role"?e.name==null?`role=${e.role}`:`role=${e.role}[${e.name}]`:`${e.by}=${e.value}`}function $(e){return e.type==="static"?`"${e.value}"`:`{{${e.name}}}`}function Cs(e){return e.type==="static"?String(e.value):`{{${e.name}}}`}var Nr=new Set,Cr=new Set;Object.entries(xr).forEach(([e,t])=>{t==="controlFlow"&&Nr.add(e),t==="assertion"&&Cr.add(e)});function Ar(e){let t=`[${e.type}]`;return Nr.has(e.type)?Is(e,t):Cr.has(e.type)?$s(e,t):As(e,t)}function As(e,t){let r=e;switch(r.type){case"goto":return`${t} Navigate to ${$(r.url)}`;case"click":return`${t} click ${w(r.locator)}`;case"hover":return`${t} hover ${w(r.locator)}`;case"check":return`${t} check ${w(r.locator)}`;case"uncheck":return`${t} uncheck ${w(r.locator)}`;case"fill":return`${t} Fill ${w(r.locator)} with ${$(r.value)}`;case"select":return`${t} Select ${$(r.value)} in ${w(r.locator)}`;case"press":return`${t} Press "${r.key}"`;case"waitFor":{let o=r.state==null?"":` to be ${r.state}`;return`${t} Wait for ${w(r.locator)}${o}`}case"waitForUrl":return`${t} Wait for URL ${r.operator} ${$(r.expected)}`;case"waitForResponse":return`${t} Wait for response matching ${$(r.urlPattern)}`;case"waitForRequest":return`${t} Wait for request matching ${$(r.urlPattern)}`;case"extractText":return`${t} Extract text from ${w(r.locator)} into $${r.variable}`;case"screenshot":case"clipboard":return`${t} ${r.type}`;case"setViewport":return`${t} setViewport ${String(r.width)}x${String(r.height)}`;case"wait":return`${t} wait ${String(r.duration)}ms`;case"fail":return`${t} fail: ${r.message}`;case"setVariable":return`${t} setVariable ${r.variable}`;case"upload":return`${t} Upload files to ${w(r.locator)}`;case"dblclick":return`${t} Double-click ${w(r.locator)}`;case"drag":return`${t} Drag ${w(r.source)} to ${w(r.target)}`;case"scrollIntoView":return`${t} Scroll into view ${w(r.locator)}`;case"type":return`${t} Type ${$(r.value)} into ${w(r.locator)}`;case"focus":return`${t} Focus ${w(r.locator)}`;case"clear":return`${t} Clear ${w(r.locator)}`;case"rightClick":return`${t} Right-click ${w(r.locator)}`;case"handleDialog":return`${t} Handle dialog: ${r.action}`;case"scroll":return r.locator==null?`${t} Scroll page`:`${t} Scroll ${w(r.locator)}`;case"setPermission":return`${t} Set permission ${r.permission} to ${r.state}`}}function $s(e,t){let r=e;switch(r.type){case"assertVisible":return`${t} Assert visible: ${w(r.locator)}`;case"assertNotVisible":return`${t} Assert not visible: ${w(r.locator)}`;case"assertText":return`${t} Assert text of ${w(r.locator)} ${r.operator} ${$(r.expected)}`;case"assertUrl":return`${t} Assert URL ${r.operator} ${$(r.expected)}`;case"assertCount":return`${t} Assert count of ${w(r.locator)} ${r.operator} ${Cs(r.expected)}`;case"assertValue":return`${t} Assert value of ${w(r.locator)} ${r.operator} ${$(r.expected)}`;case"assertAttribute":return`${t} Assert attribute "${r.attribute}" of ${w(r.locator)} ${r.operator} ${$(r.expected)}`;case"assertEnabled":return`${t} Assert enabled: ${w(r.locator)}`;case"assertDisabled":return`${t} Assert disabled: ${w(r.locator)}`}}function Is(e,t){let r=e;switch(r.type){case"if":return`${t} if (condition)`;case"loop":return`${t} loop ${String(r.times)} times`;case"forEach":return`${t} forEach ${r.iteratorVar}`;case"parallel":return`${t} parallel (${String(r.branches.length)} branches)`;case"group":return`${t} group (${String(r.nodes.length)} nodes)`;case"try":return`${t} try/catch`}}function Es({flags:e,orderedNodes:t,terminalNodes:r,wf:o}){let n=t.filter(d=>fe.has(d.type)).length,s=t.filter(d=>qe.has(d.type)).length;return[js(o),Os(t),Vs(r),Fs({actionCount:s,assertionCount:n,orderedNodes:t,terminalNodes:r}),...Us(e),Ds].join(`
438
438
  `)}function js(e){let t=["## Declared Intent",`**Name:** ${e.name}`,`**Description:** ${e.description}`,`**Expected Outcome:** ${e.expectedOutcome}`];if(e.additionalChecks.length>0){let r=e.additionalChecks.map(o=>`- ${o}`).join(`
439
439
  `);t.push(`**Additional Checks:**
440
440
  ${r}`)}return t.join(`
@@ -443,8 +443,8 @@ ${r}`)}return t.join(`
443
443
  `)}function Fs({actionCount:e,assertionCount:t,orderedNodes:r,terminalNodes:o}){return["","## Statistics",`- Total nodes: ${String(r.length)}`,`- Action nodes: ${String(e)}`,`- Assertion nodes: ${String(t)}`,`- Terminal nodes: ${String(o.length)}`].join(`
444
444
  `)}function Us(e){return e.length===0?[]:[["","## Flags",...e.map(r=>`- ${r}`)].join(`
445
445
  `)]}var Ds=["","## Review Instructions","Compare the **Declared Intent** against the **Workflow Steps** and answer these questions:","","1. Does the workflow actually test the **Expected Outcome**? Look at the assertions \u2014 do they verify that the expected outcome was achieved, or do they only check intermediate states?","2. Does the workflow exercise the flow described in **Description**? Are the right forms filled, buttons clicked, and pages navigated?","3. Are there assertions after the key state-changing action (e.g., after form submission, after deletion, after creation)? The final result should be verified, not just that a button was clicked.","4. If there are **Additional Checks**, does the workflow have assertions that cover them?","5. Could this workflow pass even if the feature is broken? For example, if it only asserts a page loaded but never checks the actual outcome.","","If any answer is NO, revise the workflow to address the gap. Then call `validate_workflow` and `review_workflow` again."].join(`
446
- `);function Ir(e){let t=Ws(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=ee.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(l=>({message:l.message,path:l.path.join(".")})),valid:!1};let o=ne(r.data.spec);if(!o.success)return{errors:o.errors,valid:!1};let n=qs(o.data);return n.length>0?{errors:n,valid:!1}:{errors:[],valid:!0}}function Ws(e){try{return{data:de(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}function qs(e){let t=new Set(Object.keys(e.nodes)),r=[];t.has(e.entryNode)||r.push({message:`entryNode "${e.entryNode}" does not exist in nodes`,path:"entryNode"}),Object.entries(e.nodes).forEach(([l,d])=>{$r(d).forEach(i=>{t.has(i.id)||r.push({message:`references non-existent node "${i.id}"`,path:`nodes.${l}.${i.field}`})})});let o=new Set,n=t.has(e.entryNode)?[e.entryNode]:[],s=0;for(;s<n.length;){let l=n[s];if(s+=1,l==null||o.has(l))continue;o.add(l);let d=e.nodes[l];d!=null&&$r(d).forEach(i=>{o.has(i.id)||n.push(i.id)})}return t.forEach(l=>{o.has(l)||r.push({message:"unreachable from entryNode",path:`nodes.${l}`})}),r}function $r(e){let t=[];typeof e.next=="string"&&t.push({field:"next",id:e.next}),["consequent","alternate","body","catch","finally","nodes"].forEach(n=>{let s=e[n];Array.isArray(s)&&s.forEach((l,d)=>{typeof l=="string"&&t.push({field:`${n}[${String(d)}]`,id:l})})});let o=e.branches;return Array.isArray(o)&&o.forEach((n,s)=>{Array.isArray(n)&&n.forEach((l,d)=>{typeof l=="string"&&t.push({field:`branches[${String(s)}][${String(d)}]`,id:l})})}),t}import Er from"fs";import Ls from"path";function jr(e){let t=_s(),r=[`Create a Ripplo workflow spec for: ${e}`,"","1. Call get_spec_documentation to learn the workflow spec format.","2. Call get_graph_documentation to learn the state graph and precondition system."];return t?r.push("3. This is the first workflow \u2014 build a minimal graph:"," a. Create a graph with exactly 2 states (start state and end state) and 1 edge."," b. Define preconditions for whatever the source state needs (e.g. `auth:user` for logged-in pages, `data:project` for existing entities)."," c. Set `preconditionApiPath` in `.ripplo/settings.json`."," d. Write to `.ripplo/graph.json`."," e. Call validate_graph. Fix any errors."," f. Call validate_settings. Fix any warnings.","4. Read the relevant source files (routes, components, forms) to find real URLs, form fields, and button labels.","5. Set up precondition endpoints (if the source state has preconditions):"," a. Check if precondition endpoints already exist at the preconditionApiPath. If so, skip."," b. Implement the three endpoints (check, execute, teardown) \u2014 see get_precondition_schema for the API contract."," c. Read the app's auth system to understand how to create sessions programmatically (direct DB/ORM calls, not UI login)."," d. Read the data model to understand how to seed required entities."," e. Each executor should create ephemeral test data with a `ripplo-test-` prefix for easy cleanup."," f. Delegate this to a sub-agent \u2014 give it the precondition names, auth architecture, and data model summary."," g. PARALLEL SAFETY: Every execute handler must generate unique names/emails using random suffixes (e.g., `ripplo-test-${crypto.randomUUID().slice(0,8)}`). Never hardcode entity names \u2014 parallel runs will hit unique constraint violations. Return created entity IDs in the `data` response. Teardown must only delete that run's data (use session cookies to identify the user, not prefix-based bulk deletion).","6. Create .ripplo/workflows/<slug>.json with a complete spec using real locators from the code.","7. Call validate_workflow on the file. Fix any errors.","8. Call review_workflow on the file. Read the report and fix any gaps \u2014 ensure assertions verify the expectedOutcome, not just intermediate states. Re-validate after changes.","9. Call `run` with the workflow slug to run it. Report the result and include the run URL."):r.push("3. Read .ripplo/graph.json to understand existing states, edges, and preconditions.","4. Read the relevant source files (routes, components, forms) to find real URLs, form fields, and button labels.","5. Determine what preconditions this flow needs (e.g. auth, test data). If the required preconditions don't exist in graph.json:"," a. Add them to the graph's preconditions object with appropriate depends chains."," b. Add or update the state that this workflow transitions from, ensuring it lists the required preconditions."," c. Add the edge connecting the source state to the target state with the workflow slug."," d. Call validate_graph on .ripplo/graph.json. Fix any errors."," e. Call validate_settings to verify preconditionApiPath is set. Fix any warnings."," f. Check if precondition API endpoints exist at the preconditionApiPath. If not, create them following the pattern in get_precondition_schema."," g. Run ripplo generate-types to regenerate the typed schemas after adding preconditions."," h. Verify precondition executors are parallel-safe: unique data per run (random suffixes), IDs returned via `data`, teardown scoped to that run's user (not global prefix deletion).","6. Create .ripplo/workflows/<slug>.json with a complete spec using real locators from the code.","7. Call validate_workflow on the file. Fix any errors.","8. Call review_workflow on the file. Read the report and fix any gaps \u2014 ensure assertions verify the expectedOutcome, not just intermediate states. Re-validate after changes.","9. Call `run` with the workflow slug to run it. Report the result and include the run URL."),r.join(`
447
- `)}function _s(){let e=Ls.join(process.cwd(),".ripplo","workflows");return Er.existsSync(e)?!Er.readdirSync(e).some(r=>r.endsWith(".json")):!0}import Vr from"fs";import Bs from"path";var K="## Iteration \u2014 run until it works\n\nYou MUST keep iterating until the workflow passes AND accomplishes the expected outcome:\n\n1. Call `run` with the workflow slug(s). Pass multiple slugs to run workflows in parallel \u2014 always preferred over calling `run` multiple times. If any fail, read the debug artifacts from `.ripplo/debug/<runId>/`.\n2. Read debug artifacts from `.ripplo/debug/<runId>/` for deeper investigation:\n - Grep `console.log` for errors/warnings around the failure timestamp.\n - Check `network.jsonl` for failed requests, unexpected status codes, or missing API responses.\n - Read `steps/<failedIndex>/dom.html` to inspect the actual DOM at the failing step.\n - Read `steps/<failedIndex>/accessibility-tree.txt` to understand page structure and find correct locators.\n - Read `steps/<failedIndex>/storage.json` to check auth state, session data, or app state.\n - Compare with `steps/<failedIndex - 1>/` to see what changed between the previous step and the failure.\n - Check `page-errors.log` for uncaught JavaScript exceptions.\n3. Identify the root cause \u2014 don't just retry. Common root causes:\n - Wrong locator (check the DOM and accessibility tree from the debug artifacts)\n - Missing waitFor before an element appears\n - Wrong URL path or missing query params\n - Missing or misconfigured precondition\n - Race condition (action fires before page transition completes)\n - Parallel-safety violation (unique constraint error, \"not authorized\" from session conflict) \u2014 precondition creates hardcoded/shared data instead of unique-per-run data, or teardown globally deletes all test data instead of only that run's data. Fix the precondition executor.\n - A bug in the user's application code (not the workflow)\n4. If the root cause is a bug in the application (not the workflow spec), report it to the user with the evidence (failed step, expected vs actual, relevant source code, debug artifact excerpts) and ask them to fix it. Do NOT work around app bugs by weakening assertions.\n5. Otherwise, fix the root cause in the workflow spec.\n6. Call `validate_workflow`, then `review_workflow`, then `run` again.\n7. When the run passes, call `review_workflow` one final time. Verify the passing run actually tests the expectedOutcome \u2014 a workflow that passes but doesn't assert the outcome is not done.\n8. If the same failure repeats after 3 targeted fixes, report the root cause to the user and ask for help. Do NOT silently give up.",Or="If the run fails, iterate:\n- Read debug artifacts from `.ripplo/debug/<runId>/` \u2014 check `console.log`, `network.jsonl`, and `steps/<failedIndex>/dom.html` for root cause clues.\n- **Bad locator**: element not found or wrong element \u2014 check `dom.html` and `accessibility-tree.txt`, re-read the component source, fix the locator in the spec, re-run.\n- **Bad assertion**: text or visibility mismatch \u2014 fix the expected value, re-run.\n- **Precondition issue**: state wasn't set up correctly \u2014 check `storage.json` for auth/session state, fix the precondition API, re-run.\n- **Parallel collision**: unique constraint error or auth failure when multiple runs execute simultaneously \u2014 precondition creates shared data or teardown nukes all test data globally. Fix executor to use unique names per run and scope teardown to that run's user/session.\n- **App-level bug**: the application itself is broken \u2014 note the failure as an app issue. Do not keep iterating on test-side fixes.\n- A workflow is done when its run **passes** or when the failure is confirmed as **app-level**.";function Fr(e){return`# Debug a Failing Workflow
446
+ `);function Ir(e){let t=Ls(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=ee.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(l=>({message:l.message,path:l.path.join(".")})),valid:!1};let o=ne(r.data.spec);if(!o.success)return{errors:o.errors,valid:!1};let n=Ws(o.data);return n.length>0?{errors:n,valid:!1}:{errors:[],valid:!0}}function Ls(e){try{return{data:de(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}function Ws(e){let t=new Set(Object.keys(e.nodes)),r=[];t.has(e.entryNode)||r.push({message:`entryNode "${e.entryNode}" does not exist in nodes`,path:"entryNode"}),Object.entries(e.nodes).forEach(([l,d])=>{$r(d).forEach(i=>{t.has(i.id)||r.push({message:`references non-existent node "${i.id}"`,path:`nodes.${l}.${i.field}`})})});let o=new Set,n=t.has(e.entryNode)?[e.entryNode]:[],s=0;for(;s<n.length;){let l=n[s];if(s+=1,l==null||o.has(l))continue;o.add(l);let d=e.nodes[l];d!=null&&$r(d).forEach(i=>{o.has(i.id)||n.push(i.id)})}return t.forEach(l=>{o.has(l)||r.push({message:"unreachable from entryNode",path:`nodes.${l}`})}),r}function $r(e){let t=[];typeof e.next=="string"&&t.push({field:"next",id:e.next}),["consequent","alternate","body","catch","finally","nodes"].forEach(n=>{let s=e[n];Array.isArray(s)&&s.forEach((l,d)=>{typeof l=="string"&&t.push({field:`${n}[${String(d)}]`,id:l})})});let o=e.branches;return Array.isArray(o)&&o.forEach((n,s)=>{Array.isArray(n)&&n.forEach((l,d)=>{typeof l=="string"&&t.push({field:`branches[${String(s)}][${String(d)}]`,id:l})})}),t}import Er from"fs";import qs from"path";function jr(e){let t=_s(),r=[`Create a Ripplo workflow spec for: ${e}`,"","1. Call get_spec_documentation to learn the workflow spec format.","2. Call get_graph_documentation to learn the state graph and precondition system."];return t?r.push("3. This is the first workflow \u2014 build a minimal graph:"," a. Create a graph with exactly 2 states (start state and end state) and 1 edge."," b. Define preconditions for whatever the source state needs (e.g. `auth:user` for logged-in pages, `data:project` for existing entities)."," c. Set `preconditionApiPath` in `.ripplo/settings.json`."," d. Write to `.ripplo/graph.json`."," e. Call validate_graph. Fix any errors."," f. Call validate_settings. Fix any warnings.","4. Read the relevant source files (routes, components, forms) to find real URLs, form fields, and button labels.","5. Set up precondition endpoints (if the source state has preconditions):"," a. Check if precondition endpoints already exist at the preconditionApiPath. If so, skip."," b. Implement the three endpoints (check, execute, teardown) \u2014 see get_precondition_schema for the API contract."," c. Read the app's auth system to understand how to create sessions programmatically (direct DB/ORM calls, not UI login)."," d. Read the data model to understand how to seed required entities."," e. Each executor should create ephemeral test data with a `ripplo-test-` prefix for easy cleanup."," f. Delegate this to a sub-agent \u2014 give it the precondition names, auth architecture, and data model summary."," g. PARALLEL SAFETY: Every execute handler must generate unique names/emails using random suffixes (e.g., `ripplo-test-${crypto.randomUUID().slice(0,8)}`). Never hardcode entity names \u2014 parallel runs will hit unique constraint violations. Return created entity IDs in the `data` response. Teardown must only delete that run's data (use session cookies to identify the user, not prefix-based bulk deletion).","6. Create .ripplo/workflows/<slug>.json with a complete spec using real locators from the code.","7. Call validate_workflow on the file. Fix any errors.","8. Call review_workflow on the file. Read the report and fix any gaps \u2014 ensure assertions verify the expectedOutcome, not just intermediate states. Re-validate after changes.","9. Call `run` with the workflow slug to run it. Report the result and include the run URL."):r.push("3. Read .ripplo/graph.json to understand existing states, edges, and preconditions.","4. Read the relevant source files (routes, components, forms) to find real URLs, form fields, and button labels.","5. Determine what preconditions this flow needs (e.g. auth, test data). If the required preconditions don't exist in graph.json:"," a. Add them to the graph's preconditions object with appropriate depends chains."," b. Add or update the state that this workflow transitions from, ensuring it lists the required preconditions."," c. Add the edge connecting the source state to the target state with the workflow slug."," d. Call validate_graph on .ripplo/graph.json. Fix any errors."," e. Call validate_settings to verify preconditionApiPath is set. Fix any warnings."," f. Check if precondition API endpoints exist at the preconditionApiPath. If not, create them following the pattern in get_precondition_schema."," g. Run ripplo generate-types to regenerate the typed schemas after adding preconditions."," h. Verify precondition executors are parallel-safe: unique data per run (random suffixes), IDs returned via `data`, teardown scoped to that run's user (not global prefix deletion).","6. Create .ripplo/workflows/<slug>.json with a complete spec using real locators from the code.","7. Call validate_workflow on the file. Fix any errors.","8. Call review_workflow on the file. Read the report and fix any gaps \u2014 ensure assertions verify the expectedOutcome, not just intermediate states. Re-validate after changes.","9. Call `run` with the workflow slug to run it. Report the result and include the run URL."),r.join(`
447
+ `)}function _s(){let e=qs.join(process.cwd(),".ripplo","workflows");return Er.existsSync(e)?!Er.readdirSync(e).some(r=>r.endsWith(".json")):!0}import Vr from"fs";import Bs from"path";var Y="## Iteration \u2014 run until it works\n\nYou MUST keep iterating until the workflow passes AND accomplishes the expected outcome:\n\n1. Call `run` with the workflow slug(s). Pass multiple slugs to run workflows in parallel \u2014 always preferred over calling `run` multiple times. If any fail, read the debug artifacts from `.ripplo/debug/<runId>/`.\n2. Read debug artifacts from `.ripplo/debug/<runId>/` for deeper investigation:\n - Grep `console.log` for errors/warnings around the failure timestamp.\n - Check `network.jsonl` for failed requests, unexpected status codes, or missing API responses.\n - Read `steps/<failedIndex>/dom.html` to inspect the actual DOM at the failing step.\n - Read `steps/<failedIndex>/accessibility-tree.txt` to understand page structure and find correct locators.\n - Read `steps/<failedIndex>/storage.json` to check auth state, session data, or app state.\n - Compare with `steps/<failedIndex - 1>/` to see what changed between the previous step and the failure.\n - Check `page-errors.log` for uncaught JavaScript exceptions.\n3. Identify the root cause \u2014 don't just retry. Common root causes:\n - Wrong locator (check the DOM and accessibility tree from the debug artifacts)\n - Missing waitFor before an element appears\n - Wrong URL path or missing query params\n - Missing or misconfigured precondition\n - Race condition (action fires before page transition completes)\n - Parallel-safety violation (unique constraint error, \"not authorized\" from session conflict) \u2014 precondition creates hardcoded/shared data instead of unique-per-run data, or teardown globally deletes all test data instead of only that run's data. Fix the precondition executor.\n - A bug in the user's application code (not the workflow)\n4. If the root cause is a bug in the application (not the workflow spec), report it to the user with the evidence (failed step, expected vs actual, relevant source code, debug artifact excerpts) and ask them to fix it. Do NOT work around app bugs by weakening assertions.\n5. Otherwise, fix the root cause in the workflow spec.\n6. Call `validate_workflow`, then `review_workflow`, then `run` again.\n7. When the run passes, call `review_workflow` one final time. Verify the passing run actually tests the expectedOutcome \u2014 a workflow that passes but doesn't assert the outcome is not done.\n8. If the same failure repeats after 3 targeted fixes, report the root cause to the user and ask for help. Do NOT silently give up.",Or="If the run fails, iterate:\n- Read debug artifacts from `.ripplo/debug/<runId>/` \u2014 check `console.log`, `network.jsonl`, and `steps/<failedIndex>/dom.html` for root cause clues.\n- **Bad locator**: element not found or wrong element \u2014 check `dom.html` and `accessibility-tree.txt`, re-read the component source, fix the locator in the spec, re-run.\n- **Bad assertion**: text or visibility mismatch \u2014 fix the expected value, re-run.\n- **Precondition issue**: state wasn't set up correctly \u2014 check `storage.json` for auth/session state, fix the precondition API, re-run.\n- **Parallel collision**: unique constraint error or auth failure when multiple runs execute simultaneously \u2014 precondition creates shared data or teardown nukes all test data globally. Fix executor to use unique names per run and scope teardown to that run's user/session.\n- **App-level bug**: the application itself is broken \u2014 note the failure as an app issue. Do not keep iterating on test-side fixes.\n- A workflow is done when its run **passes** or when the failure is confirmed as **app-level**.";function Fr(e){return`# Debug a Failing Workflow
448
448
 
449
449
  ${Ms(e)}
450
450
 
@@ -493,7 +493,7 @@ Categorize the root cause:
493
493
 
494
494
  ## Step 4: Fix and re-run
495
495
 
496
- ${K}
496
+ ${Y}
497
497
 
498
498
  ## Important rules
499
499
 
@@ -507,7 +507,7 @@ Find it in \`.ripplo/workflows/\`. If the name doesn't match exactly, look for a
507
507
  ${t.map(r=>`- ${r}`).join(`
508
508
  `)}
509
509
 
510
- Ask the user which workflow to debug, or pick the one most likely to be failing.`}function Gs(){let e=Bs.join(process.cwd(),".ripplo","workflows");return Vr.existsSync(e)?Vr.readdirSync(e).filter(t=>t.endsWith(".json")).map(t=>t.replace(/\.json$/,"")):[]}function Ur(e){let t=e.baseUrl==null?"IMPORTANT: No baseUrl was provided. Before writing any files, find the dev server URL from the project's config (package.json scripts, vite.config.ts, next.config.js, etc.) and use it as baseUrl in every graph and workflow file.":`The application base URL is: ${e.baseUrl}`;return e.scope!=null?zs({baseUrlInstruction:t,scope:e.scope}):[Js(t),Ks(),Ys(),Qs(),ei(),Xs(),Zs(),ti()].join(`
510
+ Ask the user which workflow to debug, or pick the one most likely to be failing.`}function Gs(){let e=Bs.join(process.cwd(),".ripplo","workflows");return Vr.existsSync(e)?Vr.readdirSync(e).filter(t=>t.endsWith(".json")).map(t=>t.replace(/\.json$/,"")):[]}function Ur(e){let t=e.baseUrl==null?"IMPORTANT: No baseUrl was provided. Before writing any files, find the dev server URL from the project's config (package.json scripts, vite.config.ts, next.config.js, etc.) and use it as baseUrl in every graph and workflow file.":`The application base URL is: ${e.baseUrl}`;return e.scope!=null?zs({baseUrlInstruction:t,scope:e.scope}):[Js(t),Ys(),Ks(),Qs(),ei(),Xs(),Zs(),ti()].join(`
511
511
 
512
512
  `)}function zs(e){let t=Hs(e.scope);return`# Ripplo Explore (Scoped to Changes)
513
513
 
@@ -566,7 +566,7 @@ For each confirmed flow, delegate to a sub-agent:
566
566
  6. Call \`review_workflow\` on the file. Read the report and fix any gaps \u2014 ensure assertions verify the expectedOutcome, not just intermediate states. Re-validate after changes.
567
567
  7. Call \`run\` with the workflow slug. Report the result and include the run URL.
568
568
 
569
- ${K}
569
+ ${Y}
570
570
 
571
571
  ## Step 5: Report summary
572
572
 
@@ -618,7 +618,7 @@ This is a large task. **Aggressively delegate to sub-agents** to keep the main c
618
618
  - **Workflow generation**: Always use sub-agents for writing workflow specs. Launch them in parallel batches.
619
619
  - **Precondition API implementation**: Delegate endpoint implementation entirely to a sub-agent. Give it the graph's precondition names, the auth/data architecture summary from exploration, and let it handle reading source code, writing handlers, and wiring up the codegen. Do NOT read app source code for endpoint implementation in the main context.
620
620
  - **General rule**: If a task involves reading more than 2-3 files or writing more than one file, it should be a sub-agent.
621
- - **Recursive sub-agents**: Sub-agents should spawn their own sub-agents when they hit something that needs deeper exploration. An exploration agent that discovers a complex auth middleware layer should spawn a deeper agent to trace it fully. A workflow generation agent that needs to understand a component tree should spawn an agent to map it. There is no depth limit \u2014 go as deep as needed to get complete, accurate results. Each level returns a structured summary to its parent.`}function Ks(){return`## Understanding the application
621
+ - **Recursive sub-agents**: Sub-agents should spawn their own sub-agents when they hit something that needs deeper exploration. An exploration agent that discovers a complex auth middleware layer should spawn a deeper agent to trace it fully. A workflow generation agent that needs to understand a component tree should spawn an agent to map it. There is no depth limit \u2014 go as deep as needed to get complete, accurate results. Each level returns a structured summary to its parent.`}function Ys(){return`## Understanding the application
622
622
 
623
623
  Explore the codebase deeply enough to discover every testable surface. **Use sub-agents for all exploration** \u2014 the main context should not read source files directly during discovery. Launch exploration agents in parallel where possible, and have each return a structured summary (routes found, components found, entities found, etc.) rather than raw file contents.
624
624
 
@@ -692,7 +692,7 @@ Map every write operation to its UI trigger:
692
692
  - Which UI component triggers each mutation
693
693
  - Form actions (if using framework form actions)
694
694
  - Optimistic updates and their rollback behavior
695
- - Which mutations are restricted to which roles`}function Ys(){return`## Extracting edges
695
+ - Which mutations are restricted to which roles`}function Ks(){return`## Extracting edges
696
696
 
697
697
  From everything discovered, extract EVERY meaningful user journey as an edge.
698
698
 
@@ -864,5 +864,5 @@ ${r.warnings.join(`
864
864
  `)}`:"";return v(`Valid${n}`)}let o=r.errors.map(n=>`${n.path}: ${n.message}`);return v(`Invalid:
865
865
  ${o.join(`
866
866
  `)}`)})}function ai(e){e.registerPrompt("explore",{argsSchema:{baseUrl:T.string().optional().describe("Base URL of the application (e.g. http://localhost:3000)"),scope:T.string().optional().describe("Narrow exploration to code changes. Pass a PR number, branch name, 'changes' for local diff, or a description. Omit for full exhaustive exploration.")},description:"Guided flow to crawl a codebase, build a state graph, and generate workflow specs. Optionally scoped to recent code changes."},t=>({messages:[{content:{text:Ur({baseUrl:t.baseUrl,scope:t.scope}),type:"text"},role:"user"}]}))}function si(e){e.registerTool("run",{description:"Run one or more workflows in parallel. Creates runs on the server and waits for the local dev mode agent to execute them. Multiple workflows execute concurrently. Requires `ripplo dev` to be running. Workflows are defined as JSON files in `.ripplo/workflows/` \u2014 the slug is the filename without `.json`. Results include run URLs and debug artifact paths.",inputSchema:{slugs:T.array(T.string()).describe("Workflow slugs to run (supports 1 or many, all run in parallel)")}},t=>lr(async r=>{try{let o=await gr({config:r,slugs:t.slugs});return v(o.summary)}catch(o){let n=o instanceof Error?o.message:String(o);return v(`ERROR: ${n}`)}}))}function ii(e){e.registerPrompt("run",{argsSchema:{workflow:T.string().optional().describe("Workflow name to run (optional)")},description:"Run ripplo user flows locally using Playwright"},t=>({messages:[{content:{text:t.workflow==null?["Run all workflows in parallel:","","1. Read the `.ripplo/workflows/` directory to find all workflow JSON files. The slug is the filename without `.json`.","2. Call `run` with all slugs at once to execute them in parallel.","3. Report an aggregate summary: total run, passed, failed, and failure details.","","Do not ask any questions. Do not read workflow file contents."].join(`
867
- `):`Read \`.ripplo/workflows/\` to find the workflow named "${t.workflow}" (check the "name" field in each JSON file), then call the \`run\` tool with its slug (filename without .json). Do not ask any questions or read any files.`,type:"text"},role:"user"}]})),e.registerPrompt("create",{argsSchema:{flow:T.string().optional().describe("User flow to test (e.g. login, checkout)")},description:"Create a new Ripplo workflow spec by analyzing the codebase"},t=>({messages:[{content:{text:[jr(t.flow??"the most important user flow"),"",K].join(`
867
+ `):`Read \`.ripplo/workflows/\` to find the workflow named "${t.workflow}" (check the "name" field in each JSON file), then call the \`run\` tool with its slug (filename without .json). Do not ask any questions or read any files.`,type:"text"},role:"user"}]})),e.registerPrompt("create",{argsSchema:{flow:T.string().optional().describe("User flow to test (e.g. login, checkout)")},description:"Create a new Ripplo workflow spec by analyzing the codebase"},t=>({messages:[{content:{text:[jr(t.flow??"the most important user flow"),"",Y].join(`
868
868
  `),type:"text"},role:"user"}]})),e.registerPrompt("debug",{argsSchema:{workflow:T.string().optional().describe("Workflow name or slug to debug (lists available if omitted)")},description:"Debug a failing workflow \u2014 identifies root cause using browser logs, DOM, network, and storage snapshots from .ripplo/debug/"},t=>({messages:[{content:{text:Fr(t.workflow),type:"text"},role:"user"}]}))}async function ci(){let e=Dr(),t=new li;await e.connect(t)}ci().catch(e=>{process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ripplo",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "CLI for Ripplo — AI-powered end-to-end testing",
5
5
  "type": "module",
6
6
  "homepage": "https://ripplo.ai",
@@ -43,8 +43,8 @@
43
43
  "typescript": "^5.9.3",
44
44
  "@ripplo/eslint-config": "0.0.0",
45
45
  "@ripplo/runtime": "^0.0.0",
46
- "@ripplo/spec": "^0.0.0",
47
- "@ripplo/graph": "^0.0.0"
46
+ "@ripplo/graph": "^0.0.0",
47
+ "@ripplo/spec": "^0.0.0"
48
48
  },
49
49
  "scripts": {
50
50
  "dev": "tsx src/index.ts",