ripplo 0.0.1 → 0.0.2
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/index.js +53 -53
- package/dist/mcp/index.js +37 -37
- package/package.json +3 -3
package/dist/mcp/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{StdioServerTransport as
|
|
3
|
-
`):`${e.status}: ${e.title}`;return{specNode:e.specNode,stepResult:n,toolOutput:o}}var mr=ue.object({expected:ue.string().describe("The expected text content"),selector:ue.string().describe("CSS selector for the element to check")}),Oe=u({description:"Assert that an element's text content matches the expected value",name:"assert_text",schema:mr,execute:async(e,t)=>{let r=performance.now(),n=await e.page.locator(t.selector).textContent({timeout:5e3}).catch(()=>null),o=n!=null&&n.includes(t.expected),s=o?"passed":"failed",l=[{description:`Text contains "${t.expected}"`,detail:`Actual: "${n??"(not found)"}"`,status:s}],c=`agent-step-${String(e.stepIndex)}`,i={expected:{type:"static",value:t.expected},id:c,locator:{by:"css",value:t.selector},operator:"contains",type:"assertText"};return h({assertions:l,detail:o?`Text matches: "${t.expected}"`:`Expected "${t.expected}", got "${n??"(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 Fe}from"zod";var hr=Fe.object({selector:Fe.string().describe("CSS selector for the element to check")}),Ue=u({description:"Assert that an element matching the selector is visible on the page",name:"assert_visible",schema:hr,execute:async(e,t)=>{let r=performance.now(),n=await e.page.locator(t.selector).isVisible().catch(()=>!1),o=n?"passed":"failed",s=[{description:`Element ${t.selector} is visible`,detail:n?"Element is visible":"Element is not visible",status:o}],c={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertVisible"};return h({assertions:s,detail:n?"Element is visible":"Element is not visible",duration:performance.now()-r,nodeType:"assertVisible",page:e.page,runStartTime:e.runStartTime,specNode:c,status:o,stepIndex:e.stepIndex,title:`Assert visible: ${t.selector}`})}});import{z as De}from"zod";var gr=De.object({selector:De.string().describe("CSS selector for the checkbox to check")}),We=u({description:"Check a checkbox matching the CSS selector",name:"check",schema:gr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).check({timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"check"};return h({assertions:[],detail:`Checked ${t.selector}`,duration:performance.now()-r,nodeType:"check",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Check ${t.selector}`})}});import{z as qe}from"zod";var yr=qe.object({selector:qe.string().describe("CSS selector for the element to click")}),Le=u({description:"Click an element matching the CSS selector",name:"click",schema:yr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"click"};return h({assertions:[],detail:`Clicked ${t.selector}`,duration:performance.now()-r,nodeType:"click",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Click ${t.selector}`})}});import{z as X}from"zod";var _e=X.object({summary:X.string().describe("
|
|
2
|
+
import{StdioServerTransport as Ka}from"@modelcontextprotocol/sdk/server/stdio.js";import{McpServer as Ma}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as T}from"zod";import Zi from"@anthropic-ai/sdk";import{z as ue}from"zod";import{z as fr}from"zod";async function H({page:e,runStartTime:t,targets:r}){let n=e.viewportSize();if(n==null)throw new Error("Page has no viewport set");let[o,s]=await Promise.all([e.screenshot({type:"png"}),ur(r)]),l=o.toString("base64");return{annotations:s,screenshotBase64:l,snapshotTimestamp:Math.round(performance.now()-t),url:e.url(),viewportHeight:n.height,viewportWidth:n.width}}function dr(e){return"locator"in e}async function ur(e){return(await Promise.all(e.map(r=>pr(r)))).filter(r=>r!=null)}async function pr(e){if(!dr(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 u({description:e,execute:t,name:r,schema:n}){let o=fr.toJSONSchema(n,{target:"draft-2020-12"});return{anthropicTool:{description:e,input_schema:{...o,type:"object"},name:r},name:r,async execute(s,l){let c=n.parse(l);return{...await Promise.resolve(t(s,c)),kind:"action"}}}}async function h(e){let t=await H({page:e.page,runStartTime:e.runStartTime,targets:[]}),r=[...e.assertions],n={annotations:t.annotations,assertions:r,detail:e.detail,duration:Math.round(e.duration),nodeId:`agent-step-${String(e.stepIndex)}`,nodeType:e.nodeType,screenshotBase64:t.screenshotBase64,snapshotTimestamp:t.snapshotTimestamp,status:e.status,stepIndex:e.stepIndex,title:e.title,url:t.url,viewportHeight:t.viewportHeight,viewportWidth:t.viewportWidth},o=r.length>0?r.map(s=>`${s.status}: ${s.description} \u2014 ${s.detail??""}`).join(`
|
|
3
|
+
`):`${e.status}: ${e.title}`;return{specNode:e.specNode,stepResult:n,toolOutput:o}}var mr=ue.object({expected:ue.string().describe("The expected text content"),selector:ue.string().describe("CSS selector for the element to check")}),Oe=u({description:"Assert that an element's text content matches the expected value",name:"assert_text",schema:mr,execute:async(e,t)=>{let r=performance.now(),n=await e.page.locator(t.selector).textContent({timeout:5e3}).catch(()=>null),o=n!=null&&n.includes(t.expected),s=o?"passed":"failed",l=[{description:`Text contains "${t.expected}"`,detail:`Actual: "${n??"(not found)"}"`,status:s}],c=`agent-step-${String(e.stepIndex)}`,i={expected:{type:"static",value:t.expected},id:c,locator:{by:"css",value:t.selector},operator:"contains",type:"assertText"};return h({assertions:l,detail:o?`Text matches: "${t.expected}"`:`Expected "${t.expected}", got "${n??"(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 Fe}from"zod";var hr=Fe.object({selector:Fe.string().describe("CSS selector for the element to check")}),Ue=u({description:"Assert that an element matching the selector is visible on the page",name:"assert_visible",schema:hr,execute:async(e,t)=>{let r=performance.now(),n=await e.page.locator(t.selector).isVisible().catch(()=>!1),o=n?"passed":"failed",s=[{description:`Element ${t.selector} is visible`,detail:n?"Element is visible":"Element is not visible",status:o}],c={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertVisible"};return h({assertions:s,detail:n?"Element is visible":"Element is not visible",duration:performance.now()-r,nodeType:"assertVisible",page:e.page,runStartTime:e.runStartTime,specNode:c,status:o,stepIndex:e.stepIndex,title:`Assert visible: ${t.selector}`})}});import{z as De}from"zod";var gr=De.object({selector:De.string().describe("CSS selector for the checkbox to check")}),We=u({description:"Check a checkbox matching the CSS selector",name:"check",schema:gr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).check({timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"check"};return h({assertions:[],detail:`Checked ${t.selector}`,duration:performance.now()-r,nodeType:"check",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Check ${t.selector}`})}});import{z as qe}from"zod";var yr=qe.object({selector:qe.string().describe("CSS selector for the element to click")}),Le=u({description:"Click an element matching the CSS selector",name:"click",schema:yr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"click"};return h({assertions:[],detail:`Clicked ${t.selector}`,duration:performance.now()-r,nodeType:"click",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Click ${t.selector}`})}});import{z as X}from"zod";var _e=X.object({summary:X.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:X.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.")}),Be={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:{...X.toJSONSchema(_e,{target:"draft-2020-12"}),type:"object"},name:"complete_test"},name:"complete_test",execute(e,t){let r=_e.parse(t),n={kind:"verdict",summary:r.summary,toolOutput:`Verdict: ${r.verdict}
|
|
4
4
|
${r.summary}`,verdict:r.verdict};return Promise.resolve(n)}};import{z as Me}from"zod";var wr=Me.object({selector:Me.string().describe("CSS selector for the element to extract text from")}),Ge=u({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:wr,execute:async(e,t)=>{let r=performance.now(),o=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 h({assertions:[],detail:`Extracted text: "${o}"`,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 pe}from"zod";var br=pe.object({selector:pe.string().describe("CSS selector for the input element"),value:pe.string().describe("The text to type into the element")}),ze=u({description:"Clear and type text into an input element matching the CSS selector",name:"fill",schema:br,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).fill(t.value,{timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"fill",value:{type:"static",value:t.value}};return h({assertions:[],detail:`Filled ${t.selector} with "${t.value}"`,duration:performance.now()-r,nodeType:"fill",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Fill ${t.selector}`})}});import{z as He}from"zod";var Sr=He.object({level:He.string().describe("Filter by log level: 'error', 'warning', 'log', 'info', 'debug', or 'all'").default("all")}),Je=u({description:"Get console log messages from the browser. Optionally filter by level (error, warning, log, info, debug, all).",name:"get_console_logs",schema:Sr,execute:(e,t)=>{let r=t.level==="all"?e.monitor.consoleEntries:e.monitor.consoleEntries.filter(o=>o.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 n=r.map(o=>Rr(o)).join(`
|
|
5
5
|
`);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(r.length)} console messages:
|
|
6
|
-
${n}`}}});function Rr(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as
|
|
6
|
+
${n}`}}});function Rr(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as Ye}from"zod";var fe=50,xr=Ye.object({urlFilter:Ye.string().describe("Optional regex to filter network entries by URL. Omit to see all.").default("")}),Ke=u({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:xr,execute:(e,t)=>{let r=t.urlFilter.length>0?new RegExp(t.urlFilter,"i"):void 0,n=r==null?e.monitor.networkEntries:e.monitor.networkEntries.filter(c=>r.test(c.url));if(n.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:"No network requests recorded."};let o=n.slice(-fe),s=o.map(c=>{let i=c.statusCode==null?"pending":String(c.statusCode),d=c.responseHeaders["content-type"]??"";return`${c.method} ${i} ${c.url} [${d}]`}).join(`
|
|
7
7
|
`),l=n.length>fe?`
|
|
8
8
|
(showing last ${String(fe)} of ${String(n.length)})`:"";return{specNode:void 0,stepResult:void 0,toolOutput:`${String(o.length)} network requests:${l}
|
|
9
|
-
${s}`}}});import{z as Qe}from"zod";var
|
|
9
|
+
${s}`}}});import{z as Qe}from"zod";var kr=Qe.object({urlPattern:Qe.string().describe("Regex pattern to match the request URL")}),Xe=u({description:"Get the full response body for a specific network request matching the URL pattern. Returns the most recent match.",name:"get_network_response",schema:kr,execute:(e,t)=>{let r=new RegExp(t.urlPattern,"i"),n=e.monitor.networkEntries.filter(i=>r.test(i.url));if(n.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No network requests matching: ${t.urlPattern}`};let o=n.at(-1);if(o==null)return{specNode:void 0,stepResult:void 0,toolOutput:`No network requests matching: ${t.urlPattern}`};let s=o.statusCode==null?"pending":String(o.statusCode),l=Object.entries(o.responseHeaders).map(([i,d])=>` ${i}: ${d}`).join(`
|
|
10
10
|
`),c=o.responseBody??"(no body captured)";return{specNode:void 0,stepResult:void 0,toolOutput:`${o.method} ${s} ${o.url}
|
|
11
11
|
|
|
12
12
|
Response Headers:
|
|
@@ -16,30 +16,30 @@ Body:
|
|
|
16
16
|
${c}`}}});import{z as vr}from"zod";var Pr=vr.object({}),Ze=u({description:"Get a text representation of the current page content for inspection",name:"get_page_content",schema:Pr,execute:async(e,t)=>{let r=await e.page.locator("body").textContent({timeout:5e3}),n=e.page.url(),o=await e.page.title();return{specNode:void 0,stepResult:void 0,toolOutput:`URL: ${n}
|
|
17
17
|
Title: ${o}
|
|
18
18
|
|
|
19
|
-
${r??""}`}}});import{z as et}from"zod";var Tr=et.object({selector:et.string().describe("CSS selector for the element to hover over")}),tt=u({description:"Hover over an element matching the CSS selector",name:"hover",schema:Tr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).hover({timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"hover"};return h({assertions:[],detail:`Hovered over ${t.selector}`,duration:performance.now()-r,nodeType:"hover",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Hover ${t.selector}`})}});import{z as rt}from"zod";var Nr=rt.object({url:rt.string().describe("The URL to navigate to")}),ot=u({description:"Navigate the browser to a URL",name:"navigate",schema:Nr,execute:async(e,t)=>{let r=performance.now();await e.page.goto(t.url,{waitUntil:"domcontentloaded"});let o={id:`agent-step-${String(e.stepIndex)}`,type:"goto",url:{type:"static",value:t.url}};return h({assertions:[],detail:`Navigated to ${t.url}`,duration:performance.now()-r,nodeType:"goto",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Navigate to ${t.url}`})}});import{z as nt}from"zod";var Ar=nt.object({key:nt.string().describe("Key to press, e.g. 'Enter', 'Tab', 'Escape'")}),at=u({description:"Press a keyboard key",name:"press",schema:Ar,execute:async(e,t)=>{let r=performance.now();await e.page.keyboard.press(t.key);let o={id:`agent-step-${String(e.stepIndex)}`,key:t.key,type:"press"};return h({assertions:[],detail:`Pressed ${t.key}`,duration:performance.now()-r,nodeType:"press",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Press ${t.key}`})}});import{z as me}from"zod";var Cr=me.object({height:me.number().int().positive().describe("Viewport height in pixels"),width:me.number().int().positive().describe("Viewport width in pixels")}),st=u({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:Cr,execute:async(e,t)=>{let r=performance.now();await e.page.setViewportSize({height:t.height,width:t.width});let n=`agent-step-${String(e.stepIndex)}`,o={height:t.height,id:n,type:"setViewport",width:t.width};return h({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:o,status:"passed",stepIndex:e.stepIndex,title:`Resize viewport to ${String(t.width)}x${String(t.height)}`})}});import{z as
|
|
19
|
+
${r??""}`}}});import{z as et}from"zod";var Tr=et.object({selector:et.string().describe("CSS selector for the element to hover over")}),tt=u({description:"Hover over an element matching the CSS selector",name:"hover",schema:Tr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).hover({timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"hover"};return h({assertions:[],detail:`Hovered over ${t.selector}`,duration:performance.now()-r,nodeType:"hover",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Hover ${t.selector}`})}});import{z as rt}from"zod";var Nr=rt.object({url:rt.string().describe("The URL to navigate to")}),ot=u({description:"Navigate the browser to a URL",name:"navigate",schema:Nr,execute:async(e,t)=>{let r=performance.now();await e.page.goto(t.url,{waitUntil:"domcontentloaded"});let o={id:`agent-step-${String(e.stepIndex)}`,type:"goto",url:{type:"static",value:t.url}};return h({assertions:[],detail:`Navigated to ${t.url}`,duration:performance.now()-r,nodeType:"goto",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Navigate to ${t.url}`})}});import{z as nt}from"zod";var Ar=nt.object({key:nt.string().describe("Key to press, e.g. 'Enter', 'Tab', 'Escape'")}),at=u({description:"Press a keyboard key",name:"press",schema:Ar,execute:async(e,t)=>{let r=performance.now();await e.page.keyboard.press(t.key);let o={id:`agent-step-${String(e.stepIndex)}`,key:t.key,type:"press"};return h({assertions:[],detail:`Pressed ${t.key}`,duration:performance.now()-r,nodeType:"press",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Press ${t.key}`})}});import{z as me}from"zod";var Cr=me.object({height:me.number().int().positive().describe("Viewport height in pixels"),width:me.number().int().positive().describe("Viewport width in pixels")}),st=u({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:Cr,execute:async(e,t)=>{let r=performance.now();await e.page.setViewportSize({height:t.height,width:t.width});let n=`agent-step-${String(e.stepIndex)}`,o={height:t.height,id:n,type:"setViewport",width:t.width};return h({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:o,status:"passed",stepIndex:e.stepIndex,title:`Resize viewport to ${String(t.width)}x${String(t.height)}`})}});import{z as Ir}from"zod";var $r=Ir.object({}),it=u({description:"Take a screenshot of the current page",name:"screenshot",schema:$r,execute:async(e,t)=>{let r=performance.now(),o={id:`agent-step-${String(e.stepIndex)}`,type:"screenshot"};return h({assertions:[],detail:"Screenshot captured",duration:performance.now()-r,nodeType:"screenshot",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:"Screenshot"})}});import{z as lt}from"zod";var Er=lt.object({pattern:lt.string().describe("Regex pattern to search console log messages")}),ct=u({description:"Search console log messages for a regex pattern. Returns matching entries.",name:"search_console",schema:Er,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),n=e.monitor.consoleEntries.filter(s=>r.test(s.text));if(n.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No console messages matching: ${t.pattern}`};let o=n.map(s=>jr(s)).join(`
|
|
20
20
|
`);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(n.length)} matching console messages:
|
|
21
21
|
${o}`}}});function jr(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as dt}from"zod";var Vr=dt.object({pattern:dt.string().describe("Regex pattern to search across network request URLs and response bodies")}),ut=u({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:Vr,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),n=e.monitor.networkEntries.filter(s=>r.test(s.url)||s.responseBody!=null&&r.test(s.responseBody));if(n.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No network entries matching: ${t.pattern}`};let o=n.map(s=>{let l=s.statusCode==null?"pending":String(s.statusCode),c=r.test(s.url)?" [URL match]":"",i=s.responseBody!=null&&r.test(s.responseBody)?" [body match]":"";return`${s.method} ${l} ${s.url}${c}${i}`}).join(`
|
|
22
22
|
`);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(n.length)} matching network entries:
|
|
23
23
|
${o}`}}});import{z as pt}from"zod";var Or=20,Fr=2,Ur=pt.object({pattern:pt.string().describe("Regex pattern to search for in the page HTML")}),ft=u({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:Ur,execute:async(e,t)=>{let n=(await e.page.content()).split(`
|
|
24
24
|
`),o=Dr({contextLines:Fr,lines:n,maxResults:Or,pattern:t.pattern});return o.length===0?{specNode:void 0,stepResult:void 0,toolOutput:`No matches found for pattern: ${t.pattern}`}:{specNode:void 0,stepResult:void 0,toolOutput:o.join(`
|
|
25
25
|
---
|
|
26
|
-
`)}}});function Dr({contextLines:e,lines:t,maxResults:r,pattern:n}){let o=new RegExp(n,"i"),s=[];return t.forEach((l,c)=>{if(s.length>=r||!o.test(l))return;let i=Math.max(0,c-e),d=Math.min(t.length-1,c+e),m=t.slice(i,d+1).map((
|
|
27
|
-
`);s.push(m)}),s}import{z as he}from"zod";var Wr=he.object({selector:he.string().describe("CSS selector for the select element"),value:he.string().describe("The option value to select")}),mt=u({description:"Select an option from a dropdown/select element",name:"select",schema:Wr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).selectOption(t.value,{timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"select",value:{type:"static",value:t.value}};return h({assertions:[],detail:`Selected "${t.value}" in ${t.selector}`,duration:performance.now()-r,nodeType:"select",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Select ${t.value}`})}});import{z as ht}from"zod";var qr=ht.object({selector:ht.string().describe("CSS selector for the checkbox to uncheck")}),gt=u({description:"Uncheck a checkbox matching the CSS selector",name:"uncheck",schema:qr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).uncheck({timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"uncheck"};return h({assertions:[],detail:`Unchecked ${t.selector}`,duration:performance.now()-r,nodeType:"uncheck",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Uncheck ${t.selector}`})}});import{z as yt}from"zod";var Lr=yt.object({ms:yt.number().describe("Milliseconds to wait")}),wt=u({description:"Wait for a specified duration in milliseconds",name:"wait",schema:Lr,execute:async(e,t)=>{let r=performance.now();await e.page.waitForTimeout(t.ms);let n=`agent-step-${String(e.stepIndex)}`,o={duration:t.ms,id:n,type:"wait"};return h({assertions:[],detail:`Waited ${String(t.ms)}ms`,duration:performance.now()-r,nodeType:"wait",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Wait ${String(t.ms)}ms`})}});import{z as ge}from"zod";var _r=ge.object({selector:ge.string().describe("CSS selector for the element to wait for"),state:ge.enum(["visible","hidden","attached","detached"]).default("visible").describe("The state to wait for (default: visible)")}),bt=u({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:_r,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).waitFor({state:t.state,timeout:15e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},state:t.state,type:"waitFor"};return h({assertions:[],detail:`Waited for ${t.selector} to be ${t.state}`,duration:performance.now()-r,nodeType:"waitFor",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Wait for ${t.selector} to be ${t.state}`})}});var ye=[ot,Le,ze,at,We,gt,mt,tt,wt,bt,it,Ze,ft,Ue,Oe,Je,
|
|
28
|
-
`);import Gr from"pino";var
|
|
26
|
+
`)}}});function Dr({contextLines:e,lines:t,maxResults:r,pattern:n}){let o=new RegExp(n,"i"),s=[];return t.forEach((l,c)=>{if(s.length>=r||!o.test(l))return;let i=Math.max(0,c-e),d=Math.min(t.length-1,c+e),m=t.slice(i,d+1).map(($,Q)=>{let cr=i+Q+1;return`${i+Q===c?">":" "} ${String(cr)}: ${$}`}).join(`
|
|
27
|
+
`);s.push(m)}),s}import{z as he}from"zod";var Wr=he.object({selector:he.string().describe("CSS selector for the select element"),value:he.string().describe("The option value to select")}),mt=u({description:"Select an option from a dropdown/select element",name:"select",schema:Wr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).selectOption(t.value,{timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"select",value:{type:"static",value:t.value}};return h({assertions:[],detail:`Selected "${t.value}" in ${t.selector}`,duration:performance.now()-r,nodeType:"select",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Select ${t.value}`})}});import{z as ht}from"zod";var qr=ht.object({selector:ht.string().describe("CSS selector for the checkbox to uncheck")}),gt=u({description:"Uncheck a checkbox matching the CSS selector",name:"uncheck",schema:qr,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).uncheck({timeout:5e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"uncheck"};return h({assertions:[],detail:`Unchecked ${t.selector}`,duration:performance.now()-r,nodeType:"uncheck",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Uncheck ${t.selector}`})}});import{z as yt}from"zod";var Lr=yt.object({ms:yt.number().describe("Milliseconds to wait")}),wt=u({description:"Wait for a specified duration in milliseconds",name:"wait",schema:Lr,execute:async(e,t)=>{let r=performance.now();await e.page.waitForTimeout(t.ms);let n=`agent-step-${String(e.stepIndex)}`,o={duration:t.ms,id:n,type:"wait"};return h({assertions:[],detail:`Waited ${String(t.ms)}ms`,duration:performance.now()-r,nodeType:"wait",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Wait ${String(t.ms)}ms`})}});import{z as ge}from"zod";var _r=ge.object({selector:ge.string().describe("CSS selector for the element to wait for"),state:ge.enum(["visible","hidden","attached","detached"]).default("visible").describe("The state to wait for (default: visible)")}),bt=u({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:_r,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).waitFor({state:t.state,timeout:15e3});let o={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},state:t.state,type:"waitFor"};return h({assertions:[],detail:`Waited for ${t.selector} to be ${t.state}`,duration:performance.now()-r,nodeType:"waitFor",page:e.page,runStartTime:e.runStartTime,specNode:o,status:"passed",stepIndex:e.stepIndex,title:`Wait for ${t.selector} to be ${t.state}`})}});var ye=[ot,Le,ze,at,We,gt,mt,tt,wt,bt,it,Ze,ft,Ue,Oe,Je,Ke,Xe,ct,ut,st,Ge,Be],zi=new Map(ye.map(e=>[e.name,e])),Br=ye.map(e=>e.anthropicTool),Mr=ye.map(e=>`- **${e.name}**: ${e.anthropicTool.description??""}`).join(`
|
|
28
|
+
`);import Gr from"pino";var N=Gr({transport:{options:{ignore:"pid,hostname"},target:"pino-pretty"}});import{execFileSync as il}from"child_process";import{createRequire as cl}from"module";import ul from"fs";import{chromium as fl}from"playwright";import{mkdir as yl,writeFile as wl}from"fs/promises";import Sl from"path";import{readdir as El,rm as jl,stat as Vl}from"fs/promises";import Xr from"path";import{graphql as Zr}from"gql.tada";import{print as Yr}from"graphql";async function V(e){let t=Yr(e.document),r=JSON.stringify({query:t,variables:e.variables}),o=await(await fetch(`${e.config.ripploServerUrl}/graphql`,{body:r,headers:{Authorization:`Bearer ${e.config.token}`,"Content-Type":"application/json"},method:"POST"})).json();if(!Kr(o))throw new Error("Invalid GraphQL response");if(Qr(o))throw new Error(o.errors.map(s=>s.message).join(", "));if(o.data==null)throw new Error("No data returned from server");return o.data}function Kr(e){return typeof e=="object"&&e!=null&&("data"in e||"errors"in e)}function Qr(e){return Array.isArray(e.errors)&&e.errors.length>0}var Wl=Xr.join(process.cwd(),".ripplo","debug"),ql=360*60*1e3,Ll=Zr(`
|
|
29
29
|
mutation RevokeCurrentCliToken {
|
|
30
30
|
revokeCurrentCliToken
|
|
31
31
|
}
|
|
32
|
-
`);import Pt from"fs";import we from"path";import{z as O}from"zod";var ro=O.object({baseUrl:O.string().optional(),cloudBaseUrl:O.string(),preconditionApiPath:O.string().optional(),projectId:O.string()}),oo=O.object({ripploServerUrl:O.string(),token:O.string()}),no=ro.extend(oo.shape),Nt=we.join(process.cwd(),".ripplo"),ao=we.join(Nt,"settings.json"),so=we.join(Nt,"settings.local.json");function Tt(e){if(!Pt.existsSync(e))return null;let t=Pt.readFileSync(e,"utf8");return JSON.parse(t)}function be(){let e=Tt(ao),t=Tt(so);if(e==null||t==null)return null;let r=no.safeParse({...e,...t});return r.success?r.data:null}import{z as a}from"zod";import{z as N}from"zod";import{z as g}from"zod";var lo=g.object({by:g.literal("css"),value:g.string().min(1)}),co=g.object({by:g.literal("testId"),value:g.string().min(1)}),uo=g.object({by:g.literal("role"),exact:g.boolean().optional(),name:g.string().optional(),role:g.string().min(1)}),po=g.object({by:g.literal("text"),exact:g.boolean().optional(),value:g.string().min(1)}),fo=g.object({by:g.literal("label"),exact:g.boolean().optional(),value:g.string().min(1)}),mo=g.object({by:g.literal("placeholder"),value:g.string().min(1)}),ho=g.object({by:g.literal("altText"),value:g.string().min(1)}),f=g.discriminatedUnion("by",[lo,co,uo,po,fo,mo,ho]);import{z as At}from"zod";var v=At.enum(["equals","notEquals","contains","startsWith","endsWith","matches"]),E=At.enum(["equals","notEquals","greaterThan","greaterThanOrEqual","lessThan","lessThanOrEqual"]);import{z as S}from"zod";var go=S.object({type:S.literal("static"),value:S.union([S.string(),S.number(),S.boolean()])}),Se=S.object({name:S.string().min(1),type:S.literal("variable")}),F=S.discriminatedUnion("type",[go,Se]),b=S.discriminatedUnion("type",[S.object({type:S.literal("static"),value:S.string()}),Se]),D=S.discriminatedUnion("type",[S.object({type:S.literal("static"),value:S.number().int().nonnegative()}),Se]);var W=N.discriminatedUnion("type",[N.object({locator:f,type:N.literal("elementVisible")}),N.object({locator:f,type:N.literal("elementNotVisible")}),N.object({expected:b,operator:v,type:N.literal("urlMatch")}),N.object({expected:b,locator:f,operator:v,type:N.literal("textMatch")}),N.object({expected:F,operator:E,type:N.literal("variableCompare"),variable:N.string().min(1)})]);import{z as P}from"zod";var B=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 yo=a.string().min(1),j=a.array(yo).min(1),p={comment:a.string().optional(),id:a.string().min(1),label:a.string().optional(),next:a.string().optional(),timeout:a.number().int().positive().optional()},wo=a.object({...p,type:a.literal("goto"),url:b}),bo=a.object({...p,locator:f,type:a.literal("click")}),So=a.object({...p,locator:f,type:a.literal("fill"),value:b}),Ro=a.object({...p,locator:f,type:a.literal("select"),value:b}),ko=a.object({...p,locator:f,type:a.literal("hover")}),xo=a.object({...p,key:a.string().min(1),locator:f.optional(),type:a.literal("press")}),vo=a.object({...p,locator:f,type:a.literal("check")}),Po=a.object({...p,locator:f,type:a.literal("uncheck")}),To=a.object({...p,type:a.literal("screenshot")}),No=a.object({...p,height:a.number().int().positive(),type:a.literal("setViewport"),width:a.number().int().positive()}),Ao=a.object({...p,duration:a.number().int().positive(),type:a.literal("wait")}),Co=a.object({...p,message:a.string().min(1),type:a.literal("fail")}),$o=a.object({...p,type:a.literal("setVariable"),value:F,variable:a.string().min(1)}),Io=a.object({...p,locator:f,type:a.literal("extractText"),variable:a.string().min(1)}),Eo=a.object({...p,files:a.array(a.string()).min(1),locator:f,type:a.literal("upload")}),jo=a.object({...p,locator:f,type:a.literal("dblclick")}),Vo=a.object({...p,source:f,target:f,type:a.literal("drag")}),Oo=a.object({...p,locator:f,type:a.literal("scrollIntoView")}),Fo=a.object({...p,locator:f,type:a.literal("type"),value:b}),Uo=a.object({...p,locator:f,type:a.literal("focus")}),Do=a.object({...p,locator:f,state:a.enum(["visible","hidden","attached","detached"]).optional(),type:a.literal("waitFor")}),Wo=a.object({...p,expected:b,operator:v,type:a.literal("waitForUrl")}),qo=a.object({...p,type:a.literal("waitForResponse"),urlPattern:b}),Lo=a.object({...p,type:a.literal("waitForRequest"),urlPattern:b}),_o=a.object({...p,locator:f,type:a.literal("assertVisible")}),Bo=a.object({...p,locator:f,type:a.literal("assertNotVisible")}),Mo=a.object({...p,expected:b,locator:f,operator:v,type:a.literal("assertText")}),Go=a.object({...p,expected:b,operator:v,type:a.literal("assertUrl")}),zo=a.object({...p,expected:D,locator:f,operator:E,type:a.literal("assertCount")}),Ho=a.object({...p,expected:b,locator:f,operator:v,type:a.literal("assertValue")}),Jo=a.object({...p,attribute:a.string().min(1),expected:b,locator:f,operator:v,type:a.literal("assertAttribute")}),Ko=a.object({...p,locator:f,type:a.literal("assertEnabled")}),Yo=a.object({...p,locator:f,type:a.literal("assertDisabled")}),Qo=a.object({...p,alternate:j.optional(),condition:W,consequent:j,type:a.literal("if")}),Xo=a.object({...p,body:j,iteratorVar:a.string().min(1).optional(),times:a.number().int().positive(),type:a.literal("loop")}),Zo=a.object({...p,body:j,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")}),en=a.object({...p,branches:a.array(j).min(2),type:a.literal("parallel")}),tn=a.object({...p,body:j,catch:j.optional(),finally:j.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'"}),rn=a.object({...p,nodes:j,type:a.literal("group")}),J=a.discriminatedUnion("type",[wo,bo,So,Ro,ko,xo,vo,Po,Do,Wo,qo,Lo,_o,Bo,Mo,Go,zo,Ho,Jo,Ko,Yo,To,No,Ao,Co,$o,Io,Eo,jo,Vo,Oo,Fo,Uo,Qo,Xo,Zo,en,rn]),Ct=a.union([J,tn]),q=a.object({entryNode:a.string().min(1),nodes:a.record(a.string(),Ct),variables:a.record(a.string(),B).optional(),version:a.literal(2)});function Z(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 ne,readFragment as Id}from"gql.tada";import{z as ee}from"zod";var Re=ee.object({from:ee.string().min(1).describe("Key of the source state in the states record"),to:ee.string().min(1).describe("Key of the target state in the states record"),workflow:ee.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 U}from"zod";import{z as y}from"zod";var ke=y.object({precondition:y.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"),te=y.object({satisfied:y.boolean().describe("Whether the precondition is already satisfied. If true, execution is skipped.")}).describe("Response from POST {preconditionApiUrl}/check"),re=y.object({data:y.record(y.string(),y.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:y.string().optional().describe("Human-readable error message if success is false"),navigateTo:y.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:y.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."),xe=y.object({preconditions:y.array(y.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."),ve=y.object({error:y.string().optional().describe("Human-readable error message if success is false"),success:y.boolean().describe("Whether teardown completed successfully")}).describe("Response from PUT {preconditionApiUrl}/teardown"),L=y.object({depends:y.array(y.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:y.string().min(1).describe("Human-readable description of what this precondition ensures"),returns:y.array(y.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 oe}from"zod";var Pe=oe.object({preconditions:oe.array(oe.string().min(1)).describe("Ordered list of precondition names to satisfy before entering this state"),route:oe.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 K=U.object({edges:U.array(Re).describe("Directed edges between states, each executed by a workflow"),preconditions:U.record(U.string(),L).describe("Named preconditions keyed by name (e.g. 'auth:admin', 'data:three-projects'). States reference these by name."),resetPrecondition:U.string().optional().describe("Name of a precondition to run before every state setup as a global clean slate"),states:U.record(U.string(),Pe).describe("States keyed by stable ID (kebab-case)"),version:U.literal(3).describe("Schema version, always 3")}).describe("Ripplo State Graph v3 \u2014 models application states, edges, and executable preconditions");function Te(e){let t=K.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 yn(e){let t=e.preconditions,r=new Set,n=new Set,o=[];function s(l){if(n.has(l)){o.push(`Precondition "${l}" has a circular dependency`);return}if(r.has(l))return;r.add(l),n.add(l);let c=t[l];c?.depends!=null&&c.depends.forEach(i=>{s(i)}),n.delete(l)}return Object.keys(t).forEach(l=>{s(l)}),o}function Ne(e){let t=[],r=new Set(Object.keys(e.preconditions)),n=new Set(Object.keys(e.states));e.edges.forEach((i,d)=>{n.has(i.from)||t.push({message:`References non-existent source state "${i.from}"`,path:`edges[${String(d)}].from`}),n.has(i.to)||t.push({message:`References non-existent target state "${i.to}"`,path:`edges[${String(d)}].to`})}),Object.entries(e.states).forEach(([i,d])=>{d.preconditions.forEach(m=>{r.has(m)||t.push({message:`References non-existent precondition "${m}"`,path:`states.${i}.preconditions`})})}),Object.entries(e.preconditions).forEach(([i,d])=>{d.depends!=null&&d.depends.forEach(m=>{r.has(m)||t.push({message:`Depends on non-existent precondition "${m}"`,path:`preconditions.${i}.depends`})})}),e.resetPrecondition!=null&&!r.has(e.resetPrecondition)&&t.push({message:`References non-existent precondition "${e.resetPrecondition}"`,path:"resetPrecondition"});let o=new Set(e.edges.flatMap(i=>[i.from,i.to]));Object.keys(e.states).forEach(i=>{o.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(d=>s.add(d))}),Object.values(e.preconditions).forEach(i=>{i.depends!=null&&i.depends.forEach(d=>s.add(d))}),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,d)=>{let m=`${i.from}|${i.to}|${i.workflow}`;l.has(m)&&t.push({message:`Duplicate edge from "${i.from}" to "${i.to}" with workflow "${i.workflow}"`,path:`edges[${String(d)}]`}),l.add(m)}),yn(e).forEach(i=>t.push({message:i,path:"preconditions"})),t}import{parseSetCookie as gd}from"set-cookie-parser";import{z as It}from"zod";var bd=It.record(It.string(),L);import{graphql as Ae}from"gql.tada";var vd=Ae(`
|
|
32
|
+
`);import Pt from"fs";import we from"path";import{z as O}from"zod";var ro=O.object({baseUrl:O.string().optional(),cloudBaseUrl:O.string(),preconditionApiPath:O.string().optional(),projectId:O.string()}),oo=O.object({ripploServerUrl:O.string(),token:O.string()}),no=ro.extend(oo.shape),Nt=we.join(process.cwd(),".ripplo"),ao=we.join(Nt,"settings.json"),so=we.join(Nt,"settings.local.json");function Tt(e){if(!Pt.existsSync(e))return null;let t=Pt.readFileSync(e,"utf8");return JSON.parse(t)}function be(){let e=Tt(ao),t=Tt(so);if(e==null||t==null)return null;let r=no.safeParse({...e,...t});return r.success?r.data:null}import{z as a}from"zod";import{z as A}from"zod";import{z as g}from"zod";var lo=g.object({by:g.literal("css"),value:g.string().min(1)}),co=g.object({by:g.literal("testId"),value:g.string().min(1)}),uo=g.object({by:g.literal("role"),exact:g.boolean().optional(),name:g.string().optional(),role:g.string().min(1)}),po=g.object({by:g.literal("text"),exact:g.boolean().optional(),value:g.string().min(1)}),fo=g.object({by:g.literal("label"),exact:g.boolean().optional(),value:g.string().min(1)}),mo=g.object({by:g.literal("placeholder"),value:g.string().min(1)}),ho=g.object({by:g.literal("altText"),value:g.string().min(1)}),f=g.discriminatedUnion("by",[lo,co,uo,po,fo,mo,ho]);import{z as At}from"zod";var v=At.enum(["equals","notEquals","contains","startsWith","endsWith","matches"]),E=At.enum(["equals","notEquals","greaterThan","greaterThanOrEqual","lessThan","lessThanOrEqual"]);import{z as S}from"zod";var go=S.object({type:S.literal("static"),value:S.union([S.string(),S.number(),S.boolean()])}),Se=S.object({name:S.string().min(1),type:S.literal("variable")}),F=S.discriminatedUnion("type",[go,Se]),b=S.discriminatedUnion("type",[S.object({type:S.literal("static"),value:S.string()}),Se]),D=S.discriminatedUnion("type",[S.object({type:S.literal("static"),value:S.number().int().nonnegative()}),Se]);var W=A.discriminatedUnion("type",[A.object({locator:f,type:A.literal("elementVisible")}),A.object({locator:f,type:A.literal("elementNotVisible")}),A.object({expected:b,operator:v,type:A.literal("urlMatch")}),A.object({expected:b,locator:f,operator:v,type:A.literal("textMatch")}),A.object({expected:F,operator:E,type:A.literal("variableCompare"),variable:A.string().min(1)})]);import{z as P}from"zod";var B=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 yo=a.string().min(1),j=a.array(yo).min(1),p={comment:a.string().optional(),id:a.string().min(1),label:a.string().optional(),next:a.string().optional(),timeout:a.number().int().positive().optional()},wo=a.object({...p,type:a.literal("goto"),url:b}),bo=a.object({...p,locator:f,type:a.literal("click")}),So=a.object({...p,locator:f,type:a.literal("fill"),value:b}),Ro=a.object({...p,locator:f,type:a.literal("select"),value:b}),xo=a.object({...p,locator:f,type:a.literal("hover")}),ko=a.object({...p,key:a.string().min(1),locator:f.optional(),type:a.literal("press")}),vo=a.object({...p,locator:f,type:a.literal("check")}),Po=a.object({...p,locator:f,type:a.literal("uncheck")}),To=a.object({...p,type:a.literal("screenshot")}),No=a.object({...p,height:a.number().int().positive(),type:a.literal("setViewport"),width:a.number().int().positive()}),Ao=a.object({...p,duration:a.number().int().positive(),type:a.literal("wait")}),Co=a.object({...p,message:a.string().min(1),type:a.literal("fail")}),Io=a.object({...p,type:a.literal("setVariable"),value:F,variable:a.string().min(1)}),$o=a.object({...p,locator:f,type:a.literal("extractText"),variable:a.string().min(1)}),Eo=a.object({...p,files:a.array(a.string()).min(1),locator:f,type:a.literal("upload")}),jo=a.object({...p,locator:f,type:a.literal("dblclick")}),Vo=a.object({...p,source:f,target:f,type:a.literal("drag")}),Oo=a.object({...p,locator:f,type:a.literal("scrollIntoView")}),Fo=a.object({...p,locator:f,type:a.literal("type"),value:b}),Uo=a.object({...p,locator:f,type:a.literal("focus")}),Do=a.object({...p,locator:f,state:a.enum(["visible","hidden","attached","detached"]).optional(),type:a.literal("waitFor")}),Wo=a.object({...p,expected:b,operator:v,type:a.literal("waitForUrl")}),qo=a.object({...p,type:a.literal("waitForResponse"),urlPattern:b}),Lo=a.object({...p,type:a.literal("waitForRequest"),urlPattern:b}),_o=a.object({...p,locator:f,type:a.literal("assertVisible")}),Bo=a.object({...p,locator:f,type:a.literal("assertNotVisible")}),Mo=a.object({...p,expected:b,locator:f,operator:v,type:a.literal("assertText")}),Go=a.object({...p,expected:b,operator:v,type:a.literal("assertUrl")}),zo=a.object({...p,expected:D,locator:f,operator:E,type:a.literal("assertCount")}),Ho=a.object({...p,expected:b,locator:f,operator:v,type:a.literal("assertValue")}),Jo=a.object({...p,attribute:a.string().min(1),expected:b,locator:f,operator:v,type:a.literal("assertAttribute")}),Yo=a.object({...p,locator:f,type:a.literal("assertEnabled")}),Ko=a.object({...p,locator:f,type:a.literal("assertDisabled")}),Qo=a.object({...p,alternate:j.optional(),condition:W,consequent:j,type:a.literal("if")}),Xo=a.object({...p,body:j,iteratorVar:a.string().min(1).optional(),times:a.number().int().positive(),type:a.literal("loop")}),Zo=a.object({...p,body:j,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")}),en=a.object({...p,branches:a.array(j).min(2),type:a.literal("parallel")}),tn=a.object({...p,body:j,catch:j.optional(),finally:j.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'"}),rn=a.object({...p,nodes:j,type:a.literal("group")}),J=a.discriminatedUnion("type",[wo,bo,So,Ro,xo,ko,vo,Po,Do,Wo,qo,Lo,_o,Bo,Mo,Go,zo,Ho,Jo,Yo,Ko,To,No,Ao,Co,Io,$o,Eo,jo,Vo,Oo,Fo,Uo,Qo,Xo,Zo,en,rn]),Ct=a.union([J,tn]),q=a.object({entryNode:a.string().min(1),nodes:a.record(a.string(),Ct),variables:a.record(a.string(),B).optional(),version:a.literal(2)});function Z(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 ne,readFragment as jd}from"gql.tada";import{z as ee}from"zod";var Re=ee.object({from:ee.string().min(1).describe("Key of the source state in the states record"),to:ee.string().min(1).describe("Key of the target state in the states record"),workflow:ee.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 U}from"zod";import{z as y}from"zod";var xe=y.object({precondition:y.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"),te=y.object({satisfied:y.boolean().describe("Whether the precondition is already satisfied. If true, execution is skipped.")}).describe("Response from POST {preconditionApiUrl}/check"),re=y.object({data:y.record(y.string(),y.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:y.string().optional().describe("Human-readable error message if success is false"),navigateTo:y.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:y.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."),ke=y.object({preconditions:y.array(y.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."),ve=y.object({error:y.string().optional().describe("Human-readable error message if success is false"),success:y.boolean().describe("Whether teardown completed successfully")}).describe("Response from PUT {preconditionApiUrl}/teardown"),L=y.object({depends:y.array(y.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:y.string().min(1).describe("Human-readable description of what this precondition ensures"),returns:y.array(y.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 oe}from"zod";var Pe=oe.object({preconditions:oe.array(oe.string().min(1)).describe("Ordered list of precondition names to satisfy before entering this state"),route:oe.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 Y=U.object({edges:U.array(Re).describe("Directed edges between states, each executed by a workflow"),preconditions:U.record(U.string(),L).describe("Named preconditions keyed by name (e.g. 'auth:admin', 'data:three-projects'). States reference these by name."),resetPrecondition:U.string().optional().describe("Name of a precondition to run before every state setup as a global clean slate"),states:U.record(U.string(),Pe).describe("States keyed by stable ID (kebab-case)"),version:U.literal(3).describe("Schema version, always 3")}).describe("Ripplo State Graph v3 \u2014 models application states, edges, and executable preconditions");function Te(e){let t=Y.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 yn(e){let t=e.preconditions,r=new Set,n=new Set,o=[];function s(l){if(n.has(l)){o.push(`Precondition "${l}" has a circular dependency`);return}if(r.has(l))return;r.add(l),n.add(l);let c=t[l];c?.depends!=null&&c.depends.forEach(i=>{s(i)}),n.delete(l)}return Object.keys(t).forEach(l=>{s(l)}),o}function Ne(e){let t=[],r=new Set(Object.keys(e.preconditions)),n=new Set(Object.keys(e.states));e.edges.forEach((i,d)=>{n.has(i.from)||t.push({message:`References non-existent source state "${i.from}"`,path:`edges[${String(d)}].from`}),n.has(i.to)||t.push({message:`References non-existent target state "${i.to}"`,path:`edges[${String(d)}].to`})}),Object.entries(e.states).forEach(([i,d])=>{d.preconditions.forEach(m=>{r.has(m)||t.push({message:`References non-existent precondition "${m}"`,path:`states.${i}.preconditions`})})}),Object.entries(e.preconditions).forEach(([i,d])=>{d.depends!=null&&d.depends.forEach(m=>{r.has(m)||t.push({message:`Depends on non-existent precondition "${m}"`,path:`preconditions.${i}.depends`})})}),e.resetPrecondition!=null&&!r.has(e.resetPrecondition)&&t.push({message:`References non-existent precondition "${e.resetPrecondition}"`,path:"resetPrecondition"});let o=new Set(e.edges.flatMap(i=>[i.from,i.to]));Object.keys(e.states).forEach(i=>{o.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(d=>s.add(d))}),Object.values(e.preconditions).forEach(i=>{i.depends!=null&&i.depends.forEach(d=>s.add(d))}),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,d)=>{let m=`${i.from}|${i.to}|${i.workflow}`;l.has(m)&&t.push({message:`Duplicate edge from "${i.from}" to "${i.to}" with workflow "${i.workflow}"`,path:`edges[${String(d)}]`}),l.add(m)}),yn(e).forEach(i=>t.push({message:i,path:"preconditions"})),t}import{parseSetCookie as wd}from"set-cookie-parser";import{z as $t}from"zod";var Rd=$t.record($t.string(),L);import{graphql as Ae}from"gql.tada";var Td=Ae(`
|
|
33
33
|
mutation StartRunCLI($runId: String!, $platform: String!, $agentProfileId: String) {
|
|
34
34
|
startRun(runId: $runId, platform: $platform, agentProfileId: $agentProfileId) {
|
|
35
35
|
id
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
`),
|
|
38
|
+
`),Nd=Ae(`
|
|
39
39
|
mutation SubmitRunStepsCLI($runResultId: String!, $steps: [StepInput!]!) {
|
|
40
40
|
submitRunSteps(runResultId: $runResultId, steps: $steps)
|
|
41
41
|
}
|
|
42
|
-
`),
|
|
42
|
+
`),Ad=Ae(`
|
|
43
43
|
mutation CompleteRunCLI(
|
|
44
44
|
$runResultId: String!
|
|
45
45
|
$status: String!
|
|
@@ -88,13 +88,13 @@ ${o}`}}});import{z as pt}from"zod";var Or=20,Fr=2,Ur=pt.object({pattern:pt.strin
|
|
|
88
88
|
...WorkflowRun
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
`,[vn]),
|
|
91
|
+
`,[vn]),Md=ne(`
|
|
92
92
|
query ProjectRun($projectId: String!) {
|
|
93
93
|
project(id: $projectId) {
|
|
94
94
|
...ProjectRun
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
`,[Pn]),
|
|
97
|
+
`,[Pn]),Gd=ne(`
|
|
98
98
|
query AgentProfileRun($id: String!) {
|
|
99
99
|
agentProfile(id: $id) {
|
|
100
100
|
id
|
|
@@ -104,7 +104,7 @@ ${o}`}}});import{z as pt}from"zod";var Or=20,Fr=2,Ur=pt.object({pattern:pt.strin
|
|
|
104
104
|
successCriteria
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
`);function
|
|
107
|
+
`);function x(e){return{content:[{text:e,type:"text"}]}}async function Et(e){let t=be();return t==null?x("Not logged in. Run: ripplo login"):e(t)}import{z as k}from"zod";import Tn from"fs";import Tu from"path";import{z as M}from"zod";var K=M.object({additionalChecks:M.array(M.string()).default([]),description:M.string(),expectedOutcome:M.string(),name:M.string(),spec:q});function ae(e){let t=Tn.readFileSync(e,"utf8"),r=JSON.parse(t);return K.parse(r)}var Nn=k.object({additionalProperties:k.unknown().optional(),properties:k.record(k.string(),k.record(k.string(),k.unknown())).optional(),required:k.array(k.string()).optional(),type:k.string().optional()}),jt=k.object({oneOf:k.array(Nn).optional()});function Vt(){let e=Vn(),t={category:"Control Flow",fields:"body, catch?, finally?",typeName:"try"},r=[...e.map(s=>On(s)),t],n=Fn(r);return`# Ripplo Workflow Spec v2
|
|
108
108
|
|
|
109
109
|
## Node Types
|
|
110
110
|
|
|
@@ -135,7 +135,7 @@ Each workflow file in \`.ripplo/workflows/\` is a JSON file with: name, descript
|
|
|
135
135
|
The spec contains: version (2), entryNode, nodes, variables?.
|
|
136
136
|
|
|
137
137
|
Use \`get_node_schema\` for full JSON Schema of specific node types.
|
|
138
|
-
Use \`get_spec_patterns\` for locator, condition, and variable schemas + best practices.`}function Ot({nodeTypes:e}){let r=jt.parse(structuredClone(
|
|
138
|
+
Use \`get_spec_patterns\` for locator, condition, and variable schemas + best practices.`}function Ot({nodeTypes:e}){let r=jt.parse(structuredClone(k.toJSONSchema(J))).oneOf??[],n=new Map;r.forEach(s=>{let l=Ut(s);l.length>0&&n.set(l,s)});let o=[];return e.forEach(s=>{if(s==="try"){o.push(Wn);return}let l=n.get(s);if(l==null){o.push(`## ${s}
|
|
139
139
|
|
|
140
140
|
Unknown node type "${s}".`);return}o.push(`## ${s}
|
|
141
141
|
|
|
@@ -145,10 +145,10 @@ ${JSON.stringify(l,null,2)}
|
|
|
145
145
|
|
|
146
146
|
`)}function Ft(){return`# Spec Patterns & Building Blocks
|
|
147
147
|
|
|
148
|
-
${[{label:"Locator",schema:f},{label:"Condition",schema:W},{label:"StringValueRef",schema:b},{label:"NumericValueRef",schema:D},{label:"ValueRef",schema:F},{label:"ComparisonOperator (string)",schema:v},{label:"NumericOperator",schema:E},{label:"VariableDef",schema:B},{label:"WorkflowFile (top-level)",schema:
|
|
148
|
+
${[{label:"Locator",schema:f},{label:"Condition",schema:W},{label:"StringValueRef",schema:b},{label:"NumericValueRef",schema:D},{label:"ValueRef",schema:F},{label:"ComparisonOperator (string)",schema:v},{label:"NumericOperator",schema:E},{label:"VariableDef",schema:B},{label:"WorkflowFile (top-level)",schema:K},{label:"WorkflowSpec (the spec field)",schema:q}].map(({label:r,schema:n})=>`### ${r}
|
|
149
149
|
|
|
150
150
|
\`\`\`json
|
|
151
|
-
${JSON.stringify(
|
|
151
|
+
${JSON.stringify(k.toJSONSchema(n),null,2)}
|
|
152
152
|
\`\`\``).join(`
|
|
153
153
|
|
|
154
154
|
`)}
|
|
@@ -167,7 +167,7 @@ ${JSON.stringify(x.toJSONSchema(n),null,2)}
|
|
|
167
167
|
10. **URLs in goto nodes must be relative paths** (\`/login\`), never absolute URLs \u2014 the baseUrl handles the origin
|
|
168
168
|
11. **Value references use \`"type": "static"\`**, never \`"type": "literal"\`
|
|
169
169
|
12. **Node IDs must match their key** in the \`nodes\` object
|
|
170
|
-
13. **Template variables use \`{{name}}\`**, never \`$name\` or \`\${name}\` \u2014 only double curly braces work`}var An=["Actions","Waits","Assertions","Variables","Control Flow","Other"],Cn=new Set(["if","loop","forEach","parallel","try","group"])
|
|
170
|
+
13. **Template variables use \`{{name}}\`**, never \`$name\` or \`\${name}\` \u2014 only double curly braces work`}var An=["Actions","Waits","Assertions","Variables","Control Flow","Other"],Cn=new Set(["if","loop","forEach","parallel","try","group"]),In=new Set(["setVariable","extractText"]),$n=new Set(["screenshot","setViewport","fail"]),En=new Set(["comment","id","label","next","timeout","type"]);function jn(e){return e.startsWith("assert")?"Assertions":e.startsWith("waitFor")||e==="wait"?"Waits":Cn.has(e)?"Control Flow":In.has(e)?"Variables":$n.has(e)?"Other":"Actions"}function Vn(){return jt.parse(structuredClone(k.toJSONSchema(J))).oneOf??[]}function Ut(e){let t=e.properties?.type;if(t==null)return"";let r=t.const;return typeof r=="string"?r:""}function On(e){let t=Ut(e),r=new Set(e.required??[]),n=Object.keys(e.properties??{}).filter(o=>!En.has(o)).map(o=>r.has(o)?o:`${o}?`).join(", ");return{category:jn(t),fields:n,typeName:t}}function Fn(e){let t=new Map;return e.forEach(r=>{let n=t.get(r.category)??[];n.push(r),t.set(r.category,n)}),t}function Un(){return`---
|
|
171
171
|
|
|
172
172
|
## Referenced Schemas
|
|
173
173
|
|
|
@@ -176,7 +176,7 @@ ${[G("Locator",f),G("StringValueRef",b),G("NumericValueRef",D),G("ComparisonOper
|
|
|
176
176
|
`)}`}var Dn=Un();function G(e,t){return`### ${e}
|
|
177
177
|
|
|
178
178
|
\`\`\`json
|
|
179
|
-
${JSON.stringify(
|
|
179
|
+
${JSON.stringify(k.toJSONSchema(t),null,2)}
|
|
180
180
|
\`\`\``}var Wn=["## try","","The `try` node uses a refinement: at least one of `catch` or `finally` is required.","","Fields: body (required), catch? (optional), finally? (optional)","Plus base fields: id, type, next?, timeout?, label?, comment?"].join(`
|
|
181
181
|
`);import{graphql as se,readFragment as qn}from"gql.tada";var Wt=se(`
|
|
182
182
|
fragment WorkflowIdMCP on Workflow {
|
|
@@ -229,16 +229,16 @@ ${JSON.stringify(x.toJSONSchema(t),null,2)}
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
-
`);async function qt({config:e,slugs:t}){let r=await Mn({config:e,slugs:t}),n=await Promise.allSettled(r.map(async({slug:i,workflowId:d})=>({runId:await zn({config:e,workflowId:d}),slug:i}))),o=[],s=[];n.forEach((i,d)=>{let m=r[d];if(m==null)return;if(i.status==="fulfilled"){o.push(i.value);return}let
|
|
232
|
+
`);async function qt({config:e,slugs:t}){let r=await Mn({config:e,slugs:t}),n=await Promise.allSettled(r.map(async({slug:i,workflowId:d})=>({runId:await zn({config:e,workflowId:d}),slug:i}))),o=[],s=[];n.forEach((i,d)=>{let m=r[d];if(m==null)return;if(i.status==="fulfilled"){o.push(i.value);return}let $=i.reason instanceof Error?i.reason.message:String(i.reason);s.push({runId:"",slug:m.slug,summary:`ERROR creating run: ${$}`})}),(await Promise.allSettled(o.map(async({runId:i,slug:d})=>{let m=await Jn({config:e,runId:i}),$=Gn({config:e,runId:i,slug:d}),Q=`.ripplo/debug/${i}/`;return{runId:i,slug:d,summary:`View run: ${$}
|
|
233
233
|
Debug artifacts: ${Q}
|
|
234
234
|
|
|
235
|
-
${m}`}}))).forEach((i,d)=>{let m=o[d];if(m==null)return;if(i.status==="fulfilled"){s.push(i.value);return}let
|
|
235
|
+
${m}`}}))).forEach((i,d)=>{let m=o[d];if(m==null)return;if(i.status==="fulfilled"){s.push(i.value);return}let $=i.reason instanceof Error?i.reason.message:String(i.reason);s.push({runId:m.runId,slug:m.slug,summary:`ERROR polling run ${m.runId}: ${$}`})});let c=s.map(i=>`**${i.slug}:**
|
|
236
236
|
${i.summary}`).join(`
|
|
237
237
|
|
|
238
238
|
---
|
|
239
239
|
|
|
240
|
-
`);return{results:s,summary:c}}async function Mn({config:e,slugs:t}){let r=await V({config:e,document:Ln,variables:{projectId:e.projectId}});if(r.project==null)throw new Error("Project not found");if(r.project.devSession==null)throw new Error("No active dev session. Start one by running `ripplo dev` in your terminal, then try again.");let n=(r.project.workflows??[]).map(o=>qn(Wt,o));return t.map(o=>{let s=n.find(l=>l.slug===o);if(s==null)throw new Error(`Workflow "${o}" not found on server. Is the dev session running and synced?`);return{slug:o,workflowId:s.id}})}function Gn({config:e,runId:t,slug:r}){return`${e.ripploServerUrl.replace(/:3000\b/,":3001")}/projects/${e.projectId}/workflows/${r}/runs/${t}`}async function zn({config:e,workflowId:t}){let r=await V({config:e,document:_n,variables:{concurrency:1,platforms:["chromium"],workflowId:t}});if(r.createRun==null)throw new Error("Failed to create run");return r.createRun.id}var Hn=2e3,Dt=150;async function Jn({config:e,runId:t}){let r=0;for(;r<Dt;){await
|
|
241
|
-
`)}function
|
|
240
|
+
`);return{results:s,summary:c}}async function Mn({config:e,slugs:t}){let r=await V({config:e,document:Ln,variables:{projectId:e.projectId}});if(r.project==null)throw new Error("Project not found");if(r.project.devSession==null)throw new Error("No active dev session. Start one by running `ripplo dev` in your terminal, then try again.");let n=(r.project.workflows??[]).map(o=>qn(Wt,o));return t.map(o=>{let s=n.find(l=>l.slug===o);if(s==null)throw new Error(`Workflow "${o}" not found on server. Is the dev session running and synced?`);return{slug:o,workflowId:s.id}})}function Gn({config:e,runId:t,slug:r}){return`${e.ripploServerUrl.replace(/:3000\b/,":3001")}/projects/${e.projectId}/workflows/${r}/runs/${t}`}async function zn({config:e,workflowId:t}){let r=await V({config:e,document:_n,variables:{concurrency:1,platforms:["chromium"],workflowId:t}});if(r.createRun==null)throw new Error("Failed to create run");return r.createRun.id}var Hn=2e3,Dt=150;async function Jn({config:e,runId:t}){let r=0;for(;r<Dt;){await Kn(Hn),r+=1;let o=(await V({config:e,document:Bn,variables:{id:t}})).run;if(o==null)throw new Error(`Run ${t} not found`);if(!(o.status==="pending"||o.status==="running"))return Yn({results:o.results??[],runId:t,status:o.status})}return`Run ${t} timed out after polling ${String(Dt)} times.`}function Yn({results:e,runId:t,status:r}){let n=[`Run ${t}: ${r.toUpperCase()}`];return e.forEach(o=>{n.push(` ${o.status}: ${String(o.passCount)} passed, ${String(o.failCount)} failed (${String(o.duration??0)}ms)`),o.summary!=null&&n.push(` Summary: ${o.summary}`),o.traceEntries.forEach(s=>{let l=s.status==="passed"?"\u2713":"\u2717",c=s.detail==null?"":` \u2014 ${s.detail}`;n.push(` ${l} ${s.title} (${String(s.duration)}ms)${c}`),s.assertions.forEach(i=>{let d=i.status==="passed"?"\u2713":"\u2717",m=i.detail==null?"":` \u2014 ${i.detail}`;n.push(` ${d} ${i.description}${m}`)})})}),n.join(`
|
|
241
|
+
`)}function Kn(e){return new Promise(t=>{setTimeout(t,e)})}import{z as Qn}from"zod";function Lt(){return Xn()}function _t(){return Zn()}function _(e,t){let r=Qn.toJSONSchema(t);return`### ${e}
|
|
242
242
|
|
|
243
243
|
\`\`\`json
|
|
244
244
|
${JSON.stringify(r,null,2)}
|
|
@@ -250,7 +250,7 @@ The graph schema is documented below as JSON Schema (generated from Zod at runti
|
|
|
250
250
|
|
|
251
251
|
The graph lives at \`.ripplo/graph.json\`. The top-level shape is the StateGraph schema below. One graph per project.`,`## Graph Schema
|
|
252
252
|
|
|
253
|
-
`+_("StateGraph",
|
|
253
|
+
`+_("StateGraph",Y),`## Preconditions (summary)
|
|
254
254
|
|
|
255
255
|
Preconditions are named capabilities declared at the graph level and referenced by name from states. They represent what must be true before entering a state (auth, data, feature flags, etc.).
|
|
256
256
|
|
|
@@ -263,7 +263,7 @@ Dependencies (\`depends\`) are resolved via topological sort. Cycles are rejecte
|
|
|
263
263
|
|
|
264
264
|
Use \`get_precondition_schema\` for full precondition API schemas, setup guide, and framework-specific auth examples.`].join(`
|
|
265
265
|
|
|
266
|
-
`)}function Zn(){return["# Precondition API \u2014 Full Reference\n\nPreconditions are named capabilities declared at the graph level and referenced by name from states.\n\nRipplo does NOT run any business logic. The user's app exposes a Precondition API with three endpoints, and Ripplo sends the precondition name in the request body.\n\n### Execution flow for each precondition:\n1. **Check**: `POST {preconditionApiPath}/check` \u2014 is it already satisfied?\n2. **Execute** (if not satisfied): `PUT {preconditionApiPath}/execute` \u2014 satisfy it\n3. Set-Cookie headers from execute responses are automatically captured and injected into the browser. All cookie attributes (Domain, Path, SameSite, HttpOnly, Secure, Expires, Max-Age) are preserved. Always include `Path=/` and an appropriate `SameSite` value on auth cookies.\n4. If the execute response includes `navigateTo`, the browser navigates to that URL after all preconditions complete (sets the starting page for the test)\n5. **Teardown** (after run): `PUT {preconditionApiPath}/teardown` \u2014 clean up (fire-and-forget)\n\nDependencies (`depends`) are resolved via topological sort with deduplication. Cycles are rejected at validation time.",_("Precondition (graph-level declaration)",L),_("Precondition Check/Execute Request",
|
|
266
|
+
`)}function Zn(){return["# Precondition API \u2014 Full Reference\n\nPreconditions are named capabilities declared at the graph level and referenced by name from states.\n\nRipplo does NOT run any business logic. The user's app exposes a Precondition API with three endpoints, and Ripplo sends the precondition name in the request body.\n\n### Execution flow for each precondition:\n1. **Check**: `POST {preconditionApiPath}/check` \u2014 is it already satisfied?\n2. **Execute** (if not satisfied): `PUT {preconditionApiPath}/execute` \u2014 satisfy it\n3. Set-Cookie headers from execute responses are automatically captured and injected into the browser. All cookie attributes (Domain, Path, SameSite, HttpOnly, Secure, Expires, Max-Age) are preserved. Always include `Path=/` and an appropriate `SameSite` value on auth cookies.\n4. If the execute response includes `navigateTo`, the browser navigates to that URL after all preconditions complete (sets the starting page for the test)\n5. **Teardown** (after run): `PUT {preconditionApiPath}/teardown` \u2014 clean up (fire-and-forget)\n\nDependencies (`depends`) are resolved via topological sort with deduplication. Cycles are rejected at validation time.",_("Precondition (graph-level declaration)",L),_("Precondition Check/Execute Request",xe),_("Check Response",te),_("Execute Response",re),_("Teardown Request",ke),_("Teardown Response",ve),ta()].join(`
|
|
267
267
|
|
|
268
268
|
`)}function ea(){return`### Framework-specific auth examples
|
|
269
269
|
|
|
@@ -423,17 +423,17 @@ app.put("/api/test/preconditions/teardown", async (req, res) => {
|
|
|
423
423
|
}
|
|
424
424
|
res.json({ success: true });
|
|
425
425
|
});
|
|
426
|
-
\`\`\``}import
|
|
426
|
+
\`\`\``}import Ie from"fs";import $e from"path";import oa from"fs";import _u from"path";import{z as R}from"zod";var Ce=R.object({edges:R.array(R.object({from:R.string(),to:R.string(),workflow:R.string()})),preconditions:R.record(R.string(),R.unknown()).optional(),resetPrecondition:R.string().optional(),states:R.record(R.string(),R.object({preconditions:R.array(R.string()),route:R.string()})),version:R.literal(3)});function Bt(e){let t=oa.readFileSync(e,"utf8"),r=JSON.parse(t);return Ce.parse(r)}function Mt(e){return{edges:e.edges,preconditions:e.preconditions??{},resetPrecondition:e.resetPrecondition,states:e.states,version:e.version}}function Gt(e){let t=na(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=Ce.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(m=>({message:m.message,path:m.path.join(".")})),valid:!1};let n=Mt(r.data),o=Te(n);if(!o.success)return{errors:o.errors,valid:!1};let s=[...Ne(o.data)],l=$e.join($e.dirname(e),"workflows"),c=Ie.existsSync(l),i=new Set(o.data.edges.map(d=>d.workflow));return o.data.edges.forEach((d,m)=>{if(!c){s.push({message:`References workflow "${d.workflow}" but workflows directory does not exist`,path:`edges[${String(m)}].workflow`});return}let $=$e.join(l,`${d.workflow}.json`);Ie.existsSync($)||s.push({message:`References workflow "${d.workflow}" but no file exists at workflows/${d.workflow}.json`,path:`edges[${String(m)}].workflow`})}),c&&Ie.readdirSync(l).filter(d=>d.endsWith(".json")).forEach(d=>{let m=d.replace(/\.json$/,"");i.has(m)||s.push({message:`Workflow file "workflows/${d}" is not referenced by any edge`,path:`workflows/${d}`})}),{errors:s,valid:s.length===0}}function na(e){try{return{data:Bt(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}import ie from"fs";import zt from"path";import{z as C}from"zod";var aa=C.object({baseUrl:C.string().optional(),cloudBaseUrl:C.string(),preconditionApiPath:C.string().optional(),projectId:C.string()});function Ht(e){let t=zt.join(e,".ripplo","settings.json");if(!ie.existsSync(t))return{errors:[{message:".ripplo/settings.json not found",path:""}],valid:!1,warnings:[]};let r=la(t);if(!r.success)return{errors:[{message:r.error,path:"settings.json"}],valid:!1,warnings:[]};let n=aa.safeParse(r.data);if(!n.success)return{errors:n.error.issues.map(c=>({message:c.message,path:c.path.join(".")})),valid:!1,warnings:[]};let o=[],s=zt.join(e,".ripplo","graph.json");return ie.existsSync(s)&&ia(s)&&(n.data.preconditionApiPath==null||n.data.preconditionApiPath.length===0)&&o.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:o}}var sa=C.object({states:C.record(C.string(),C.object({preconditions:C.array(C.string())}))});function ia(e){try{let t=ie.readFileSync(e,"utf8"),r=JSON.parse(t),n=sa.safeParse(r);return n.success?Object.values(n.data.states).some(o=>o.preconditions.length>0):!1}catch{return!1}}function la(e){try{let t=ie.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 Jt={assertAttribute:"assertion",assertCount:"assertion",assertDisabled:"assertion",assertEnabled:"assertion",assertNotVisible:"assertion",assertText:"assertion",assertUrl:"assertion",assertValue:"assertion",assertVisible:"assertion",check:"action",click:"action",dblclick:"action",drag:"action",extractText:"other",fail:"other",fill:"action",focus:"action",forEach:"controlFlow",goto:"other",group:"controlFlow",hover:"action",if:"controlFlow",loop:"controlFlow",parallel:"controlFlow",press:"action",screenshot:"other",scrollIntoView:"action",select:"action",setVariable:"other",setViewport:"other",try:"controlFlow",type:"action",uncheck:"action",upload:"action",wait:"other",waitFor:"other",waitForRequest:"other",waitForResponse:"other",waitForUrl:"other"},le=new Set,Ee=new Set;Object.entries(Jt).forEach(([e,t])=>{t==="assertion"&&le.add(e),t==="action"&&Ee.add(e)});function Yt(e){let t=ae(e),{spec:r}=t,n=ma(r),o=ha(r),s=ca({orderedNodes:n,spec:r,terminalNodes:o}),l=Sa({flags:s,orderedNodes:n,terminalNodes:o,wf:t});return{flags:s,report:l}}function ca({orderedNodes:e,spec:t,terminalNodes:r}){let n=e.filter(c=>le.has(c.type)).length,o=e.filter(c=>Ee.has(c.type)).length,s=Object.keys(t.nodes).length,l=r.some(c=>le.has(c.type));return[...da(n),...ua(l,n),...pa(o),...fa(n,s)]}function da(e){return e===0?["NO_ASSERTIONS: The workflow has zero assertion nodes. It cannot verify its expectedOutcome."]:[]}function ua(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 pa(e){return e===0?["NO_INTERACTIONS: The workflow has zero interaction nodes (click, fill, select, etc.). It does not exercise any user action."]:[]}function fa(e,t){if(e>0&&t>3){let r=e/t;if(r<.15){let n=String(Math.round(r*100));return[`LOW_ASSERTION_RATIO: Only ${String(e)}/${String(t)} nodes are assertions (${n}%). Consider adding more verification.`]}}return[]}function ma(e){let t=new Set,r=[];function n(o){if(t.has(o))return;t.add(o);let s=e.nodes[o];s!=null&&(r.push(s),s.next!=null&&n(s.next),Kt(s).forEach(l=>{n(l)}))}return n(e.entryNode),r}function Kt(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":return[]}}function ha(e){return Object.values(e.nodes).filter(t=>t.next!=null?!1:Kt(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 I(e){return e.type==="static"?`"${e.value}"`:`{{${e.name}}}`}function ga(e){return e.type==="static"?String(e.value):`{{${e.name}}}`}var Qt=new Set,Xt=new Set;Object.entries(Jt).forEach(([e,t])=>{t==="controlFlow"&&Qt.add(e),t==="assertion"&&Xt.add(e)});function Zt(e){let t=`[${e.type}]`;return Qt.has(e.type)?ba(e,t):Xt.has(e.type)?wa(e,t):ya(e,t)}function ya(e,t){let r=e;switch(r.type){case"goto":return`${t} Navigate to ${I(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 ${I(r.value)}`;case"select":return`${t} Select ${I(r.value)} in ${w(r.locator)}`;case"press":return`${t} Press "${r.key}"`;case"waitFor":{let n=r.state==null?"":` to be ${r.state}`;return`${t} Wait for ${w(r.locator)}${n}`}case"waitForUrl":return`${t} Wait for URL ${r.operator} ${I(r.expected)}`;case"waitForResponse":return`${t} Wait for response matching ${I(r.urlPattern)}`;case"waitForRequest":return`${t} Wait for request matching ${I(r.urlPattern)}`;case"extractText":return`${t} Extract text from ${w(r.locator)} into $${r.variable}`;case"screenshot":return`${t} screenshot`;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 ${I(r.value)} into ${w(r.locator)}`;case"focus":return`${t} Focus ${w(r.locator)}`}}function wa(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} ${I(r.expected)}`;case"assertUrl":return`${t} Assert URL ${r.operator} ${I(r.expected)}`;case"assertCount":return`${t} Assert count of ${w(r.locator)} ${r.operator} ${ga(r.expected)}`;case"assertValue":return`${t} Assert value of ${w(r.locator)} ${r.operator} ${I(r.expected)}`;case"assertAttribute":return`${t} Assert attribute "${r.attribute}" of ${w(r.locator)} ${r.operator} ${I(r.expected)}`;case"assertEnabled":return`${t} Assert enabled: ${w(r.locator)}`;case"assertDisabled":return`${t} Assert disabled: ${w(r.locator)}`}}function ba(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 Sa({flags:e,orderedNodes:t,terminalNodes:r,wf:n}){let o=t.filter(c=>le.has(c.type)).length,s=t.filter(c=>Ee.has(c.type)).length;return[Ra(n),xa(t),ka(r),va({actionCount:s,assertionCount:o,orderedNodes:t,terminalNodes:r}),...Pa(e),Ta].join(`
|
|
427
427
|
`)}function Ra(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(n=>`- ${n}`).join(`
|
|
428
428
|
`);t.push(`**Additional Checks:**
|
|
429
429
|
${r}`)}return t.join(`
|
|
430
|
-
`)}function
|
|
431
|
-
`)}function
|
|
430
|
+
`)}function xa(e){return["","## Workflow Steps (execution order)",...e.map((r,n)=>`${String(n+1)}. ${Zt(r)}`)].join(`
|
|
431
|
+
`)}function ka(e){return["","## Terminal Nodes (where the workflow ends)",...e.map(r=>`- ${r.id}: ${Zt(r)}`)].join(`
|
|
432
432
|
`)}function va({actionCount:e,assertionCount:t,orderedNodes:r,terminalNodes:n}){return["","## Statistics",`- Total nodes: ${String(r.length)}`,`- Action nodes: ${String(e)}`,`- Assertion nodes: ${String(t)}`,`- Terminal nodes: ${String(n.length)}`].join(`
|
|
433
433
|
`)}function Pa(e){return e.length===0?[]:[["","## Flags",...e.map(r=>`- ${r}`)].join(`
|
|
434
434
|
`)]}var Ta=["","## 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(`
|
|
435
|
-
`);function tr(e){let t=Na(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=
|
|
436
|
-
`)}function
|
|
435
|
+
`);function tr(e){let t=Na(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=K.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(l=>({message:l.message,path:l.path.join(".")})),valid:!1};let n=Z(r.data.spec);if(!n.success)return{errors:n.errors,valid:!1};let o=Aa(n.data);return o.length>0?{errors:o,valid:!1}:{errors:[],valid:!0}}function Na(e){try{return{data:ae(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}function Aa(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,c])=>{er(c).forEach(i=>{t.has(i.id)||r.push({message:`references non-existent node "${i.id}"`,path:`nodes.${l}.${i.field}`})})});let n=new Set,o=t.has(e.entryNode)?[e.entryNode]:[],s=0;for(;s<o.length;){let l=o[s];if(s+=1,l==null||n.has(l))continue;n.add(l);let c=e.nodes[l];c!=null&&er(c).forEach(i=>{n.has(i.id)||o.push(i.id)})}return t.forEach(l=>{n.has(l)||r.push({message:"unreachable from entryNode",path:`nodes.${l}`})}),r}function er(e){let t=[];typeof e.next=="string"&&t.push({field:"next",id:e.next}),["consequent","alternate","body","catch","finally","nodes"].forEach(o=>{let s=e[o];Array.isArray(s)&&s.forEach((l,c)=>{typeof l=="string"&&t.push({field:`${o}[${String(c)}]`,id:l})})});let n=e.branches;return Array.isArray(n)&&n.forEach((o,s)=>{Array.isArray(o)&&o.forEach((l,c)=>{typeof l=="string"&&t.push({field:`branches[${String(s)}][${String(c)}]`,id:l})})}),t}import rr from"fs";import Ca from"path";function or(e){let t=Ia(),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(`
|
|
436
|
+
`)}function Ia(){let e=Ca.join(process.cwd(),".ripplo","workflows");return rr.existsSync(e)?!rr.readdirSync(e).some(r=>r.endsWith(".json")):!0}import ar from"fs";import $a from"path";var z="## 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.",nr="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 sr(e){return`# Debug a Failing Workflow
|
|
437
437
|
|
|
438
438
|
${Ea(e)}
|
|
439
439
|
|
|
@@ -496,7 +496,7 @@ Find it in \`.ripplo/workflows/\`. If the name doesn't match exactly, look for a
|
|
|
496
496
|
${t.map(r=>`- ${r}`).join(`
|
|
497
497
|
`)}
|
|
498
498
|
|
|
499
|
-
Ask the user which workflow to debug, or pick the one most likely to be failing.`}function ja(){let e
|
|
499
|
+
Ask the user which workflow to debug, or pick the one most likely to be failing.`}function ja(){let e=$a.join(process.cwd(),".ripplo","workflows");return ar.existsSync(e)?ar.readdirSync(e).filter(t=>t.endsWith(".json")).map(t=>t.replace(/\.json$/,"")):[]}function ir(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?Va({baseUrlInstruction:t,scope:e.scope}):[Fa(t),Ua(),Da(),Wa(),_a(),qa(),La(),Ba()].join(`
|
|
500
500
|
|
|
501
501
|
`)}function Va(e){let t=Oa(e.scope);return`# Ripplo Explore (Scoped to Changes)
|
|
502
502
|
|
|
@@ -841,17 +841,17 @@ Multiple workflows run simultaneously. Every precondition executor must create *
|
|
|
841
841
|
|
|
842
842
|
### Step 3: Verify
|
|
843
843
|
|
|
844
|
-
Ensure \`preconditionApiPath\` is set in \`.ripplo/settings.json\` to point to the endpoints (e.g., \`"preconditionApiPath": "http://localhost:3000/api/test/preconditions"\`). Can be a relative path (appended to \`baseUrl\`) or an absolute URL. The precondition runner will call these endpoints during test execution.`}function Ba(){return'## Rules\n\n- Value references use `"type": "static"`, never `"type": "literal"`\n- Goto node URLs must be relative paths (`/projects`), never absolute URLs\n- Graph files use `version: 3`, workflow spec files use `version: 2`\n- IDs must match their object keys (state ID = its key in the states object)\n- Call `validate_graph` / `validate_workflow` after every edit \u2014 don\'t batch without validation\n- Workflow filenames should match their edge IDs\n- Prefer locator stability: testId > role > label > text > css\n- State names should be descriptive and encode what\'s unique: `projects-list-empty`, `create-workflow-dialog-open`, `member-role-updated` \u2014 never `state-1` or `page-loaded`'}function lr(){let e=new Ma({name:"ripplo",version:"0.1.0"});return Ga(e),za(e),Ha(e),Ja(e),
|
|
844
|
+
Ensure \`preconditionApiPath\` is set in \`.ripplo/settings.json\` to point to the endpoints (e.g., \`"preconditionApiPath": "http://localhost:3000/api/test/preconditions"\`). Can be a relative path (appended to \`baseUrl\`) or an absolute URL. The precondition runner will call these endpoints during test execution.`}function Ba(){return'## Rules\n\n- Value references use `"type": "static"`, never `"type": "literal"`\n- Goto node URLs must be relative paths (`/projects`), never absolute URLs\n- Graph files use `version: 3`, workflow spec files use `version: 2`\n- IDs must match their object keys (state ID = its key in the states object)\n- Call `validate_graph` / `validate_workflow` after every edit \u2014 don\'t batch without validation\n- Workflow filenames should match their edge IDs\n- Prefer locator stability: testId > role > label > text > css\n- State names should be descriptive and encode what\'s unique: `projects-list-empty`, `create-workflow-dialog-open`, `member-role-updated` \u2014 never `state-1` or `page-loaded`'}function lr(){let e=new Ma({name:"ripplo",version:"0.1.0"});return Ga(e),za(e),Ha(e),Ja(e),Ya(e),e}function Ga(e){e.registerTool("validate_workflow",{description:"Validate a workflow spec file. MUST be called after every edit to a .ripplo/workflows/*.json file. Returns validation errors if the spec is invalid.",inputSchema:{file:T.string().describe("Absolute path to the workflow JSON file")}},t=>{let r=tr(t.file);if(r.valid)return x("Valid");let n=r.errors.map(o=>`${o.path}: ${o.message}`);return x(`Invalid:
|
|
845
845
|
${n.join(`
|
|
846
|
-
`)}`)}),e.registerTool("get_spec_documentation",{description:"Returns a compact summary of all node types with their fields, locator types, operators, and value ref formats. Start here before creating or editing workflow specs. Use get_node_schema for full JSON Schema of specific types."},()=>
|
|
846
|
+
`)}`)}),e.registerTool("get_spec_documentation",{description:"Returns a compact summary of all node types with their fields, locator types, operators, and value ref formats. Start here before creating or editing workflow specs. Use get_node_schema for full JSON Schema of specific types."},()=>x(Vt())),e.registerTool("get_node_schema",{description:"Returns full JSON Schemas for one or more node types. Pass an array of type names (e.g. ['click', 'fill', 'assertText']). Includes referenced sub-schemas (locators, operators, value refs) automatically. Call after get_spec_documentation when you need exact node shapes.",inputSchema:{nodeTypes:T.array(T.string()).min(1).describe("Node type names to get schemas for (e.g. ['click', 'fill'])")}},t=>x(Ot({nodeTypes:t.nodeTypes}))),e.registerTool("get_spec_patterns",{description:"Returns full JSON Schemas for shared building blocks: locators, conditions, value references, operators, and variables. Plus best practices for writing specs."},()=>x(Ft())),e.registerTool("review_workflow",{description:"Review the semantic quality of a workflow spec. Call AFTER validate_workflow passes. Analyzes whether the workflow actually tests what it claims \u2014 checks assertions against expectedOutcome, verifies the flow exercises the described behavior, and flags gaps. Returns a structured report for you to review and act on.",inputSchema:{file:T.string().describe("Absolute path to the workflow JSON file")}},t=>{let r=Yt(t.file),n=r.flags.length===0?"PASS":"NEEDS REVIEW";return x(`${n}
|
|
847
847
|
|
|
848
|
-
${r.report}`)})}function za(e){e.registerTool("get_graph_documentation",{description:"Returns the state graph schema and a summary of the precondition system. Start here before creating or editing graph files. Use get_precondition_schema for full precondition API details."},()=>
|
|
848
|
+
${r.report}`)})}function za(e){e.registerTool("get_graph_documentation",{description:"Returns the state graph schema and a summary of the precondition system. Start here before creating or editing graph files. Use get_precondition_schema for full precondition API details."},()=>x(Lt())),e.registerTool("get_precondition_schema",{description:"Returns full precondition API schemas, setup guide, framework-specific auth examples, and best practices. Call when implementing precondition endpoints."},()=>x(_t())),e.registerTool("validate_graph",{description:"Validate the state graph file. MUST be called after every edit to .ripplo/graph.json.",inputSchema:{file:T.string().describe("Absolute path to the graph JSON file")}},t=>{let r=Gt(t.file);if(r.valid)return x("Valid");let n=r.errors.map(o=>`${o.path}: ${o.message}`);return x(`Invalid:
|
|
849
849
|
${n.join(`
|
|
850
850
|
`)}`)}),e.registerTool("validate_settings",{description:"Validate .ripplo/settings.json. Checks required fields and warns if preconditionApiPath is missing when the graph has preconditions. Call after editing settings or after adding preconditions to the graph.",inputSchema:{dir:T.string().optional().describe("Working directory (defaults to cwd)")}},t=>{let r=Ht(t.dir??process.cwd());if(r.valid){let o=r.warnings.length>0?`
|
|
851
851
|
Warnings:
|
|
852
852
|
${r.warnings.join(`
|
|
853
|
-
`)}`:"";return
|
|
853
|
+
`)}`:"";return x(`Valid${o}`)}let n=r.errors.map(o=>`${o.path}: ${o.message}`);return x(`Invalid:
|
|
854
854
|
${n.join(`
|
|
855
|
-
`)}`)})}function Ha(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:ir({baseUrl:t.baseUrl,scope:t.scope}),type:"text"},role:"user"}]}))}function Ja(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=>Et(async r=>{try{let n=await qt({config:r,slugs:t.slugs});return
|
|
855
|
+
`)}`)})}function Ha(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:ir({baseUrl:t.baseUrl,scope:t.scope}),type:"text"},role:"user"}]}))}function Ja(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=>Et(async r=>{try{let n=await qt({config:r,slugs:t.slugs});return x(n.summary)}catch(n){let o=n instanceof Error?n.message:String(n);return x(`ERROR: ${o}`)}}))}function Ya(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(`
|
|
856
856
|
`):`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:[or(t.flow??"the most important user flow"),"",z].join(`
|
|
857
|
-
`),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:sr(t.workflow),type:"text"},role:"user"}]}))}async function Qa(){let e=lr(),t=new
|
|
857
|
+
`),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:sr(t.workflow),type:"text"},role:"user"}]}))}async function Qa(){let e=lr(),t=new Ka;await e.connect(t)}Qa().catch(e=>{process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ripplo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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/graph": "^0.0.0",
|
|
46
|
-
"@ripplo/
|
|
47
|
-
"@ripplo/
|
|
46
|
+
"@ripplo/runtime": "^0.0.0",
|
|
47
|
+
"@ripplo/spec": "^0.0.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"dev": "tsx src/index.ts",
|