ripplo 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -1,56 +1,56 @@
1
1
  #!/usr/bin/env node
2
- var Co=Object.create;var gt=Object.defineProperty;var Ao=Object.getOwnPropertyDescriptor;var No=Object.getOwnPropertyNames;var Eo=Object.getPrototypeOf,Io=Object.prototype.hasOwnProperty;var Re=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var $o=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of No(t))!Io.call(e,n)&&n!==r&&gt(e,n,{get:()=>t[n],enumerable:!(o=Ao(t,n))||o.enumerable});return e};var jo=(e,t,r)=>(r=e!=null?Co(Eo(e)):{},$o(t||!e||!e.__esModule?gt(r,"default",{value:e,enumerable:!0}):r,e));var Fr=Re($e=>{"use strict";Object.defineProperty($e,"__esModule",{value:!0});$e.timingSafeEqual=void 0;function Ur(e,t=""){if(!e)throw new Error(t)}function xs(e,t){if(e.byteLength!==t.byteLength)return!1;e instanceof DataView||(e=new DataView(ArrayBuffer.isView(e)?e.buffer:e)),t instanceof DataView||(t=new DataView(ArrayBuffer.isView(t)?t.buffer:t)),Ur(e instanceof DataView),Ur(t instanceof DataView);let r=e.byteLength,o=0,n=-1;for(;++n<r;)o|=e.getUint8(n)^t.getUint8(n);return o===0}$e.timingSafeEqual=xs});var Dr=Re(q=>{"use strict";var Rs=q&&q.__extends||(function(){var e=function(t,r){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(o,n){o.__proto__=n}||function(o,n){for(var a in n)n.hasOwnProperty(a)&&(o[a]=n[a])},e(t,r)};return function(t,r){e(t,r);function o(){this.constructor=t}t.prototype=r===null?Object.create(r):(o.prototype=r.prototype,new o)}})();Object.defineProperty(q,"__esModule",{value:!0});var N=256,ct=(function(){function e(t){t===void 0&&(t="="),this._paddingCharacter=t}return e.prototype.encodedLength=function(t){return this._paddingCharacter?(t+2)/3*4|0:(t*8+5)/6|0},e.prototype.encode=function(t){for(var r="",o=0;o<t.length-2;o+=3){var n=t[o]<<16|t[o+1]<<8|t[o+2];r+=this._encodeByte(n>>>18&63),r+=this._encodeByte(n>>>12&63),r+=this._encodeByte(n>>>6&63),r+=this._encodeByte(n>>>0&63)}var a=t.length-o;if(a>0){var n=t[o]<<16|(a===2?t[o+1]<<8:0);r+=this._encodeByte(n>>>18&63),r+=this._encodeByte(n>>>12&63),a===2?r+=this._encodeByte(n>>>6&63):r+=this._paddingCharacter||"",r+=this._paddingCharacter||""}return r},e.prototype.maxDecodedLength=function(t){return this._paddingCharacter?t/4*3|0:(t*6+7)/8|0},e.prototype.decodedLength=function(t){return this.maxDecodedLength(t.length-this._getPaddingLength(t))},e.prototype.decode=function(t){if(t.length===0)return new Uint8Array(0);for(var r=this._getPaddingLength(t),o=t.length-r,n=new Uint8Array(this.maxDecodedLength(o)),a=0,i=0,d=0,l=0,p=0,g=0,f=0;i<o-4;i+=4)l=this._decodeChar(t.charCodeAt(i+0)),p=this._decodeChar(t.charCodeAt(i+1)),g=this._decodeChar(t.charCodeAt(i+2)),f=this._decodeChar(t.charCodeAt(i+3)),n[a++]=l<<2|p>>>4,n[a++]=p<<4|g>>>2,n[a++]=g<<6|f,d|=l&N,d|=p&N,d|=g&N,d|=f&N;if(i<o-1&&(l=this._decodeChar(t.charCodeAt(i)),p=this._decodeChar(t.charCodeAt(i+1)),n[a++]=l<<2|p>>>4,d|=l&N,d|=p&N),i<o-2&&(g=this._decodeChar(t.charCodeAt(i+2)),n[a++]=p<<4|g>>>2,d|=g&N),i<o-3&&(f=this._decodeChar(t.charCodeAt(i+3)),n[a++]=g<<6|f,d|=f&N),d!==0)throw new Error("Base64Coder: incorrect characters for decoding");return n},e.prototype._encodeByte=function(t){var r=t;return r+=65,r+=25-t>>>8&6,r+=51-t>>>8&-75,r+=61-t>>>8&-15,r+=62-t>>>8&3,String.fromCharCode(r)},e.prototype._decodeChar=function(t){var r=N;return r+=(42-t&t-44)>>>8&-N+t-43+62,r+=(46-t&t-48)>>>8&-N+t-47+63,r+=(47-t&t-58)>>>8&-N+t-48+52,r+=(64-t&t-91)>>>8&-N+t-65+0,r+=(96-t&t-123)>>>8&-N+t-97+26,r},e.prototype._getPaddingLength=function(t){var r=0;if(this._paddingCharacter){for(var o=t.length-1;o>=0&&t[o]===this._paddingCharacter;o--)r++;if(t.length<4||r>2)throw new Error("Base64Coder: incorrect padding")}return r},e})();q.Coder=ct;var Se=new ct;function Ps(e){return Se.encode(e)}q.encode=Ps;function Ts(e){return Se.decode(e)}q.decode=Ts;var _r=(function(e){Rs(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype._encodeByte=function(r){var o=r;return o+=65,o+=25-r>>>8&6,o+=51-r>>>8&-75,o+=61-r>>>8&-13,o+=62-r>>>8&49,String.fromCharCode(o)},t.prototype._decodeChar=function(r){var o=N;return o+=(44-r&r-46)>>>8&-N+r-45+62,o+=(94-r&r-96)>>>8&-N+r-95+63,o+=(47-r&r-58)>>>8&-N+r-48+52,o+=(64-r&r-91)>>>8&-N+r-65+0,o+=(96-r&r-123)>>>8&-N+r-97+26,o},t})(ct);q.URLSafeCoder=_r;var Lr=new _r;function Cs(e){return Lr.encode(e)}q.encodeURLSafe=Cs;function As(e){return Lr.decode(e)}q.decodeURLSafe=As;q.encodedLength=function(e){return Se.encodedLength(e)};q.maxDecodedLength=function(e){return Se.maxDecodedLength(e)};q.decodedLength=function(e){return Se.decodedLength(e)}});var qr=Re((Wr,je)=>{"use strict";(function(e,t){var r={};t(r);var o=r.default;for(var n in r)o[n]=r[n];typeof je=="object"&&typeof je.exports=="object"?je.exports=o:typeof define=="function"&&define.amd?define(function(){return o}):e.sha256=o})(Wr,function(e){"use strict";e.__esModule=!0,e.digestLength=32,e.blockSize=64;var t=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]);function r(f,c,m,y,A){for(var k,v,S,L,E,T,H,I,O,$,me,he,xe;A>=64;){for(k=c[0],v=c[1],S=c[2],L=c[3],E=c[4],T=c[5],H=c[6],I=c[7],$=0;$<16;$++)me=y+$*4,f[$]=(m[me]&255)<<24|(m[me+1]&255)<<16|(m[me+2]&255)<<8|m[me+3]&255;for($=16;$<64;$++)O=f[$-2],he=(O>>>17|O<<15)^(O>>>19|O<<13)^O>>>10,O=f[$-15],xe=(O>>>7|O<<25)^(O>>>18|O<<14)^O>>>3,f[$]=(he+f[$-7]|0)+(xe+f[$-16]|0);for($=0;$<64;$++)he=(((E>>>6|E<<26)^(E>>>11|E<<21)^(E>>>25|E<<7))+(E&T^~E&H)|0)+(I+(t[$]+f[$]|0)|0)|0,xe=((k>>>2|k<<30)^(k>>>13|k<<19)^(k>>>22|k<<10))+(k&v^k&S^v&S)|0,I=H,H=T,T=E,E=L+he|0,L=S,S=v,v=k,k=he+xe|0;c[0]+=k,c[1]+=v,c[2]+=S,c[3]+=L,c[4]+=E,c[5]+=T,c[6]+=H,c[7]+=I,y+=64,A-=64}return y}var o=(function(){function f(){this.digestLength=e.digestLength,this.blockSize=e.blockSize,this.state=new Int32Array(8),this.temp=new Int32Array(64),this.buffer=new Uint8Array(128),this.bufferLength=0,this.bytesHashed=0,this.finished=!1,this.reset()}return f.prototype.reset=function(){return this.state[0]=1779033703,this.state[1]=3144134277,this.state[2]=1013904242,this.state[3]=2773480762,this.state[4]=1359893119,this.state[5]=2600822924,this.state[6]=528734635,this.state[7]=1541459225,this.bufferLength=0,this.bytesHashed=0,this.finished=!1,this},f.prototype.clean=function(){for(var c=0;c<this.buffer.length;c++)this.buffer[c]=0;for(var c=0;c<this.temp.length;c++)this.temp[c]=0;this.reset()},f.prototype.update=function(c,m){if(m===void 0&&(m=c.length),this.finished)throw new Error("SHA256: can't update because hash was finished.");var y=0;if(this.bytesHashed+=m,this.bufferLength>0){for(;this.bufferLength<64&&m>0;)this.buffer[this.bufferLength++]=c[y++],m--;this.bufferLength===64&&(r(this.temp,this.state,this.buffer,0,64),this.bufferLength=0)}for(m>=64&&(y=r(this.temp,this.state,c,y,m),m%=64);m>0;)this.buffer[this.bufferLength++]=c[y++],m--;return this},f.prototype.finish=function(c){if(!this.finished){var m=this.bytesHashed,y=this.bufferLength,A=m/536870912|0,k=m<<3,v=m%64<56?64:128;this.buffer[y]=128;for(var S=y+1;S<v-8;S++)this.buffer[S]=0;this.buffer[v-8]=A>>>24&255,this.buffer[v-7]=A>>>16&255,this.buffer[v-6]=A>>>8&255,this.buffer[v-5]=A>>>0&255,this.buffer[v-4]=k>>>24&255,this.buffer[v-3]=k>>>16&255,this.buffer[v-2]=k>>>8&255,this.buffer[v-1]=k>>>0&255,r(this.temp,this.state,this.buffer,0,v),this.finished=!0}for(var S=0;S<8;S++)c[S*4+0]=this.state[S]>>>24&255,c[S*4+1]=this.state[S]>>>16&255,c[S*4+2]=this.state[S]>>>8&255,c[S*4+3]=this.state[S]>>>0&255;return this},f.prototype.digest=function(){var c=new Uint8Array(this.digestLength);return this.finish(c),c},f.prototype._saveState=function(c){for(var m=0;m<this.state.length;m++)c[m]=this.state[m]},f.prototype._restoreState=function(c,m){for(var y=0;y<this.state.length;y++)this.state[y]=c[y];this.bytesHashed=m,this.finished=!1,this.bufferLength=0},f})();e.Hash=o;var n=(function(){function f(c){this.inner=new o,this.outer=new o,this.blockSize=this.inner.blockSize,this.digestLength=this.inner.digestLength;var m=new Uint8Array(this.blockSize);if(c.length>this.blockSize)new o().update(c).finish(m).clean();else for(var y=0;y<c.length;y++)m[y]=c[y];for(var y=0;y<m.length;y++)m[y]^=54;this.inner.update(m);for(var y=0;y<m.length;y++)m[y]^=106;this.outer.update(m),this.istate=new Uint32Array(8),this.ostate=new Uint32Array(8),this.inner._saveState(this.istate),this.outer._saveState(this.ostate);for(var y=0;y<m.length;y++)m[y]=0}return f.prototype.reset=function(){return this.inner._restoreState(this.istate,this.inner.blockSize),this.outer._restoreState(this.ostate,this.outer.blockSize),this},f.prototype.clean=function(){for(var c=0;c<this.istate.length;c++)this.ostate[c]=this.istate[c]=0;this.inner.clean(),this.outer.clean()},f.prototype.update=function(c){return this.inner.update(c),this},f.prototype.finish=function(c){return this.outer.finished?this.outer.finish(c):(this.inner.finish(c),this.outer.update(c,this.digestLength).finish(c)),this},f.prototype.digest=function(){var c=new Uint8Array(this.digestLength);return this.finish(c),c},f})();e.HMAC=n;function a(f){var c=new o().update(f),m=c.digest();return c.clean(),m}e.hash=a,e.default=a;function i(f,c){var m=new n(f).update(c),y=m.digest();return m.clean(),y}e.hmac=i;function d(f,c,m,y){var A=y[0];if(A===0)throw new Error("hkdf: cannot expand more");c.reset(),A>1&&c.update(f),m&&c.update(m),c.update(y),c.finish(f),y[0]++}var l=new Uint8Array(e.digestLength);function p(f,c,m,y){c===void 0&&(c=l),y===void 0&&(y=32);for(var A=new Uint8Array([1]),k=i(c,f),v=new n(k),S=new Uint8Array(v.digestLength),L=S.length,E=new Uint8Array(y),T=0;T<y;T++)L===S.length&&(d(S,v,m,A),L=0),E[T]=S[L++];return v.clean(),S.fill(0),A.fill(0),E}e.hkdf=p;function g(f,c,m,y){for(var A=new n(f),k=A.digestLength,v=new Uint8Array(4),S=new Uint8Array(k),L=new Uint8Array(k),E=new Uint8Array(y),T=0;T*k<y;T++){var H=T+1;v[0]=H>>>24&255,v[1]=H>>>16&255,v[2]=H>>>8&255,v[3]=H>>>0&255,A.reset(),A.update(c),A.update(v),A.finish(L);for(var I=0;I<k;I++)S[I]=L[I];for(var I=2;I<=m;I++){A.reset(),A.update(L).finish(L);for(var O=0;O<k;O++)S[O]^=L[O]}for(var I=0;I<k&&T*k+I<y;I++)E[T*k+I]=S[I]}for(var T=0;T<k;T++)S[T]=L[T]=0;for(var T=0;T<4;T++)v[T]=0;return A.clean(),E}e.pbkdf2=g})});var Gr=Re(de=>{"use strict";Object.defineProperty(de,"__esModule",{value:!0});de.Webhook=de.WebhookVerificationError=void 0;var Ns=Fr(),Br=Dr(),Es=qr(),Mr=300,dt=class e extends Error{constructor(t){super(t),Object.setPrototypeOf(this,e.prototype),this.name="ExtendableError",this.stack=new Error(t).stack}},te=class e extends dt{constructor(t){super(t),Object.setPrototypeOf(this,e.prototype),this.name="WebhookVerificationError"}};de.WebhookVerificationError=te;var Oe=class e{constructor(t,r){if(!t)throw new Error("Secret can't be empty.");if(r?.format==="raw")t instanceof Uint8Array?this.key=t:this.key=Uint8Array.from(t,o=>o.charCodeAt(0));else{if(typeof t!="string")throw new Error("Expected secret to be of type string");t.startsWith(e.prefix)&&(t=t.substring(e.prefix.length)),this.key=Br.decode(t)}}verify(t,r){let o={};for(let c of Object.keys(r))o[c.toLowerCase()]=r[c];let n=o["webhook-id"],a=o["webhook-signature"],i=o["webhook-timestamp"];if(!a||!n||!i)throw new te("Missing required headers");let d=this.verifyTimestamp(i),p=this.sign(n,d,t).split(",")[1],g=a.split(" "),f=new globalThis.TextEncoder;for(let c of g){let[m,y]=c.split(",");if(m==="v1"&&(0,Ns.timingSafeEqual)(f.encode(y),f.encode(p)))return JSON.parse(t.toString())}throw new te("No matching signature found")}sign(t,r,o){if(typeof o!="string")if(o.constructor.name==="Buffer")o=o.toString();else throw new Error("Expected payload to be of type string or Buffer.");let n=new TextEncoder,a=Math.floor(r.getTime()/1e3),i=n.encode(`${t}.${a}.${o}`);return`v1,${Br.encode(Es.hmac(this.key,i))}`}verifyTimestamp(t){let r=Math.floor(Date.now()/1e3),o=parseInt(t,10);if(isNaN(o))throw new te("Invalid Signature Headers");if(r-o>Mr)throw new te("Message timestamp too old");if(o>r+Mr)throw new te("Message timestamp too new");return new Date(o*1e3)}};de.Webhook=Oe;Oe.prefix="whsec_"});import{StdioServerTransport as ul}from"@modelcontextprotocol/sdk/server/stdio.js";import{McpServer as al}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as B}from"zod";function J({locator:e,page:t}){switch(e.by){case"css":return t.locator(e.value);case"testId":return t.getByTestId(e.value);case"role":return t.getByRole(e.role,{exact:e.exact,name:e.name});case"text":return t.getByText(e.value,{exact:e.exact});case"label":return t.getByLabel(e.value,{exact:e.exact});case"placeholder":return t.getByPlaceholder(e.value);case"altText":return t.getByAltText(e.value)}}import Cu from"@anthropic-ai/sdk";import{z as wt}from"zod";import{z as _o}from"zod";async function ye({page:e,runStartTime:t,targets:r}){let o=e.viewportSize();if(o==null)throw new Error("Page has no viewport set");let[n,a]=await Promise.all([e.screenshot({type:"png"}),Vo(r)]),i=n.toString("base64");return{annotations:a,screenshotBase64:i,snapshotTimestamp:Math.round(performance.now()-t),url:e.url(),viewportHeight:o.height,viewportWidth:o.width}}function Oo(e){return"locator"in e}async function Vo(e){return(await Promise.all(e.map(r=>Uo(r)))).filter(r=>r!=null)}async function Uo(e){if(!Oo(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 We({node:e,page:t}){if(e.type==="assertUrl"||e.type==="waitForUrl")return[{label:e.type,type:"urlBar"}];if(e.type==="drag"){let n=J({locator:e.source,page:t}),a=J({locator:e.target,page:t});return[{label:"drag-source",locator:n,type:"action"},{label:"drag-target",locator:a,type:"action"}]}if(!Fo(e))return[];let r=e.type.startsWith("assert")?"assertion":"action",o=J({locator:e.locator,page:t});return[{label:e.type,locator:o,type:r}]}function Fo(e){return"locator"in e&&e.locator!=null}function u({description:e,execute:t,name:r,schema:o}){let n=_o.toJSONSchema(o,{target:"draft-2020-12"});return{anthropicTool:{description:e,input_schema:{...n,type:"object"},name:r},name:r,async execute(a,i){let d=o.parse(i);return{...await Promise.resolve(t(a,d)),kind:"action"}}}}async function h(e){let t=e.specNode==null?[]:We({node:e.specNode,page:e.page}),r=await ye({page:e.page,runStartTime:e.runStartTime,targets:t}),o=[...e.assertions],n={annotations:r.annotations,assertions:o,detail:e.detail,duration:Math.round(e.duration),nodeId:`agent-step-${String(e.stepIndex)}`,nodeType:e.nodeType,screenshotBase64:r.screenshotBase64,snapshotTimestamp:r.snapshotTimestamp,status:e.status,stepIndex:e.stepIndex,title:e.title,url:r.url,viewportHeight:r.viewportHeight,viewportWidth:r.viewportWidth},a=o.length>0?o.map(i=>`${i.status}: ${i.description} \u2014 ${i.detail??""}`).join(`
3
- `):`${e.status}: ${e.title}`;return{specNode:e.specNode,stepResult:n,toolOutput:a}}var Lo=wt.object({selector:wt.string().describe("CSS selector for the checkbox or radio button")}),bt=u({description:"Assert that a checkbox or radio button is checked",name:"assert_checked",schema:Lo,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).isChecked().catch(()=>!1),n=o?"passed":"failed",a=[{description:`Element ${t.selector} is checked`,detail:o?void 0:"Element is not checked",status:n}],d={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertChecked"};return h({assertions:a,detail:o?"Element is checked":"Element is not checked",duration:performance.now()-r,nodeType:"assertChecked",page:e.page,runStartTime:e.runStartTime,specNode:d,status:n,stepIndex:e.stepIndex,title:`Assert checked: ${t.selector}`})}});import{z as ie}from"zod";var Do=ie.object({httpOnly:ie.boolean().optional().describe("Assert httpOnly flag value"),name:ie.string().describe("Cookie name to check"),sameSite:ie.enum(["Strict","Lax","None"]).optional().describe("Assert sameSite value"),secure:ie.boolean().optional().describe("Assert secure flag value"),value:ie.string().optional().describe("Expected cookie value (substring match)")}),kt=u({description:"Assert properties of a browser cookie. Check existence, value, and security flags (httpOnly, secure, sameSite).",name:"assert_cookie",schema:Do,execute:async(e,t)=>{let r=performance.now(),n=(await e.page.context().cookies()).find(g=>g.name===t.name),a=n==null?[{description:`Cookie "${t.name}" exists`,detail:"Cookie not found",status:"failed"}]:[{description:`Cookie "${t.name}" exists`,detail:void 0,status:"passed"},...Wo({cookie:n,input:t})],i=a.some(g=>g.status==="failed"),d=i?"failed":"passed",p={id:`agent-step-${String(e.stepIndex)}`,name:{type:"static",value:t.name},type:"assertCookie"};return h({assertions:a,detail:i?a.filter(g=>g.status==="failed").map(g=>g.detail).join("; "):"Cookie OK",duration:performance.now()-r,nodeType:"assertCookie",page:e.page,runStartTime:e.runStartTime,specNode:p,status:d,stepIndex:e.stepIndex,title:`Assert cookie: ${t.name}`})}});function Wo({cookie:e,input:t}){return[qo(t.value,e.value),St("httpOnly",t.httpOnly,e.httpOnly),St("secure",t.secure,e.secure),Bo("sameSite",t.sameSite,e.sameSite)].filter(r=>r!=null)}function qo(e,t){if(e==null)return;let r=t.includes(e);return{description:`Cookie value contains "${e}"`,detail:r?void 0:`Got: "${t}"`,status:r?"passed":"failed"}}function St(e,t,r){if(t!=null)return{description:`Cookie ${e} is ${String(t)}`,detail:r===t?void 0:`Got: ${String(r)}`,status:r===t?"passed":"failed"}}function Bo(e,t,r){if(t!=null)return{description:`Cookie ${e} is ${t}`,detail:r===t?void 0:`Got: ${r}`,status:r===t?"passed":"failed"}}import{z as vt}from"zod";var Mo=vt.object({selector:vt.string().describe("CSS selector for the element to check focus on")}),xt=u({description:"Assert that an element has keyboard focus. Use for accessibility tab-order testing.",name:"assert_focused",schema:Mo,execute:async(e,t)=>{let r=performance.now(),n=!!await e.page.locator(t.selector).evaluate("(el) => document.activeElement === el").catch(()=>!1),a=n?"passed":"failed",i=[{description:`Element ${t.selector} is focused`,detail:n?void 0:"Element does not have focus",status:a}],l={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertFocused"};return h({assertions:i,detail:n?"Element is focused":"Element does not have focus",duration:performance.now()-r,nodeType:"assertFocused",page:e.page,runStartTime:e.runStartTime,specNode:l,status:a,stepIndex:e.stepIndex,title:`Assert focused: ${t.selector}`})}});import{z as le}from"zod";var Go=le.object({bodyContains:le.string().optional().describe("Assert response body contains this string"),headerName:le.string().optional().describe("Response header name to check"),headerValue:le.string().optional().describe("Expected value the header should contain"),status:le.number().int().optional().describe("Expected HTTP status code"),urlPattern:le.string().describe("URL substring to match the response against")}),Rt=u({description:"Wait for a network response matching the URL pattern and assert on its status, body, or headers. Useful for verifying API calls and security headers (CSP, CORS).",name:"assert_response",schema:Go,execute:async(e,t)=>{let r=performance.now(),o=await e.page.waitForResponse(p=>p.url().includes(t.urlPattern),{timeout:1e4}),n=[zo(t.status,o.status()),await Ho(t.bodyContains,o),Jo(t.headerName,t.headerValue,o.headers())].filter(p=>p!=null),a=n.length===0?[{description:`Response matching "${t.urlPattern}" received`,detail:void 0,status:"passed"}]:n,i=a.some(p=>p.status==="failed"),l={id:`agent-step-${String(e.stepIndex)}`,type:"assertResponse",urlPattern:{type:"static",value:t.urlPattern}};return h({assertions:a,detail:i?a.filter(p=>p.status==="failed").map(p=>p.detail).join("; "):"Response OK",duration:performance.now()-r,nodeType:"assertResponse",page:e.page,runStartTime:e.runStartTime,specNode:l,status:i?"failed":"passed",stepIndex:e.stepIndex,title:`Assert response: ${t.urlPattern}`})}});function zo(e,t){if(e!=null)return{description:`Response status equals ${String(e)}`,detail:t===e?void 0:`Got: ${String(t)}`,status:t===e?"passed":"failed"}}async function Ho(e,t){if(e==null)return;let o=(await t.text()).includes(e);return{description:`Response body contains "${e}"`,detail:o?void 0:"Not found in response body",status:o?"passed":"failed"}}function Jo(e,t,r){if(e==null||t==null)return;let o=r[e.toLowerCase()],n=o!=null&&o.includes(t);return{description:`Response header "${e}" contains "${t}"`,detail:n?void 0:`Got: "${o??"(missing)"}"`,status:n?"passed":"failed"}}import{z as qe}from"zod";var Ko=qe.object({expected:qe.string().describe("The expected text content"),selector:qe.string().describe("CSS selector for the element to check")}),Pt=u({description:"Assert that an element's text content matches the expected value",name:"assert_text",schema:Ko,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).textContent({timeout:5e3}).catch(()=>null),n=o!=null&&o.includes(t.expected),a=n?"passed":"failed",i=[{description:`Text contains "${t.expected}"`,detail:`Actual: "${o??"(not found)"}"`,status:a}],d=`agent-step-${String(e.stepIndex)}`,l={expected:{type:"static",value:t.expected},id:d,locator:{by:"css",value:t.selector},operator:"contains",type:"assertText"};return h({assertions:i,detail:n?`Text matches: "${t.expected}"`:`Expected "${t.expected}", got "${o??"(not found)"}"`,duration:performance.now()-r,nodeType:"assertText",page:e.page,runStartTime:e.runStartTime,specNode:l,status:a,stepIndex:e.stepIndex,title:`Assert text: ${t.selector}`})}});import{z as Be}from"zod";var Yo=Be.object({expected:Be.string().describe("Expected page title or substring"),operator:Be.enum(["equals","contains","startsWith","endsWith"]).describe("Comparison operator").default("contains")}),Tt=u({description:"Assert that the page title matches an expected value",name:"assert_title",schema:Yo,execute:async(e,t)=>{let r=performance.now(),o=await e.page.title(),n=Qo({actual:o,expected:t.expected,operator:t.operator}),a=n?"passed":"failed",i=[{description:`Title ${t.operator} "${t.expected}"`,detail:n?void 0:`Got: "${o}"`,status:a}],d=`agent-step-${String(e.stepIndex)}`,l={expected:{type:"static",value:t.expected},id:d,operator:t.operator,type:"assertTitle"};return h({assertions:i,detail:n?"Title matches":`Expected title ${t.operator} "${t.expected}", got "${o}"`,duration:performance.now()-r,nodeType:"assertTitle",page:e.page,runStartTime:e.runStartTime,specNode:l,status:a,stepIndex:e.stepIndex,title:`Assert title ${t.operator} "${t.expected}"`})}});function Qo({actual:e,expected:t,operator:r}){return r==="equals"?e===t:r==="contains"?e.includes(t):r==="startsWith"?e.startsWith(t):r==="endsWith"?e.endsWith(t):!1}import{z as Ct}from"zod";var Xo=Ct.object({selector:Ct.string().describe("CSS selector for the element to check")}),At=u({description:"Assert that an element matching the selector is visible on the page",name:"assert_visible",schema:Xo,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).isVisible().catch(()=>!1),n=o?"passed":"failed",a=[{description:`Element ${t.selector} is visible`,detail:o?"Element is visible":"Element is not visible",status:n}],d={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertVisible"};return h({assertions:a,detail:o?"Element is visible":"Element is not visible",duration:performance.now()-r,nodeType:"assertVisible",page:e.page,runStartTime:e.runStartTime,specNode:d,status:n,stepIndex:e.stepIndex,title:`Assert visible: ${t.selector}`})}});import{z as Nt}from"zod";var Zo=Nt.object({selector:Nt.string().describe("CSS selector for the checkbox to check")}),Et=u({description:"Check a checkbox matching the CSS selector",name:"check",schema:Zo,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).check({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"check"};return h({assertions:[],detail:`Checked ${t.selector}`,duration:performance.now()-r,nodeType:"check",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Check ${t.selector}`})}});import{z as It}from"zod";var en=It.object({selector:It.string().describe("CSS selector for the input to clear")}),$t=u({description:"Clear the contents of an input field",name:"clear",schema:en,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).clear({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"clear"};return h({assertions:[],detail:`Cleared ${t.selector}`,duration:performance.now()-r,nodeType:"clear",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Clear ${t.selector}`})}});import{z as jt}from"zod";var tn=jt.object({selector:jt.string().describe("CSS selector for the element to click")}),Ot=u({description:"Click an element matching the CSS selector",name:"click",schema:tn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"click"};return h({assertions:[],detail:`Clicked ${t.selector}`,duration:performance.now()-r,nodeType:"click",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Click ${t.selector}`})}});import{z as Me}from"zod";var rn=Me.object({action:Me.enum(["read","write"]).describe("Whether to read from or write to the clipboard"),value:Me.string().optional().describe("Text to write (required for write action)")}),Vt=u({description:"Read from or write to the browser clipboard. For write, provide the text value. For read, the clipboard contents are returned.",name:"clipboard",schema:rn,execute:async(e,t)=>{let r=performance.now(),o=`agent-step-${String(e.stepIndex)}`;if(t.action==="write"){if(t.value==null)throw new Error("clipboard write requires a value");let i=JSON.stringify(t.value);await e.page.evaluate(`navigator.clipboard.writeText(${i})`);let d={action:"write",id:o,type:"clipboard",value:{type:"static",value:t.value}};return h({assertions:[],detail:`Wrote to clipboard: "${t.value}"`,duration:performance.now()-r,nodeType:"clipboard",page:e.page,runStartTime:e.runStartTime,specNode:d,status:"passed",stepIndex:e.stepIndex,title:"Clipboard write"})}let n=String(await e.page.evaluate("navigator.clipboard.readText()")),a={action:"read",id:o,type:"clipboard"};return h({assertions:[],detail:`Clipboard contents: "${n}"`,duration:performance.now()-r,nodeType:"clipboard",page:e.page,runStartTime:e.runStartTime,specNode:a,status:"passed",stepIndex:e.stepIndex,title:"Clipboard read"})}});import{z as Pe}from"zod";var Ut=Pe.object({summary:Pe.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:Pe.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.")}),Ft={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:{...Pe.toJSONSchema(Ut,{target:"draft-2020-12"}),type:"object"},name:"complete_test"},name:"complete_test",execute(e,t){let r=Ut.parse(t),o={kind:"verdict",summary:r.summary,toolOutput:`Verdict: ${r.verdict}
4
- ${r.summary}`,verdict:r.verdict};return Promise.resolve(o)}};import{z as _t}from"zod";var on=_t.object({selector:_t.string().describe("CSS selector for the element to extract text from")}),Lt=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:on,execute:async(e,t)=>{let r=performance.now(),n=await e.page.locator(t.selector).textContent({timeout:5e3})??"",i={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"extractText",variable:"extractedText"};return h({assertions:[],detail:`Extracted text: "${n}"`,duration:performance.now()-r,nodeType:"extractText",page:e.page,runStartTime:e.runStartTime,specNode:i,status:"passed",stepIndex:e.stepIndex,title:`Extract text from ${t.selector}`})}});import{z as Ge}from"zod";var nn=Ge.object({selector:Ge.string().describe("CSS selector for the input element"),value:Ge.string().describe("The text to type into the element")}),Dt=u({description:"Clear and type text into an input element matching the CSS selector",name:"fill",schema:nn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).fill(t.value,{timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"fill",value:{type:"static",value:t.value}};return h({assertions:[],detail:`Filled ${t.selector} with "${t.value}"`,duration:performance.now()-r,nodeType:"fill",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Fill ${t.selector}`})}});import{z as Wt}from"zod";var an=Wt.object({level:Wt.string().describe("Filter by log level: 'error', 'warning', 'log', 'info', 'debug', or 'all'").default("all")}),qt=u({description:"Get console log messages from the browser. Optionally filter by level (error, warning, log, info, debug, all).",name:"get_console_logs",schema:an,execute:(e,t)=>{let r=t.level==="all"?e.monitor.consoleEntries:e.monitor.consoleEntries.filter(n=>n.level===t.level);if(r.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No console ${t.level==="all"?"messages":t.level+" messages"} recorded.`};let o=r.map(n=>sn(n)).join(`
2
+ var Oo=Object.create;var kt=Object.defineProperty;var Vo=Object.getOwnPropertyDescriptor;var Uo=Object.getOwnPropertyNames;var Fo=Object.getPrototypeOf,Wo=Object.prototype.hasOwnProperty;var Te=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var _o=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Uo(t))!Wo.call(e,n)&&n!==r&&kt(e,n,{get:()=>t[n],enumerable:!(o=Vo(t,n))||o.enumerable});return e};var Lo=(e,t,r)=>(r=e!=null?Oo(Fo(e)):{},_o(t||!e||!e.__esModule?kt(r,"default",{value:e,enumerable:!0}):r,e));var qr=Te(Oe=>{"use strict";Object.defineProperty(Oe,"__esModule",{value:!0});Oe.timingSafeEqual=void 0;function Dr(e,t=""){if(!e)throw new Error(t)}function Es(e,t){if(e.byteLength!==t.byteLength)return!1;e instanceof DataView||(e=new DataView(ArrayBuffer.isView(e)?e.buffer:e)),t instanceof DataView||(t=new DataView(ArrayBuffer.isView(t)?t.buffer:t)),Dr(e instanceof DataView),Dr(t instanceof DataView);let r=e.byteLength,o=0,n=-1;for(;++n<r;)o|=e.getUint8(n)^t.getUint8(n);return o===0}Oe.timingSafeEqual=Es});var Gr=Te(D=>{"use strict";var Is=D&&D.__extends||(function(){var e=function(t,r){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(o,n){o.__proto__=n}||function(o,n){for(var a in n)n.hasOwnProperty(a)&&(o[a]=n[a])},e(t,r)};return function(t,r){e(t,r);function o(){this.constructor=t}t.prototype=r===null?Object.create(r):(o.prototype=r.prototype,new o)}})();Object.defineProperty(D,"__esModule",{value:!0});var N=256,pt=(function(){function e(t){t===void 0&&(t="="),this._paddingCharacter=t}return e.prototype.encodedLength=function(t){return this._paddingCharacter?(t+2)/3*4|0:(t*8+5)/6|0},e.prototype.encode=function(t){for(var r="",o=0;o<t.length-2;o+=3){var n=t[o]<<16|t[o+1]<<8|t[o+2];r+=this._encodeByte(n>>>18&63),r+=this._encodeByte(n>>>12&63),r+=this._encodeByte(n>>>6&63),r+=this._encodeByte(n>>>0&63)}var a=t.length-o;if(a>0){var n=t[o]<<16|(a===2?t[o+1]<<8:0);r+=this._encodeByte(n>>>18&63),r+=this._encodeByte(n>>>12&63),a===2?r+=this._encodeByte(n>>>6&63):r+=this._paddingCharacter||"",r+=this._paddingCharacter||""}return r},e.prototype.maxDecodedLength=function(t){return this._paddingCharacter?t/4*3|0:(t*6+7)/8|0},e.prototype.decodedLength=function(t){return this.maxDecodedLength(t.length-this._getPaddingLength(t))},e.prototype.decode=function(t){if(t.length===0)return new Uint8Array(0);for(var r=this._getPaddingLength(t),o=t.length-r,n=new Uint8Array(this.maxDecodedLength(o)),a=0,i=0,c=0,l=0,p=0,g=0,f=0;i<o-4;i+=4)l=this._decodeChar(t.charCodeAt(i+0)),p=this._decodeChar(t.charCodeAt(i+1)),g=this._decodeChar(t.charCodeAt(i+2)),f=this._decodeChar(t.charCodeAt(i+3)),n[a++]=l<<2|p>>>4,n[a++]=p<<4|g>>>2,n[a++]=g<<6|f,c|=l&N,c|=p&N,c|=g&N,c|=f&N;if(i<o-1&&(l=this._decodeChar(t.charCodeAt(i)),p=this._decodeChar(t.charCodeAt(i+1)),n[a++]=l<<2|p>>>4,c|=l&N,c|=p&N),i<o-2&&(g=this._decodeChar(t.charCodeAt(i+2)),n[a++]=p<<4|g>>>2,c|=g&N),i<o-3&&(f=this._decodeChar(t.charCodeAt(i+3)),n[a++]=g<<6|f,c|=f&N),c!==0)throw new Error("Base64Coder: incorrect characters for decoding");return n},e.prototype._encodeByte=function(t){var r=t;return r+=65,r+=25-t>>>8&6,r+=51-t>>>8&-75,r+=61-t>>>8&-15,r+=62-t>>>8&3,String.fromCharCode(r)},e.prototype._decodeChar=function(t){var r=N;return r+=(42-t&t-44)>>>8&-N+t-43+62,r+=(46-t&t-48)>>>8&-N+t-47+63,r+=(47-t&t-58)>>>8&-N+t-48+52,r+=(64-t&t-91)>>>8&-N+t-65+0,r+=(96-t&t-123)>>>8&-N+t-97+26,r},e.prototype._getPaddingLength=function(t){var r=0;if(this._paddingCharacter){for(var o=t.length-1;o>=0&&t[o]===this._paddingCharacter;o--)r++;if(t.length<4||r>2)throw new Error("Base64Coder: incorrect padding")}return r},e})();D.Coder=pt;var ve=new pt;function $s(e){return ve.encode(e)}D.encode=$s;function js(e){return ve.decode(e)}D.decode=js;var Br=(function(e){Is(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype._encodeByte=function(r){var o=r;return o+=65,o+=25-r>>>8&6,o+=51-r>>>8&-75,o+=61-r>>>8&-13,o+=62-r>>>8&49,String.fromCharCode(o)},t.prototype._decodeChar=function(r){var o=N;return o+=(44-r&r-46)>>>8&-N+r-45+62,o+=(94-r&r-96)>>>8&-N+r-95+63,o+=(47-r&r-58)>>>8&-N+r-48+52,o+=(64-r&r-91)>>>8&-N+r-65+0,o+=(96-r&r-123)>>>8&-N+r-97+26,o},t})(pt);D.URLSafeCoder=Br;var Mr=new Br;function Os(e){return Mr.encode(e)}D.encodeURLSafe=Os;function Vs(e){return Mr.decode(e)}D.decodeURLSafe=Vs;D.encodedLength=function(e){return ve.encodedLength(e)};D.maxDecodedLength=function(e){return ve.maxDecodedLength(e)};D.decodedLength=function(e){return ve.decodedLength(e)}});var Hr=Te((zr,Ve)=>{"use strict";(function(e,t){var r={};t(r);var o=r.default;for(var n in r)o[n]=r[n];typeof Ve=="object"&&typeof Ve.exports=="object"?Ve.exports=o:typeof define=="function"&&define.amd?define(function(){return o}):e.sha256=o})(zr,function(e){"use strict";e.__esModule=!0,e.digestLength=32,e.blockSize=64;var t=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]);function r(f,d,m,y,A){for(var k,v,S,W,E,T,G,I,O,$,ge,ye,Pe;A>=64;){for(k=d[0],v=d[1],S=d[2],W=d[3],E=d[4],T=d[5],G=d[6],I=d[7],$=0;$<16;$++)ge=y+$*4,f[$]=(m[ge]&255)<<24|(m[ge+1]&255)<<16|(m[ge+2]&255)<<8|m[ge+3]&255;for($=16;$<64;$++)O=f[$-2],ye=(O>>>17|O<<15)^(O>>>19|O<<13)^O>>>10,O=f[$-15],Pe=(O>>>7|O<<25)^(O>>>18|O<<14)^O>>>3,f[$]=(ye+f[$-7]|0)+(Pe+f[$-16]|0);for($=0;$<64;$++)ye=(((E>>>6|E<<26)^(E>>>11|E<<21)^(E>>>25|E<<7))+(E&T^~E&G)|0)+(I+(t[$]+f[$]|0)|0)|0,Pe=((k>>>2|k<<30)^(k>>>13|k<<19)^(k>>>22|k<<10))+(k&v^k&S^v&S)|0,I=G,G=T,T=E,E=W+ye|0,W=S,S=v,v=k,k=ye+Pe|0;d[0]+=k,d[1]+=v,d[2]+=S,d[3]+=W,d[4]+=E,d[5]+=T,d[6]+=G,d[7]+=I,y+=64,A-=64}return y}var o=(function(){function f(){this.digestLength=e.digestLength,this.blockSize=e.blockSize,this.state=new Int32Array(8),this.temp=new Int32Array(64),this.buffer=new Uint8Array(128),this.bufferLength=0,this.bytesHashed=0,this.finished=!1,this.reset()}return f.prototype.reset=function(){return this.state[0]=1779033703,this.state[1]=3144134277,this.state[2]=1013904242,this.state[3]=2773480762,this.state[4]=1359893119,this.state[5]=2600822924,this.state[6]=528734635,this.state[7]=1541459225,this.bufferLength=0,this.bytesHashed=0,this.finished=!1,this},f.prototype.clean=function(){for(var d=0;d<this.buffer.length;d++)this.buffer[d]=0;for(var d=0;d<this.temp.length;d++)this.temp[d]=0;this.reset()},f.prototype.update=function(d,m){if(m===void 0&&(m=d.length),this.finished)throw new Error("SHA256: can't update because hash was finished.");var y=0;if(this.bytesHashed+=m,this.bufferLength>0){for(;this.bufferLength<64&&m>0;)this.buffer[this.bufferLength++]=d[y++],m--;this.bufferLength===64&&(r(this.temp,this.state,this.buffer,0,64),this.bufferLength=0)}for(m>=64&&(y=r(this.temp,this.state,d,y,m),m%=64);m>0;)this.buffer[this.bufferLength++]=d[y++],m--;return this},f.prototype.finish=function(d){if(!this.finished){var m=this.bytesHashed,y=this.bufferLength,A=m/536870912|0,k=m<<3,v=m%64<56?64:128;this.buffer[y]=128;for(var S=y+1;S<v-8;S++)this.buffer[S]=0;this.buffer[v-8]=A>>>24&255,this.buffer[v-7]=A>>>16&255,this.buffer[v-6]=A>>>8&255,this.buffer[v-5]=A>>>0&255,this.buffer[v-4]=k>>>24&255,this.buffer[v-3]=k>>>16&255,this.buffer[v-2]=k>>>8&255,this.buffer[v-1]=k>>>0&255,r(this.temp,this.state,this.buffer,0,v),this.finished=!0}for(var S=0;S<8;S++)d[S*4+0]=this.state[S]>>>24&255,d[S*4+1]=this.state[S]>>>16&255,d[S*4+2]=this.state[S]>>>8&255,d[S*4+3]=this.state[S]>>>0&255;return this},f.prototype.digest=function(){var d=new Uint8Array(this.digestLength);return this.finish(d),d},f.prototype._saveState=function(d){for(var m=0;m<this.state.length;m++)d[m]=this.state[m]},f.prototype._restoreState=function(d,m){for(var y=0;y<this.state.length;y++)this.state[y]=d[y];this.bytesHashed=m,this.finished=!1,this.bufferLength=0},f})();e.Hash=o;var n=(function(){function f(d){this.inner=new o,this.outer=new o,this.blockSize=this.inner.blockSize,this.digestLength=this.inner.digestLength;var m=new Uint8Array(this.blockSize);if(d.length>this.blockSize)new o().update(d).finish(m).clean();else for(var y=0;y<d.length;y++)m[y]=d[y];for(var y=0;y<m.length;y++)m[y]^=54;this.inner.update(m);for(var y=0;y<m.length;y++)m[y]^=106;this.outer.update(m),this.istate=new Uint32Array(8),this.ostate=new Uint32Array(8),this.inner._saveState(this.istate),this.outer._saveState(this.ostate);for(var y=0;y<m.length;y++)m[y]=0}return f.prototype.reset=function(){return this.inner._restoreState(this.istate,this.inner.blockSize),this.outer._restoreState(this.ostate,this.outer.blockSize),this},f.prototype.clean=function(){for(var d=0;d<this.istate.length;d++)this.ostate[d]=this.istate[d]=0;this.inner.clean(),this.outer.clean()},f.prototype.update=function(d){return this.inner.update(d),this},f.prototype.finish=function(d){return this.outer.finished?this.outer.finish(d):(this.inner.finish(d),this.outer.update(d,this.digestLength).finish(d)),this},f.prototype.digest=function(){var d=new Uint8Array(this.digestLength);return this.finish(d),d},f})();e.HMAC=n;function a(f){var d=new o().update(f),m=d.digest();return d.clean(),m}e.hash=a,e.default=a;function i(f,d){var m=new n(f).update(d),y=m.digest();return m.clean(),y}e.hmac=i;function c(f,d,m,y){var A=y[0];if(A===0)throw new Error("hkdf: cannot expand more");d.reset(),A>1&&d.update(f),m&&d.update(m),d.update(y),d.finish(f),y[0]++}var l=new Uint8Array(e.digestLength);function p(f,d,m,y){d===void 0&&(d=l),y===void 0&&(y=32);for(var A=new Uint8Array([1]),k=i(d,f),v=new n(k),S=new Uint8Array(v.digestLength),W=S.length,E=new Uint8Array(y),T=0;T<y;T++)W===S.length&&(c(S,v,m,A),W=0),E[T]=S[W++];return v.clean(),S.fill(0),A.fill(0),E}e.hkdf=p;function g(f,d,m,y){for(var A=new n(f),k=A.digestLength,v=new Uint8Array(4),S=new Uint8Array(k),W=new Uint8Array(k),E=new Uint8Array(y),T=0;T*k<y;T++){var G=T+1;v[0]=G>>>24&255,v[1]=G>>>16&255,v[2]=G>>>8&255,v[3]=G>>>0&255,A.reset(),A.update(d),A.update(v),A.finish(W);for(var I=0;I<k;I++)S[I]=W[I];for(var I=2;I<=m;I++){A.reset(),A.update(W).finish(W);for(var O=0;O<k;O++)S[O]^=W[O]}for(var I=0;I<k&&T*k+I<y;I++)E[T*k+I]=S[I]}for(var T=0;T<k;T++)S[T]=W[T]=0;for(var T=0;T<4;T++)v[T]=0;return A.clean(),E}e.pbkdf2=g})});var Yr=Te(pe=>{"use strict";Object.defineProperty(pe,"__esModule",{value:!0});pe.Webhook=pe.WebhookVerificationError=void 0;var Us=qr(),Jr=Gr(),Fs=Hr(),Kr=300,ft=class e extends Error{constructor(t){super(t),Object.setPrototypeOf(this,e.prototype),this.name="ExtendableError",this.stack=new Error(t).stack}},te=class e extends ft{constructor(t){super(t),Object.setPrototypeOf(this,e.prototype),this.name="WebhookVerificationError"}};pe.WebhookVerificationError=te;var Ue=class e{constructor(t,r){if(!t)throw new Error("Secret can't be empty.");if(r?.format==="raw")t instanceof Uint8Array?this.key=t:this.key=Uint8Array.from(t,o=>o.charCodeAt(0));else{if(typeof t!="string")throw new Error("Expected secret to be of type string");t.startsWith(e.prefix)&&(t=t.substring(e.prefix.length)),this.key=Jr.decode(t)}}verify(t,r){let o={};for(let d of Object.keys(r))o[d.toLowerCase()]=r[d];let n=o["webhook-id"],a=o["webhook-signature"],i=o["webhook-timestamp"];if(!a||!n||!i)throw new te("Missing required headers");let c=this.verifyTimestamp(i),p=this.sign(n,c,t).split(",")[1],g=a.split(" "),f=new globalThis.TextEncoder;for(let d of g){let[m,y]=d.split(",");if(m==="v1"&&(0,Us.timingSafeEqual)(f.encode(y),f.encode(p)))return JSON.parse(t.toString())}throw new te("No matching signature found")}sign(t,r,o){if(typeof o!="string")if(o.constructor.name==="Buffer")o=o.toString();else throw new Error("Expected payload to be of type string or Buffer.");let n=new TextEncoder,a=Math.floor(r.getTime()/1e3),i=n.encode(`${t}.${a}.${o}`);return`v1,${Jr.encode(Fs.hmac(this.key,i))}`}verifyTimestamp(t){let r=Math.floor(Date.now()/1e3),o=parseInt(t,10);if(isNaN(o))throw new te("Invalid Signature Headers");if(r-o>Kr)throw new te("Message timestamp too old");if(o>r+Kr)throw new te("Message timestamp too new");return new Date(o*1e3)}};pe.Webhook=Ue;Ue.prefix="whsec_"});import{StdioServerTransport as Vl}from"@modelcontextprotocol/sdk/server/stdio.js";import{McpServer as Nl}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as H}from"zod";function z({locator:e,page:t}){switch(e.by){case"css":return t.locator(e.value);case"testId":return t.getByTestId(e.value);case"role":return t.getByRole(e.role,{exact:e.exact,name:e.name});case"text":return t.getByText(e.value,{exact:e.exact});case"label":return t.getByLabel(e.value,{exact:e.exact});case"placeholder":return t.getByPlaceholder(e.value);case"altText":return t.getByAltText(e.value)}}import tp from"@anthropic-ai/sdk";import{z as xt}from"zod";import{z as Go}from"zod";async function be({page:e,runStartTime:t,targets:r}){let o=e.viewportSize();if(o==null)throw new Error("Page has no viewport set");let[n,a]=await Promise.all([e.screenshot({type:"png"}),qo(r)]),i=n.toString("base64");return{annotations:a,screenshotBase64:i,snapshotTimestamp:Math.round(performance.now()-t),url:e.url(),viewportHeight:o.height,viewportWidth:o.width}}function Do(e){return"locator"in e}async function qo(e){return(await Promise.all(e.map(r=>Bo(r)))).filter(r=>r!=null)}async function Bo(e){if(!Do(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 Me({node:e,page:t}){if(e.type==="assertUrl"||e.type==="waitForUrl")return[{label:e.type,type:"urlBar"}];if(e.type==="drag"){let n=z({locator:e.source,page:t}),a=z({locator:e.target,page:t});return[{label:"drag-source",locator:n,type:"action"},{label:"drag-target",locator:a,type:"action"}]}if(!Mo(e))return[];let r=e.type.startsWith("assert")?"assertion":"action",o=z({locator:e.locator,page:t});return[{label:e.type,locator:o,type:r}]}function Mo(e){return"locator"in e&&e.locator!=null}function u({description:e,execute:t,name:r,schema:o}){let n=Go.toJSONSchema(o,{target:"draft-2020-12"});return{anthropicTool:{description:e,input_schema:{...n,type:"object"},name:r},name:r,async execute(a,i){let c=o.parse(i);return{...await Promise.resolve(t(a,c)),kind:"action"}}}}async function h(e){let t=e.specNode==null?[]:Me({node:e.specNode,page:e.page}),r=await be({page:e.page,runStartTime:e.runStartTime,targets:t}),o=[...e.assertions],n={annotations:r.annotations,assertions:o,detail:e.detail,duration:Math.round(e.duration),nodeId:`agent-step-${String(e.stepIndex)}`,nodeType:e.nodeType,screenshotBase64:r.screenshotBase64,snapshotTimestamp:r.snapshotTimestamp,status:e.status,stepIndex:e.stepIndex,title:e.title,url:r.url,viewportHeight:r.viewportHeight,viewportWidth:r.viewportWidth},a=o.length>0?o.map(i=>`${i.status}: ${i.description} \u2014 ${i.detail??""}`).join(`
3
+ `):`${e.status}: ${e.title}`;return{specNode:e.specNode,stepResult:n,toolOutput:a}}var zo=xt.object({selector:xt.string().describe("CSS selector for the checkbox or radio button")}),Rt=u({description:"Assert that a checkbox or radio button is checked",name:"assert_checked",schema:zo,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).isChecked().catch(()=>!1),n=o?"passed":"failed",a=[{description:`Element ${t.selector} is checked`,detail:o?void 0:"Element is not checked",status:n}],c={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertChecked"};return h({assertions:a,detail:o?"Element is checked":"Element is not checked",duration:performance.now()-r,nodeType:"assertChecked",page:e.page,runStartTime:e.runStartTime,specNode:c,status:n,stepIndex:e.stepIndex,title:`Assert checked: ${t.selector}`})}});import{z as ce}from"zod";var Ho=ce.object({httpOnly:ce.boolean().optional().describe("Assert httpOnly flag value"),name:ce.string().describe("Cookie name to check"),sameSite:ce.enum(["Strict","Lax","None"]).optional().describe("Assert sameSite value"),secure:ce.boolean().optional().describe("Assert secure flag value"),value:ce.string().optional().describe("Expected cookie value (substring match)")}),Tt=u({description:"Assert properties of a browser cookie. Check existence, value, and security flags (httpOnly, secure, sameSite).",name:"assert_cookie",schema:Ho,execute:async(e,t)=>{let r=performance.now(),n=(await e.page.context().cookies()).find(g=>g.name===t.name),a=n==null?[{description:`Cookie "${t.name}" exists`,detail:"Cookie not found",status:"failed"}]:[{description:`Cookie "${t.name}" exists`,detail:void 0,status:"passed"},...Jo({cookie:n,input:t})],i=a.some(g=>g.status==="failed"),c=i?"failed":"passed",p={id:`agent-step-${String(e.stepIndex)}`,name:{type:"static",value:t.name},type:"assertCookie"};return h({assertions:a,detail:i?a.filter(g=>g.status==="failed").map(g=>g.detail).join("; "):"Cookie OK",duration:performance.now()-r,nodeType:"assertCookie",page:e.page,runStartTime:e.runStartTime,specNode:p,status:c,stepIndex:e.stepIndex,title:`Assert cookie: ${t.name}`})}});function Jo({cookie:e,input:t}){return[Ko(t.value,e.value),Pt("httpOnly",t.httpOnly,e.httpOnly),Pt("secure",t.secure,e.secure),Yo("sameSite",t.sameSite,e.sameSite)].filter(r=>r!=null)}function Ko(e,t){if(e==null)return;let r=t.includes(e);return{description:`Cookie value contains "${e}"`,detail:r?void 0:`Got: "${t}"`,status:r?"passed":"failed"}}function Pt(e,t,r){if(t!=null)return{description:`Cookie ${e} is ${String(t)}`,detail:r===t?void 0:`Got: ${String(r)}`,status:r===t?"passed":"failed"}}function Yo(e,t,r){if(t!=null)return{description:`Cookie ${e} is ${t}`,detail:r===t?void 0:`Got: ${r}`,status:r===t?"passed":"failed"}}import{z as Ct}from"zod";var Qo=Ct.object({selector:Ct.string().describe("CSS selector for the element to check focus on")}),At=u({description:"Assert that an element has keyboard focus. Use for accessibility tab-order testing.",name:"assert_focused",schema:Qo,execute:async(e,t)=>{let r=performance.now(),n=!!await e.page.locator(t.selector).evaluate("(el) => document.activeElement === el").catch(()=>!1),a=n?"passed":"failed",i=[{description:`Element ${t.selector} is focused`,detail:n?void 0:"Element does not have focus",status:a}],l={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertFocused"};return h({assertions:i,detail:n?"Element is focused":"Element does not have focus",duration:performance.now()-r,nodeType:"assertFocused",page:e.page,runStartTime:e.runStartTime,specNode:l,status:a,stepIndex:e.stepIndex,title:`Assert focused: ${t.selector}`})}});import{z as de}from"zod";var Xo=de.object({bodyContains:de.string().optional().describe("Assert response body contains this string"),headerName:de.string().optional().describe("Response header name to check"),headerValue:de.string().optional().describe("Expected value the header should contain"),status:de.number().int().optional().describe("Expected HTTP status code"),urlPattern:de.string().describe("URL substring to match the response against")}),Nt=u({description:"Wait for a network response matching the URL pattern and assert on its status, body, or headers. Useful for verifying API calls and security headers (CSP, CORS).",name:"assert_response",schema:Xo,execute:async(e,t)=>{let r=performance.now(),o=await e.page.waitForResponse(p=>p.url().includes(t.urlPattern),{timeout:1e4}),n=[Zo(t.status,o.status()),await en(t.bodyContains,o),tn(t.headerName,t.headerValue,o.headers())].filter(p=>p!=null),a=n.length===0?[{description:`Response matching "${t.urlPattern}" received`,detail:void 0,status:"passed"}]:n,i=a.some(p=>p.status==="failed"),l={id:`agent-step-${String(e.stepIndex)}`,type:"assertResponse",urlPattern:{type:"static",value:t.urlPattern}};return h({assertions:a,detail:i?a.filter(p=>p.status==="failed").map(p=>p.detail).join("; "):"Response OK",duration:performance.now()-r,nodeType:"assertResponse",page:e.page,runStartTime:e.runStartTime,specNode:l,status:i?"failed":"passed",stepIndex:e.stepIndex,title:`Assert response: ${t.urlPattern}`})}});function Zo(e,t){if(e!=null)return{description:`Response status equals ${String(e)}`,detail:t===e?void 0:`Got: ${String(t)}`,status:t===e?"passed":"failed"}}async function en(e,t){if(e==null)return;let o=(await t.text()).includes(e);return{description:`Response body contains "${e}"`,detail:o?void 0:"Not found in response body",status:o?"passed":"failed"}}function tn(e,t,r){if(e==null||t==null)return;let o=r[e.toLowerCase()],n=o!=null&&o.includes(t);return{description:`Response header "${e}" contains "${t}"`,detail:n?void 0:`Got: "${o??"(missing)"}"`,status:n?"passed":"failed"}}import{z as Ge}from"zod";var rn=Ge.object({expected:Ge.string().describe("The expected text content"),selector:Ge.string().describe("CSS selector for the element to check")}),Et=u({description:"Assert that an element's text content matches the expected value",name:"assert_text",schema:rn,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).textContent({timeout:5e3}).catch(()=>null),n=o!=null&&o.includes(t.expected),a=n?"passed":"failed",i=[{description:`Text contains "${t.expected}"`,detail:`Actual: "${o??"(not found)"}"`,status:a}],c=`agent-step-${String(e.stepIndex)}`,l={expected:{type:"static",value:t.expected},id:c,locator:{by:"css",value:t.selector},operator:"contains",type:"assertText"};return h({assertions:i,detail:n?`Text matches: "${t.expected}"`:`Expected "${t.expected}", got "${o??"(not found)"}"`,duration:performance.now()-r,nodeType:"assertText",page:e.page,runStartTime:e.runStartTime,specNode:l,status:a,stepIndex:e.stepIndex,title:`Assert text: ${t.selector}`})}});import{z as ze}from"zod";var on=ze.object({expected:ze.string().describe("Expected page title or substring"),operator:ze.enum(["equals","contains","startsWith","endsWith"]).describe("Comparison operator").default("contains")}),It=u({description:"Assert that the page title matches an expected value",name:"assert_title",schema:on,execute:async(e,t)=>{let r=performance.now(),o=await e.page.title(),n=nn({actual:o,expected:t.expected,operator:t.operator}),a=n?"passed":"failed",i=[{description:`Title ${t.operator} "${t.expected}"`,detail:n?void 0:`Got: "${o}"`,status:a}],c=`agent-step-${String(e.stepIndex)}`,l={expected:{type:"static",value:t.expected},id:c,operator:t.operator,type:"assertTitle"};return h({assertions:i,detail:n?"Title matches":`Expected title ${t.operator} "${t.expected}", got "${o}"`,duration:performance.now()-r,nodeType:"assertTitle",page:e.page,runStartTime:e.runStartTime,specNode:l,status:a,stepIndex:e.stepIndex,title:`Assert title ${t.operator} "${t.expected}"`})}});function nn({actual:e,expected:t,operator:r}){return r==="equals"?e===t:r==="contains"?e.includes(t):r==="startsWith"?e.startsWith(t):r==="endsWith"?e.endsWith(t):!1}import{z as $t}from"zod";var an=$t.object({selector:$t.string().describe("CSS selector for the element to check")}),jt=u({description:"Assert that an element matching the selector is visible on the page",name:"assert_visible",schema:an,execute:async(e,t)=>{let r=performance.now(),o=await e.page.locator(t.selector).isVisible().catch(()=>!1),n=o?"passed":"failed",a=[{description:`Element ${t.selector} is visible`,detail:o?"Element is visible":"Element is not visible",status:n}],c={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"assertVisible"};return h({assertions:a,detail:o?"Element is visible":"Element is not visible",duration:performance.now()-r,nodeType:"assertVisible",page:e.page,runStartTime:e.runStartTime,specNode:c,status:n,stepIndex:e.stepIndex,title:`Assert visible: ${t.selector}`})}});import{z as Ot}from"zod";var sn=Ot.object({selector:Ot.string().describe("CSS selector for the checkbox to check")}),Vt=u({description:"Check a checkbox matching the CSS selector",name:"check",schema:sn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).check({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"check"};return h({assertions:[],detail:`Checked ${t.selector}`,duration:performance.now()-r,nodeType:"check",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Check ${t.selector}`})}});import{z as Ut}from"zod";var ln=Ut.object({selector:Ut.string().describe("CSS selector for the input to clear")}),Ft=u({description:"Clear the contents of an input field",name:"clear",schema:ln,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).clear({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"clear"};return h({assertions:[],detail:`Cleared ${t.selector}`,duration:performance.now()-r,nodeType:"clear",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Clear ${t.selector}`})}});import{z as Wt}from"zod";var cn=Wt.object({selector:Wt.string().describe("CSS selector for the element to click")}),_t=u({description:"Click an element matching the CSS selector",name:"click",schema:cn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"click"};return h({assertions:[],detail:`Clicked ${t.selector}`,duration:performance.now()-r,nodeType:"click",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Click ${t.selector}`})}});import{z as He}from"zod";var dn=He.object({action:He.enum(["read","write"]).describe("Whether to read from or write to the clipboard"),value:He.string().optional().describe("Text to write (required for write action)")}),Lt=u({description:"Read from or write to the browser clipboard. For write, provide the text value. For read, the clipboard contents are returned.",name:"clipboard",schema:dn,execute:async(e,t)=>{let r=performance.now(),o=`agent-step-${String(e.stepIndex)}`;if(t.action==="write"){if(t.value==null)throw new Error("clipboard write requires a value");let i=JSON.stringify(t.value);await e.page.evaluate(`navigator.clipboard.writeText(${i})`);let c={action:"write",id:o,type:"clipboard",value:{type:"static",value:t.value}};return h({assertions:[],detail:`Wrote to clipboard: "${t.value}"`,duration:performance.now()-r,nodeType:"clipboard",page:e.page,runStartTime:e.runStartTime,specNode:c,status:"passed",stepIndex:e.stepIndex,title:"Clipboard write"})}let n=String(await e.page.evaluate("navigator.clipboard.readText()")),a={action:"read",id:o,type:"clipboard"};return h({assertions:[],detail:`Clipboard contents: "${n}"`,duration:performance.now()-r,nodeType:"clipboard",page:e.page,runStartTime:e.runStartTime,specNode:a,status:"passed",stepIndex:e.stepIndex,title:"Clipboard read"})}});import{z as Ce}from"zod";var Dt=Ce.object({summary:Ce.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:Ce.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.")}),qt={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:{...Ce.toJSONSchema(Dt,{target:"draft-2020-12"}),type:"object"},name:"complete_test"},name:"complete_test",execute(e,t){let r=Dt.parse(t),o={kind:"verdict",summary:r.summary,toolOutput:`Verdict: ${r.verdict}
4
+ ${r.summary}`,verdict:r.verdict};return Promise.resolve(o)}};import{z as Bt}from"zod";var un=Bt.object({selector:Bt.string().describe("CSS selector for the element to extract text from")}),Mt=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:un,execute:async(e,t)=>{let r=performance.now(),n=await e.page.locator(t.selector).textContent({timeout:5e3})??"",i={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"extractText",variable:"extractedText"};return h({assertions:[],detail:`Extracted text: "${n}"`,duration:performance.now()-r,nodeType:"extractText",page:e.page,runStartTime:e.runStartTime,specNode:i,status:"passed",stepIndex:e.stepIndex,title:`Extract text from ${t.selector}`})}});import{z as Je}from"zod";var pn=Je.object({selector:Je.string().describe("CSS selector for the input element"),value:Je.string().describe("The text to type into the element")}),Gt=u({description:"Clear and type text into an input element matching the CSS selector",name:"fill",schema:pn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).fill(t.value,{timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"fill",value:{type:"static",value:t.value}};return h({assertions:[],detail:`Filled ${t.selector} with "${t.value}"`,duration:performance.now()-r,nodeType:"fill",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Fill ${t.selector}`})}});import{z as zt}from"zod";var fn=zt.object({level:zt.string().describe("Filter by log level: 'error', 'warning', 'log', 'info', 'debug', or 'all'").default("all")}),Ht=u({description:"Get console log messages from the browser. Optionally filter by level (error, warning, log, info, debug, all).",name:"get_console_logs",schema:fn,execute:(e,t)=>{let r=t.level==="all"?e.monitor.consoleEntries:e.monitor.consoleEntries.filter(n=>n.level===t.level);if(r.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No console ${t.level==="all"?"messages":t.level+" messages"} recorded.`};let o=r.map(n=>mn(n)).join(`
5
5
  `);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(r.length)} console messages:
6
- ${o}`}}});function sn(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as Bt}from"zod";var ze=50,ln=Bt.object({urlFilter:Bt.string().describe("Optional regex to filter network entries by URL. Omit to see all.").default("")}),Mt=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:ln,execute:(e,t)=>{let r=t.urlFilter.length>0?new RegExp(t.urlFilter,"i"):void 0,o=r==null?e.monitor.networkEntries:e.monitor.networkEntries.filter(d=>r.test(d.url));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:"No network requests recorded."};let n=o.slice(-ze),a=n.map(d=>{let l=d.statusCode==null?"pending":String(d.statusCode),p=d.responseHeaders["content-type"]??"";return`${d.method} ${l} ${d.url} [${p}]`}).join(`
7
- `),i=o.length>ze?`
8
- (showing last ${String(ze)} of ${String(o.length)})`:"";return{specNode:void 0,stepResult:void 0,toolOutput:`${String(n.length)} network requests:${i}
9
- ${a}`}}});import{z as Gt}from"zod";var cn=Gt.object({urlPattern:Gt.string().describe("Regex pattern to match the request URL")}),zt=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:cn,execute:(e,t)=>{let r=new RegExp(t.urlPattern,"i"),o=e.monitor.networkEntries.filter(l=>r.test(l.url));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No network requests matching: ${t.urlPattern}`};let n=o.at(-1);if(n==null)return{specNode:void 0,stepResult:void 0,toolOutput:`No network requests matching: ${t.urlPattern}`};let a=n.statusCode==null?"pending":String(n.statusCode),i=Object.entries(n.responseHeaders).map(([l,p])=>` ${l}: ${p}`).join(`
10
- `),d=n.responseBody??"(no body captured)";return{specNode:void 0,stepResult:void 0,toolOutput:`${n.method} ${a} ${n.url}
6
+ ${o}`}}});function mn(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as Jt}from"zod";var Ke=50,hn=Jt.object({urlFilter:Jt.string().describe("Optional regex to filter network entries by URL. Omit to see all.").default("")}),Kt=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:hn,execute:(e,t)=>{let r=t.urlFilter.length>0?new RegExp(t.urlFilter,"i"):void 0,o=r==null?e.monitor.networkEntries:e.monitor.networkEntries.filter(c=>r.test(c.url));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:"No network requests recorded."};let n=o.slice(-Ke),a=n.map(c=>{let l=c.statusCode==null?"pending":String(c.statusCode),p=c.responseHeaders["content-type"]??"";return`${c.method} ${l} ${c.url} [${p}]`}).join(`
7
+ `),i=o.length>Ke?`
8
+ (showing last ${String(Ke)} of ${String(o.length)})`:"";return{specNode:void 0,stepResult:void 0,toolOutput:`${String(n.length)} network requests:${i}
9
+ ${a}`}}});import{z as Yt}from"zod";var gn=Yt.object({urlPattern:Yt.string().describe("Regex pattern to match the request URL")}),Qt=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:gn,execute:(e,t)=>{let r=new RegExp(t.urlPattern,"i"),o=e.monitor.networkEntries.filter(l=>r.test(l.url));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No network requests matching: ${t.urlPattern}`};let n=o.at(-1);if(n==null)return{specNode:void 0,stepResult:void 0,toolOutput:`No network requests matching: ${t.urlPattern}`};let a=n.statusCode==null?"pending":String(n.statusCode),i=Object.entries(n.responseHeaders).map(([l,p])=>` ${l}: ${p}`).join(`
10
+ `),c=n.responseBody??"(no body captured)";return{specNode:void 0,stepResult:void 0,toolOutput:`${n.method} ${a} ${n.url}
11
11
 
12
12
  Response Headers:
13
13
  ${i}
14
14
 
15
15
  Body:
16
- ${d}`}}});import{z as Ht}from"zod";var dn=Ht.object({selector:Ht.string().optional().describe("CSS selector to scope the tree to a subtree (defaults to full page)")}),Jt=u({description:"Get the accessibility tree (ARIA snapshot) of the current page or a specific element. Use this to verify ARIA roles, labels, states, and the semantic structure of the page.",name:"get_accessibility_tree",schema:dn,execute:async(e,t)=>{let r=t.selector==null?e.page.locator(":root"):e.page.locator(t.selector);try{return{specNode:void 0,stepResult:void 0,toolOutput:await r.ariaSnapshot()}}catch{return{specNode:void 0,stepResult:void 0,toolOutput:"(unable to capture accessibility tree)"}}}});import{z as un}from"zod";var pn=un.object({}),Kt=u({description:"Get a text representation of the current page content for inspection",name:"get_page_content",schema:pn,execute:async(e,t)=>{let r=await e.page.locator("body").textContent({timeout:5e3}),o=e.page.url(),n=await e.page.title();return{specNode:void 0,stepResult:void 0,toolOutput:`URL: ${o}
16
+ ${c}`}}});import{z as Xt}from"zod";var yn=Xt.object({selector:Xt.string().optional().describe("CSS selector to scope the tree to a subtree (defaults to full page)")}),Zt=u({description:"Get the accessibility tree (ARIA snapshot) of the current page or a specific element. Use this to verify ARIA roles, labels, states, and the semantic structure of the page.",name:"get_accessibility_tree",schema:yn,execute:async(e,t)=>{let r=t.selector==null?e.page.locator(":root"):e.page.locator(t.selector);try{return{specNode:void 0,stepResult:void 0,toolOutput:await r.ariaSnapshot()}}catch{return{specNode:void 0,stepResult:void 0,toolOutput:"(unable to capture accessibility tree)"}}}});import{z as wn}from"zod";var bn=wn.object({}),er=u({description:"Get a text representation of the current page content for inspection",name:"get_page_content",schema:bn,execute:async(e,t)=>{let r=await e.page.locator("body").textContent({timeout:5e3}),o=e.page.url(),n=await e.page.title();return{specNode:void 0,stepResult:void 0,toolOutput:`URL: ${o}
17
17
  Title: ${n}
18
18
 
19
- ${r??""}`}}});import{z as fn}from"zod";var mn=fn.object({}),Yt=u({description:"Get uncaught JavaScript exceptions (page errors) that have occurred. These are runtime errors thrown in the browser that were not caught by error handlers.",name:"get_page_errors",schema:mn,execute:(e,t)=>{if(e.monitor.pageErrors.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:"No page errors recorded."};let r=e.monitor.pageErrors.map((o,n)=>`--- Error ${String(n+1)} ---
19
+ ${r??""}`}}});import{z as Sn}from"zod";var kn=Sn.object({}),tr=u({description:"Get uncaught JavaScript exceptions (page errors) that have occurred. These are runtime errors thrown in the browser that were not caught by error handlers.",name:"get_page_errors",schema:kn,execute:(e,t)=>{if(e.monitor.pageErrors.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:"No page errors recorded."};let r=e.monitor.pageErrors.map((o,n)=>`--- Error ${String(n+1)} ---
20
20
  ${o}`).join(`
21
21
 
22
22
  `);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(e.monitor.pageErrors.length)} page errors:
23
23
 
24
- ${r}`}}});import{z as Qt}from"zod";var hn=Qt.object({type:Qt.enum(["all","cookies","localStorage","sessionStorage"]).describe("Which storage to retrieve").default("all")}),Zt=u({description:"Get browser storage state: cookies (with httpOnly, secure, sameSite flags), localStorage, and/or sessionStorage. Useful for verifying authentication state, security cookie flags, and stored data.",name:"get_storage",schema:hn,execute:async(e,t)=>({specNode:void 0,stepResult:void 0,toolOutput:[...t.type==="all"||t.type==="cookies"?[await gn(e.page)]:[],...t.type==="all"||t.type==="localStorage"?[await Xt(e.page,"localStorage")]:[],...t.type==="all"||t.type==="sessionStorage"?[await Xt(e.page,"sessionStorage")]:[]].join(`
24
+ ${r}`}}});import{z as rr}from"zod";var vn=rr.object({type:rr.enum(["all","cookies","localStorage","sessionStorage"]).describe("Which storage to retrieve").default("all")}),nr=u({description:"Get browser storage state: cookies (with httpOnly, secure, sameSite flags), localStorage, and/or sessionStorage. Useful for verifying authentication state, security cookie flags, and stored data.",name:"get_storage",schema:vn,execute:async(e,t)=>({specNode:void 0,stepResult:void 0,toolOutput:[...t.type==="all"||t.type==="cookies"?[await xn(e.page)]:[],...t.type==="all"||t.type==="localStorage"?[await or(e.page,"localStorage")]:[],...t.type==="all"||t.type==="sessionStorage"?[await or(e.page,"sessionStorage")]:[]].join(`
25
25
 
26
- `)})});async function gn(e){let t=await e.context().cookies();if(t.length===0)return`## Cookies
26
+ `)})});async function xn(e){let t=await e.context().cookies();if(t.length===0)return`## Cookies
27
27
  (none)`;let r=t.map(o=>` ${o.name}=${o.value} (domain=${o.domain}, httpOnly=${String(o.httpOnly)}, secure=${String(o.secure)}, sameSite=${o.sameSite})`);return`## Cookies (${String(t.length)})
28
28
  ${r.join(`
29
- `)}`}async function Xt(e,t){let r=await e.evaluate(`JSON.stringify(Object.fromEntries(Object.entries(${t})))`);return`## ${t}
30
- ${String(r)}`}import{z as He}from"zod";var yn=He.object({action:He.enum(["accept","dismiss"]).describe("Whether to accept or dismiss the dialog"),promptText:He.string().optional().describe("Text to enter for prompt dialogs")}),er=u({description:"Set up a handler for the next browser dialog (alert, confirm, prompt). Call this BEFORE the action that triggers the dialog.",name:"handle_dialog",schema:yn,execute:(e,t)=>{let r=performance.now();e.page.once("dialog",async a=>{t.action==="accept"?await a.accept(t.promptText??void 0):await a.dismiss()});let o=`agent-step-${String(e.stepIndex)}`,n={action:t.action,id:o,type:"handleDialog"};return h({assertions:[],detail:`Dialog handler set: ${t.action}`,duration:performance.now()-r,nodeType:"handleDialog",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Handle dialog: ${t.action}`})}});import{z as tr}from"zod";var wn=tr.object({selector:tr.string().describe("CSS selector for the element to hover over")}),rr=u({description:"Hover over an element matching the CSS selector",name:"hover",schema:wn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).hover({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"hover"};return h({assertions:[],detail:`Hovered over ${t.selector}`,duration:performance.now()-r,nodeType:"hover",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Hover ${t.selector}`})}});import{z as Je}from"zod";var bn=Je.object({permission:Je.string().describe("Permission name (e.g. geolocation, notifications, camera, clipboard-read)"),state:Je.enum(["granted","prompt"]).describe("Permission state to set")}),or=u({description:"Set a browser permission state (geolocation, notifications, camera, etc.)",name:"set_permission",schema:bn,execute:async(e,t)=>{let r=performance.now(),o=e.page.context();t.state==="granted"?await o.grantPermissions([t.permission]):await o.clearPermissions();let a={id:`agent-step-${String(e.stepIndex)}`,permission:t.permission,state:t.state,type:"setPermission"};return h({assertions:[],detail:`Set ${t.permission} to ${t.state}`,duration:performance.now()-r,nodeType:"setPermission",page:e.page,runStartTime:e.runStartTime,specNode:a,status:"passed",stepIndex:e.stepIndex,title:`Set permission: ${t.permission} \u2192 ${t.state}`})}});import{z as nr}from"zod";var Sn=nr.object({url:nr.string().describe("The URL to navigate to")}),ar=u({description:"Navigate the browser to a URL",name:"navigate",schema:Sn,execute:async(e,t)=>{let r=performance.now();await e.page.goto(t.url,{waitUntil:"domcontentloaded"});let n={id:`agent-step-${String(e.stepIndex)}`,type:"goto",url:{type:"static",value:t.url}};return h({assertions:[],detail:`Navigated to ${t.url}`,duration:performance.now()-r,nodeType:"goto",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Navigate to ${t.url}`})}});import{z as sr}from"zod";var kn=sr.object({key:sr.string().describe("Key to press, e.g. 'Enter', 'Tab', 'Escape'")}),ir=u({description:"Press a keyboard key",name:"press",schema:kn,execute:async(e,t)=>{let r=performance.now();await e.page.keyboard.press(t.key);let n={id:`agent-step-${String(e.stepIndex)}`,key:t.key,type:"press"};return h({assertions:[],detail:`Pressed ${t.key}`,duration:performance.now()-r,nodeType:"press",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Press ${t.key}`})}});import{z as Ke}from"zod";var vn=Ke.object({height:Ke.number().int().positive().describe("Viewport height in pixels"),width:Ke.number().int().positive().describe("Viewport width in pixels")}),lr=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:vn,execute:async(e,t)=>{let r=performance.now();await e.page.setViewportSize({height:t.height,width:t.width});let o=`agent-step-${String(e.stepIndex)}`,n={height:t.height,id:o,type:"setViewport",width:t.width};return 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:n,status:"passed",stepIndex:e.stepIndex,title:`Resize viewport to ${String(t.width)}x${String(t.height)}`})}});import{z as cr}from"zod";var xn=cr.object({selector:cr.string().describe("CSS selector for the element to right-click")}),dr=u({description:"Right-click an element to open the context menu",name:"right_click",schema:xn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({button:"right",timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"rightClick"};return h({assertions:[],detail:`Right-clicked ${t.selector}`,duration:performance.now()-r,nodeType:"rightClick",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Right-click ${t.selector}`})}});import{z as Te}from"zod";var Rn=Te.object({selector:Te.string().optional().describe("CSS selector to scroll within (omit to scroll the page)"),x:Te.number().int().optional().describe("Horizontal scroll offset in pixels").default(0),y:Te.number().int().optional().describe("Vertical scroll offset in pixels").default(0)}),ur=u({description:"Scroll the page or a specific element by pixel offset. Positive y scrolls down, negative scrolls up.",name:"scroll",schema:Rn,execute:async(e,t)=>{let r=performance.now();if(t.selector==null)await e.page.mouse.wheel(t.x,t.y);else{let i=await e.page.locator(t.selector).elementHandle();if(i==null)throw new Error("Scroll target element not found");await i.evaluate(`(el) => el.scrollBy(${String(t.x)}, ${String(t.y)})`)}let n={id:`agent-step-${String(e.stepIndex)}`,type:"scroll",x:t.x,y:t.y},a=t.selector??"page";return h({assertions:[],detail:`Scrolled ${a} by (${String(t.x)}, ${String(t.y)})`,duration:performance.now()-r,nodeType:"scroll",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Scroll ${a}`})}});import{z as Pn}from"zod";var Tn=Pn.object({}),pr=u({description:"Take a screenshot of the current page",name:"screenshot",schema:Tn,execute:async(e,t)=>{let r=performance.now(),n={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:n,status:"passed",stepIndex:e.stepIndex,title:"Screenshot"})}});import{z as fr}from"zod";var Cn=fr.object({pattern:fr.string().describe("Regex pattern to search console log messages")}),mr=u({description:"Search console log messages for a regex pattern. Returns matching entries.",name:"search_console",schema:Cn,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),o=e.monitor.consoleEntries.filter(a=>r.test(a.text));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No console messages matching: ${t.pattern}`};let n=o.map(a=>An(a)).join(`
29
+ `)}`}async function or(e,t){let r=await e.evaluate(`JSON.stringify(Object.fromEntries(Object.entries(${t})))`);return`## ${t}
30
+ ${String(r)}`}import{z as Ye}from"zod";var Rn=Ye.object({action:Ye.enum(["accept","dismiss"]).describe("Whether to accept or dismiss the dialog"),promptText:Ye.string().optional().describe("Text to enter for prompt dialogs")}),ar=u({description:"Set up a handler for the next browser dialog (alert, confirm, prompt). Call this BEFORE the action that triggers the dialog.",name:"handle_dialog",schema:Rn,execute:(e,t)=>{let r=performance.now();e.page.once("dialog",async a=>{t.action==="accept"?await a.accept(t.promptText??void 0):await a.dismiss()});let o=`agent-step-${String(e.stepIndex)}`,n={action:t.action,id:o,type:"handleDialog"};return h({assertions:[],detail:`Dialog handler set: ${t.action}`,duration:performance.now()-r,nodeType:"handleDialog",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Handle dialog: ${t.action}`})}});import{z as sr}from"zod";var Pn=sr.object({selector:sr.string().describe("CSS selector for the element to hover over")}),ir=u({description:"Hover over an element matching the CSS selector",name:"hover",schema:Pn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).hover({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"hover"};return h({assertions:[],detail:`Hovered over ${t.selector}`,duration:performance.now()-r,nodeType:"hover",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Hover ${t.selector}`})}});import{z as Qe}from"zod";var Tn=Qe.object({permission:Qe.string().describe("Permission name (e.g. geolocation, notifications, camera, clipboard-read)"),state:Qe.enum(["granted","prompt"]).describe("Permission state to set")}),lr=u({description:"Set a browser permission state (geolocation, notifications, camera, etc.)",name:"set_permission",schema:Tn,execute:async(e,t)=>{let r=performance.now(),o=e.page.context();t.state==="granted"?await o.grantPermissions([t.permission]):await o.clearPermissions();let a={id:`agent-step-${String(e.stepIndex)}`,permission:t.permission,state:t.state,type:"setPermission"};return h({assertions:[],detail:`Set ${t.permission} to ${t.state}`,duration:performance.now()-r,nodeType:"setPermission",page:e.page,runStartTime:e.runStartTime,specNode:a,status:"passed",stepIndex:e.stepIndex,title:`Set permission: ${t.permission} \u2192 ${t.state}`})}});import{z as cr}from"zod";var Cn=cr.object({url:cr.string().describe("The URL to navigate to")}),dr=u({description:"Navigate the browser to a URL",name:"navigate",schema:Cn,execute:async(e,t)=>{let r=performance.now();await e.page.goto(t.url,{waitUntil:"domcontentloaded"});let n={id:`agent-step-${String(e.stepIndex)}`,type:"goto",url:{type:"static",value:t.url}};return h({assertions:[],detail:`Navigated to ${t.url}`,duration:performance.now()-r,nodeType:"goto",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Navigate to ${t.url}`})}});import{z as ur}from"zod";var An=ur.object({key:ur.string().describe("Key to press, e.g. 'Enter', 'Tab', 'Escape'")}),pr=u({description:"Press a keyboard key",name:"press",schema:An,execute:async(e,t)=>{let r=performance.now();await e.page.keyboard.press(t.key);let n={id:`agent-step-${String(e.stepIndex)}`,key:t.key,type:"press"};return h({assertions:[],detail:`Pressed ${t.key}`,duration:performance.now()-r,nodeType:"press",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Press ${t.key}`})}});import{z as Xe}from"zod";var Nn=Xe.object({height:Xe.number().int().positive().describe("Viewport height in pixels"),width:Xe.number().int().positive().describe("Viewport width in pixels")}),fr=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:Nn,execute:async(e,t)=>{let r=performance.now();await e.page.setViewportSize({height:t.height,width:t.width});let o=`agent-step-${String(e.stepIndex)}`,n={height:t.height,id:o,type:"setViewport",width:t.width};return 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:n,status:"passed",stepIndex:e.stepIndex,title:`Resize viewport to ${String(t.width)}x${String(t.height)}`})}});import{z as mr}from"zod";var En=mr.object({selector:mr.string().describe("CSS selector for the element to right-click")}),hr=u({description:"Right-click an element to open the context menu",name:"right_click",schema:En,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).click({button:"right",timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"rightClick"};return h({assertions:[],detail:`Right-clicked ${t.selector}`,duration:performance.now()-r,nodeType:"rightClick",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Right-click ${t.selector}`})}});import{z as Ae}from"zod";var In=Ae.object({selector:Ae.string().optional().describe("CSS selector to scroll within (omit to scroll the page)"),x:Ae.number().int().optional().describe("Horizontal scroll offset in pixels").default(0),y:Ae.number().int().optional().describe("Vertical scroll offset in pixels").default(0)}),gr=u({description:"Scroll the page or a specific element by pixel offset. Positive y scrolls down, negative scrolls up.",name:"scroll",schema:In,execute:async(e,t)=>{let r=performance.now();if(t.selector==null)await e.page.mouse.wheel(t.x,t.y);else{let i=await e.page.locator(t.selector).elementHandle();if(i==null)throw new Error("Scroll target element not found");await i.evaluate(`(el) => el.scrollBy(${String(t.x)}, ${String(t.y)})`)}let n={id:`agent-step-${String(e.stepIndex)}`,type:"scroll",x:t.x,y:t.y},a=t.selector??"page";return h({assertions:[],detail:`Scrolled ${a} by (${String(t.x)}, ${String(t.y)})`,duration:performance.now()-r,nodeType:"scroll",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Scroll ${a}`})}});import{z as $n}from"zod";var jn=$n.object({}),yr=u({description:"Take a screenshot of the current page",name:"screenshot",schema:jn,execute:async(e,t)=>{let r=performance.now(),n={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:n,status:"passed",stepIndex:e.stepIndex,title:"Screenshot"})}});import{z as wr}from"zod";var On=wr.object({pattern:wr.string().describe("Regex pattern to search console log messages")}),br=u({description:"Search console log messages for a regex pattern. Returns matching entries.",name:"search_console",schema:On,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),o=e.monitor.consoleEntries.filter(a=>r.test(a.text));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No console messages matching: ${t.pattern}`};let n=o.map(a=>Vn(a)).join(`
31
31
  `);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(o.length)} matching console messages:
32
- ${n}`}}});function An(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as hr}from"zod";var Nn=hr.object({pattern:hr.string().describe("Regex pattern to search across network request URLs and response bodies")}),gr=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:Nn,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),o=e.monitor.networkEntries.filter(a=>r.test(a.url)||a.responseBody!=null&&r.test(a.responseBody));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No network entries matching: ${t.pattern}`};let n=o.map(a=>{let i=a.statusCode==null?"pending":String(a.statusCode),d=r.test(a.url)?" [URL match]":"",l=a.responseBody!=null&&r.test(a.responseBody)?" [body match]":"";return`${a.method} ${i} ${a.url}${d}${l}`}).join(`
32
+ ${n}`}}});function Vn(e){let t=e.url.length>0?` (${e.url})`:"";return`[${e.level.toUpperCase()}] ${e.text}${t}`}import{z as Sr}from"zod";var Un=Sr.object({pattern:Sr.string().describe("Regex pattern to search across network request URLs and response bodies")}),kr=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:Un,execute:(e,t)=>{let r=new RegExp(t.pattern,"i"),o=e.monitor.networkEntries.filter(a=>r.test(a.url)||a.responseBody!=null&&r.test(a.responseBody));if(o.length===0)return{specNode:void 0,stepResult:void 0,toolOutput:`No network entries matching: ${t.pattern}`};let n=o.map(a=>{let i=a.statusCode==null?"pending":String(a.statusCode),c=r.test(a.url)?" [URL match]":"",l=a.responseBody!=null&&r.test(a.responseBody)?" [body match]":"";return`${a.method} ${i} ${a.url}${c}${l}`}).join(`
33
33
  `);return{specNode:void 0,stepResult:void 0,toolOutput:`${String(o.length)} matching network entries:
34
- ${n}`}}});import{z as yr}from"zod";var En=20,In=2,$n=yr.object({pattern:yr.string().describe("Regex pattern to search for in the page HTML")}),wr=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:$n,execute:async(e,t)=>{let o=(await e.page.content()).split(`
35
- `),n=jn({contextLines:In,lines:o,maxResults:En,pattern:t.pattern});return n.length===0?{specNode:void 0,stepResult:void 0,toolOutput:`No matches found for pattern: ${t.pattern}`}:{specNode:void 0,stepResult:void 0,toolOutput:n.join(`
34
+ ${n}`}}});import{z as vr}from"zod";var Fn=20,Wn=2,_n=vr.object({pattern:vr.string().describe("Regex pattern to search for in the page HTML")}),xr=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:_n,execute:async(e,t)=>{let o=(await e.page.content()).split(`
35
+ `),n=Ln({contextLines:Wn,lines:o,maxResults:Fn,pattern:t.pattern});return n.length===0?{specNode:void 0,stepResult:void 0,toolOutput:`No matches found for pattern: ${t.pattern}`}:{specNode:void 0,stepResult:void 0,toolOutput:n.join(`
36
36
  ---
37
- `)}}});function jn({contextLines:e,lines:t,maxResults:r,pattern:o}){let n=new RegExp(o,"i"),a=[];return t.forEach((i,d)=>{if(a.length>=r||!n.test(i))return;let l=Math.max(0,d-e),p=Math.min(t.length-1,d+e),g=t.slice(l,p+1).map((f,c)=>{let m=l+c+1;return`${l+c===d?">":" "} ${String(m)}: ${f}`}).join(`
38
- `);a.push(g)}),a}import{z as Ye}from"zod";var On=Ye.object({selector:Ye.string().describe("CSS selector for the select element"),value:Ye.string().describe("The option value to select")}),br=u({description:"Select an option from a dropdown/select element",name:"select",schema:On,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).selectOption(t.value,{timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"select",value:{type:"static",value:t.value}};return h({assertions:[],detail:`Selected "${t.value}" in ${t.selector}`,duration:performance.now()-r,nodeType:"select",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Select ${t.value}`})}});import{z as Sr}from"zod";var Vn=Sr.object({selector:Sr.string().describe("CSS selector for the checkbox to uncheck")}),kr=u({description:"Uncheck a checkbox matching the CSS selector",name:"uncheck",schema:Vn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).uncheck({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"uncheck"};return h({assertions:[],detail:`Unchecked ${t.selector}`,duration:performance.now()-r,nodeType:"uncheck",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Uncheck ${t.selector}`})}});import{z as vr}from"zod";var Un=vr.object({ms:vr.number().describe("Milliseconds to wait")}),xr=u({description:"Wait for a specified duration in milliseconds",name:"wait",schema:Un,execute:async(e,t)=>{let r=performance.now();await e.page.waitForTimeout(t.ms);let o=`agent-step-${String(e.stepIndex)}`,n={duration:t.ms,id:o,type:"wait"};return h({assertions:[],detail:`Waited ${String(t.ms)}ms`,duration:performance.now()-r,nodeType:"wait",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Wait ${String(t.ms)}ms`})}});import{z as Qe}from"zod";var Fn=Qe.object({selector:Qe.string().describe("CSS selector for the element to wait for"),state:Qe.enum(["visible","hidden","attached","detached"]).default("visible").describe("The state to wait for (default: visible)")}),Rr=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:Fn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).waitFor({state:t.state,timeout:15e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},state:t.state,type:"waitFor"};return h({assertions:[],detail:`Waited for ${t.selector} to be ${t.state}`,duration:performance.now()-r,nodeType:"waitFor",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Wait for ${t.selector} to be ${t.state}`})}});var Xe=[ar,Ot,Dt,ir,Et,kr,br,rr,xr,Rr,pr,Kt,wr,$t,dr,ur,er,Vt,or,At,Pt,Tt,bt,xt,kt,Rt,qt,Mt,zt,Yt,Jt,Zt,mr,gr,lr,Lt,Ft],Su=new Map(Xe.map(e=>[e.name,e])),_n=Xe.map(e=>e.anthropicTool),Ln=Xe.map(e=>`- **${e.name}**: ${e.anthropicTool.description??""}`).join(`
39
- `);import Dn from"pino";var D=Dn({transport:{options:{ignore:"pid,hostname"},target:"pino-pretty"}});import{execFileSync as Vu}from"child_process";import{createRequire as Fu}from"module";import Lu from"fs";import Wu from"path";import{chromium as Bu}from"playwright";import{mkdir as Hu,writeFile as Ju}from"fs/promises";import Yu from"path";import{readdir as lp,rm as cp,stat as dp}from"fs/promises";import pp from"os";import Hn from"path";import{graphql as Jn}from"gql.tada";import{print as Mn}from"graphql";async function X(e){let t=Mn(e.document),r=JSON.stringify({query:t,variables:e.variables}),o=`${e.config.ripploServerUrl}/graphql`,n;try{n=await fetch(o,{body:r,headers:{Authorization:`Bearer ${e.config.token}`,"Content-Type":"application/json"},method:"POST"})}catch(i){let d=i instanceof Error?i.message:String(i);throw new Error(`Failed to connect to Ripplo server at ${e.config.ripploServerUrl}. Make sure \`ripplo dev\` is running and your internet connection is active. Details: ${d}`)}let a=await n.json();if(!Gn(a))throw new Error("Invalid GraphQL response");if(zn(a))throw new Error(a.errors.map(i=>i.message).join(", "));if(a.data==null)throw new Error("No data returned from server");return a.data}function Gn(e){return typeof e=="object"&&e!=null&&("data"in e||"errors"in e)}function zn(e){return Array.isArray(e.errors)&&e.errors.length>0}var yp=Hn.join(process.cwd(),".ripplo","debug"),wp=360*60*1e3,bp=Jn(`
37
+ `)}}});function Ln({contextLines:e,lines:t,maxResults:r,pattern:o}){let n=new RegExp(o,"i"),a=[];return t.forEach((i,c)=>{if(a.length>=r||!n.test(i))return;let l=Math.max(0,c-e),p=Math.min(t.length-1,c+e),g=t.slice(l,p+1).map((f,d)=>{let m=l+d+1;return`${l+d===c?">":" "} ${String(m)}: ${f}`}).join(`
38
+ `);a.push(g)}),a}import{z as Ze}from"zod";var Dn=Ze.object({selector:Ze.string().describe("CSS selector for the select element"),value:Ze.string().describe("The option value to select")}),Rr=u({description:"Select an option from a dropdown/select element",name:"select",schema:Dn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).selectOption(t.value,{timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"select",value:{type:"static",value:t.value}};return h({assertions:[],detail:`Selected "${t.value}" in ${t.selector}`,duration:performance.now()-r,nodeType:"select",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Select ${t.value}`})}});import{z as Pr}from"zod";var qn=Pr.object({selector:Pr.string().describe("CSS selector for the checkbox to uncheck")}),Tr=u({description:"Uncheck a checkbox matching the CSS selector",name:"uncheck",schema:qn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).uncheck({timeout:5e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},type:"uncheck"};return h({assertions:[],detail:`Unchecked ${t.selector}`,duration:performance.now()-r,nodeType:"uncheck",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Uncheck ${t.selector}`})}});import{z as Cr}from"zod";var Bn=Cr.object({ms:Cr.number().describe("Milliseconds to wait")}),Ar=u({description:"Wait for a specified duration in milliseconds",name:"wait",schema:Bn,execute:async(e,t)=>{let r=performance.now();await e.page.waitForTimeout(t.ms);let o=`agent-step-${String(e.stepIndex)}`,n={duration:t.ms,id:o,type:"wait"};return h({assertions:[],detail:`Waited ${String(t.ms)}ms`,duration:performance.now()-r,nodeType:"wait",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Wait ${String(t.ms)}ms`})}});import{z as et}from"zod";var Mn=et.object({selector:et.string().describe("CSS selector for the element to wait for"),state:et.enum(["visible","hidden","attached","detached"]).default("visible").describe("The state to wait for (default: visible)")}),Nr=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:Mn,execute:async(e,t)=>{let r=performance.now();await e.page.locator(t.selector).waitFor({state:t.state,timeout:15e3});let n={id:`agent-step-${String(e.stepIndex)}`,locator:{by:"css",value:t.selector},state:t.state,type:"waitFor"};return h({assertions:[],detail:`Waited for ${t.selector} to be ${t.state}`,duration:performance.now()-r,nodeType:"waitFor",page:e.page,runStartTime:e.runStartTime,specNode:n,status:"passed",stepIndex:e.stepIndex,title:`Wait for ${t.selector} to be ${t.state}`})}});var tt=[dr,_t,Gt,pr,Vt,Tr,Rr,ir,Ar,Nr,yr,er,xr,Ft,hr,gr,ar,Lt,lr,jt,Et,It,Rt,At,Tt,Nt,Ht,Kt,Qt,tr,Zt,nr,br,kr,fr,Mt,qt],Mu=new Map(tt.map(e=>[e.name,e])),Gn=tt.map(e=>e.anthropicTool),zn=tt.map(e=>`- **${e.name}**: ${e.anthropicTool.description??""}`).join(`
39
+ `);import Ju from"fs";import Yu from"path";import Hn from"pino";var _=Hn({transport:{options:{ignore:"pid,hostname"},target:"pino-pretty"}});import{execFile as cp}from"child_process";import{createRequire as up}from"module";import fp from"fs";import hp from"path";import{chromium as yp}from"playwright";import{mkdir as kp,writeFile as vp}from"fs/promises";import Rp from"path";import{readdir as Up,rm as Fp,stat as Wp}from"fs/promises";import Lp from"os";import ea from"path";import{graphql as ta}from"gql.tada";import{print as Qn}from"graphql";async function X(e){let t=Qn(e.document),r=JSON.stringify({query:t,variables:e.variables}),o=`${e.config.ripploServerUrl}/graphql`,n;try{n=await fetch(o,{body:r,headers:{Authorization:`Bearer ${e.config.token}`,"Content-Type":"application/json"},method:"POST"})}catch(i){let c=i instanceof Error?i.message:String(i);throw new Error(`Failed to connect to Ripplo server at ${e.config.ripploServerUrl}. Make sure \`ripplo dev\` is running and your internet connection is active. Details: ${c}`)}let a=await n.json();if(!Xn(a))throw new Error("Invalid GraphQL response");if(Zn(a))throw new Error(a.errors.map(i=>i.message).join(", "));if(a.data==null)throw new Error("No data returned from server");return a.data}function Xn(e){return typeof e=="object"&&e!=null&&("data"in e||"errors"in e)}function Zn(e){return Array.isArray(e.errors)&&e.errors.length>0}var Gp=ea.join(process.cwd(),".ripplo","debug"),zp=360*60*1e3,Hp=ta(`
40
40
  mutation RevokeCurrentCliToken {
41
41
  revokeCurrentCliToken
42
42
  }
43
- `);import Er from"fs";import Ze from"path";import{z as K}from"zod";var Qn=K.object({baseUrl:K.string().optional(),cloudBaseUrl:K.string(),preconditionApiPath:K.string().optional(),projectId:K.string()}),Xn=K.object({ripploServerUrl:K.string(),token:K.string(),webhookSecret:K.string()}),Zn=Qn.extend(Xn.shape),$r=Ze.join(process.cwd(),".ripplo"),ea=Ze.join($r,"settings.json"),ta=Ze.join($r,"settings.local.json");function Ir(e){if(!Er.existsSync(e))return null;let t=Er.readFileSync(e,"utf8");return JSON.parse(t)}function et(){let e=Ir(ea),t=Ir(ta);if(e==null||t==null)return null;let r=Zn.safeParse({...e,...t});return r.success?r.data:null}import{z as s}from"zod";import{z as M}from"zod";import{z as R}from"zod";var oa=R.object({by:R.literal("css"),value:R.string().min(1)}),na=R.object({by:R.literal("testId"),value:R.string().min(1)}),aa=R.object({by:R.literal("role"),exact:R.boolean().optional(),name:R.string().optional(),role:R.string().min(1)}),sa=R.object({by:R.literal("text"),exact:R.boolean().optional(),value:R.string().min(1)}),ia=R.object({by:R.literal("label"),exact:R.boolean().optional(),value:R.string().min(1)}),la=R.object({by:R.literal("placeholder"),value:R.string().min(1)}),ca=R.object({by:R.literal("altText"),value:R.string().min(1)}),b=R.discriminatedUnion("by",[oa,na,aa,sa,ia,la,ca]);import{z as jr}from"zod";var U=jr.enum(["equals","notEquals","contains","startsWith","endsWith","matches"]),Y=jr.enum(["equals","notEquals","greaterThan","greaterThanOrEqual","lessThan","lessThanOrEqual"]);import{z as j}from"zod";var da=j.object({type:j.literal("static"),value:j.union([j.string(),j.number(),j.boolean()])}),tt=j.object({name:j.string().min(1),type:j.literal("variable")}),Z=j.discriminatedUnion("type",[da,tt]),x=j.discriminatedUnion("type",[j.object({type:j.literal("static"),value:j.string()}),tt]),re=j.discriminatedUnion("type",[j.object({type:j.literal("static"),value:j.number().int().nonnegative()}),tt]);var oe=M.discriminatedUnion("type",[M.object({locator:b,type:M.literal("elementVisible")}),M.object({locator:b,type:M.literal("elementNotVisible")}),M.object({expected:x,operator:U,type:M.literal("urlMatch")}),M.object({expected:x,locator:b,operator:U,type:M.literal("textMatch")}),M.object({expected:Z,operator:Y,type:M.literal("variableCompare"),variable:M.string().min(1)})]);import{z as W}from"zod";var ce=W.discriminatedUnion("type",[W.object({default:W.string().optional(),type:W.literal("string")}),W.object({default:W.number().optional(),type:W.literal("number")}),W.object({default:W.boolean().optional(),type:W.literal("boolean")}),W.object({key:W.string().min(1),type:W.literal("env")})]);var ua=s.string().min(1),Q=s.array(ua).min(1),w={comment:s.string().optional(),id:s.string().min(1),label:s.string().optional(),next:s.string().optional(),timeout:s.number().int().positive().optional()},pa=s.object({...w,type:s.literal("goto"),url:x}),fa=s.object({...w,locator:b,type:s.literal("click")}),ma=s.object({...w,locator:b,type:s.literal("fill"),value:x}),ha=s.object({...w,locator:b,type:s.literal("select"),value:x}),ga=s.object({...w,locator:b,type:s.literal("hover")}),ya=s.object({...w,key:s.string().min(1),locator:b.optional(),type:s.literal("press")}),wa=s.object({...w,locator:b,type:s.literal("check")}),ba=s.object({...w,locator:b,type:s.literal("uncheck")}),Sa=s.object({...w,type:s.literal("screenshot")}),ka=s.object({...w,height:s.number().int().positive(),type:s.literal("setViewport"),width:s.number().int().positive()}),va=s.object({...w,duration:s.number().int().positive(),type:s.literal("wait")}),xa=s.object({...w,message:s.string().min(1),type:s.literal("fail")}),Ra=s.object({...w,type:s.literal("setVariable"),value:Z,variable:s.string().min(1)}),Pa=s.object({...w,locator:b,type:s.literal("extractText"),variable:s.string().min(1)}),Ta=s.object({...w,files:s.array(s.string()).min(1),locator:b,type:s.literal("upload")}),Ca=s.object({...w,locator:b,type:s.literal("dblclick")}),Aa=s.object({...w,source:b,target:b,type:s.literal("drag")}),Na=s.object({...w,locator:b,type:s.literal("scrollIntoView")}),Ea=s.object({...w,locator:b,type:s.literal("type"),value:x}),Ia=s.object({...w,locator:b,type:s.literal("focus")}),$a=s.object({...w,locator:b,type:s.literal("clear")}),ja=s.object({...w,locator:b,type:s.literal("rightClick")}),Oa=s.object({...w,action:s.enum(["accept","dismiss"]),promptText:s.string().optional(),type:s.literal("handleDialog")}),Va=s.object({...w,locator:b.optional(),type:s.literal("scroll"),x:s.number().int().optional(),y:s.number().int().optional()}),Ua=s.object({...w,action:s.enum(["read","write"]),type:s.literal("clipboard"),value:x.optional(),variable:s.string().min(1).optional()}),Fa=s.object({...w,permission:s.string().min(1),state:s.enum(["granted","prompt"]),type:s.literal("setPermission")}),_a=s.object({...w,locator:b,state:s.enum(["visible","hidden","attached","detached"]).optional(),type:s.literal("waitFor")}),La=s.object({...w,expected:x,operator:U,type:s.literal("waitForUrl")}),Da=s.object({...w,type:s.literal("waitForResponse"),urlPattern:x}),Wa=s.object({...w,type:s.literal("waitForRequest"),urlPattern:x}),qa=s.object({...w,locator:b,type:s.literal("assertVisible")}),Ba=s.object({...w,locator:b,type:s.literal("assertNotVisible")}),Ma=s.object({...w,expected:x,locator:b,operator:U,type:s.literal("assertText")}),Ga=s.object({...w,expected:x,operator:U,type:s.literal("assertUrl")}),za=s.object({...w,expected:re,locator:b,operator:Y,type:s.literal("assertCount")}),Ha=s.object({...w,expected:x,locator:b,operator:U,type:s.literal("assertValue")}),Ja=s.object({...w,attribute:s.string().min(1),expected:x,locator:b,operator:U,type:s.literal("assertAttribute")}),Ka=s.object({...w,locator:b,type:s.literal("assertEnabled")}),Ya=s.object({...w,locator:b,type:s.literal("assertDisabled")}),Qa=s.object({...w,expected:x,operator:U,type:s.literal("assertTitle")}),Xa=s.object({...w,locator:b,type:s.literal("assertChecked")}),Za=s.object({...w,locator:b,type:s.literal("assertNotChecked")}),es=s.object({...w,locator:b,type:s.literal("assertFocused")}),ts=s.object({...w,httpOnly:s.boolean().optional(),name:x,operator:U.optional(),sameSite:s.enum(["Strict","Lax","None"]).optional(),secure:s.boolean().optional(),type:s.literal("assertCookie"),value:x.optional()}),rs=s.object({...w,bodyContains:x.optional(),headerContains:s.object({name:s.string().min(1),value:x}).optional(),status:s.number().int().positive().optional(),type:s.literal("assertResponse"),urlPattern:x}),os=s.object({...w,alternate:Q.optional(),condition:oe,consequent:Q,type:s.literal("if")}),ns=s.object({...w,body:Q,iteratorVar:s.string().min(1).optional(),times:s.number().int().positive(),type:s.literal("loop")}),as=s.object({...w,body:Q,collection:s.union([s.array(s.record(s.string(),s.unknown())).min(1),s.object({name:s.string().min(1),type:s.literal("variable")})]),iteratorVar:s.string().min(1),type:s.literal("forEach")}),ss=s.object({...w,branches:s.array(Q).min(2),type:s.literal("parallel")}),is=s.object({...w,body:Q,catch:Q.optional(),finally:Q.optional(),type:s.literal("try")}).refine(e=>e.catch!=null||e.finally!=null,{message:"try node must have at least one of 'catch' or 'finally'"}),ls=s.object({...w,nodes:Q,type:s.literal("group")}),we=s.discriminatedUnion("type",[pa,fa,ma,ha,ga,ya,wa,ba,_a,La,Da,Wa,qa,Ba,Ma,Ga,za,Ha,Ja,Ka,Ya,Sa,ka,va,xa,Ra,Pa,Ta,Ca,Aa,Na,Ea,Ia,$a,ja,Oa,Va,Ua,Fa,Qa,Xa,Za,es,ts,rs,os,ns,as,ss,ls]),Or=s.union([we,is]),ne=s.object({entryNode:s.string().min(1),nodes:s.record(s.string(),Or),variables:s.record(s.string(),ce).optional(),version:s.literal(2)});function Ce(e){let t=ne.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 Ve,readFragment as Om}from"gql.tada";import{z as Ae}from"zod";var rt=Ae.object({from:Ae.string().min(1).describe("Key of the source state in the states record"),to:Ae.string().min(1).describe("Key of the target state in the states record"),workflow:Ae.string().min(1).describe("Filename (without .json) of the workflow in .ripplo/workflows/ that executes this edge")}).describe("A directed edge between two states, executed by a workflow");import{z as ee}from"zod";import{z as C}from"zod";var ot=C.object({precondition:C.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"),Ne=C.object({satisfied:C.boolean().describe("Whether the precondition is already satisfied. If true, execution is skipped.")}).describe("Response from POST {preconditionApiUrl}/check"),Ee=C.object({data:C.record(C.string(),C.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:C.string().optional().describe("Human-readable error message if success is false"),navigateTo:C.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:C.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."),nt=C.object({preconditions:C.array(C.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."),at=C.object({error:C.string().optional().describe("Human-readable error message if success is false"),success:C.boolean().describe("Whether teardown completed successfully")}).describe("Response from PUT {preconditionApiUrl}/teardown"),ae=C.object({depends:C.array(C.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:C.string().min(1).describe("Human-readable description of what this precondition ensures"),returns:C.array(C.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 Ie}from"zod";var st=Ie.object({preconditions:Ie.array(Ie.string().min(1)).describe("Ordered list of precondition names to satisfy before entering this state"),route:Ie.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 be=ee.object({edges:ee.array(rt).describe("Directed edges between states, each executed by a workflow"),preconditions:ee.record(ee.string(),ae).describe("Named preconditions keyed by name (e.g. 'auth:admin', 'data:three-projects'). States reference these by name."),resetPrecondition:ee.string().optional().describe("Name of a precondition to run before every state setup as a global clean slate"),states:ee.record(ee.string(),st).describe("States keyed by stable ID (kebab-case)"),version:ee.literal(3).describe("Schema version, always 3")}).describe("Ripplo State Graph v3 \u2014 models application states, edges, and executable preconditions");function it(e){let t=be.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 vs(e){let t=e.preconditions,r=new Set,o=new Set,n=[];function a(i){if(o.has(i)){n.push(`Precondition "${i}" has a circular dependency`);return}if(r.has(i))return;r.add(i),o.add(i);let d=t[i];d?.depends!=null&&d.depends.forEach(l=>{a(l)}),o.delete(i)}return Object.keys(t).forEach(i=>{a(i)}),n}function lt(e){let t=[],r=new Set(Object.keys(e.preconditions)),o=new Set(Object.keys(e.states));e.edges.forEach((l,p)=>{o.has(l.from)||t.push({message:`References non-existent source state "${l.from}"`,path:`edges[${String(p)}].from`}),o.has(l.to)||t.push({message:`References non-existent target state "${l.to}"`,path:`edges[${String(p)}].to`})}),Object.entries(e.states).forEach(([l,p])=>{p.preconditions.forEach(g=>{r.has(g)||t.push({message:`References non-existent precondition "${g}"`,path:`states.${l}.preconditions`})})}),Object.entries(e.preconditions).forEach(([l,p])=>{p.depends!=null&&p.depends.forEach(g=>{r.has(g)||t.push({message:`Depends on non-existent precondition "${g}"`,path:`preconditions.${l}.depends`})})}),e.resetPrecondition!=null&&!r.has(e.resetPrecondition)&&t.push({message:`References non-existent precondition "${e.resetPrecondition}"`,path:"resetPrecondition"});let n=new Set(e.edges.flatMap(l=>[l.from,l.to]));Object.keys(e.states).forEach(l=>{n.has(l)||t.push({message:"State is not referenced by any edge (orphan)",path:`states.${l}`})});let a=new Set;Object.values(e.states).forEach(l=>{l.preconditions.forEach(p=>a.add(p))}),Object.values(e.preconditions).forEach(l=>{l.depends!=null&&l.depends.forEach(p=>a.add(p))}),e.resetPrecondition!=null&&a.add(e.resetPrecondition),Object.keys(e.preconditions).forEach(l=>{a.has(l)||t.push({message:"Defined but never used by any state or dependency",path:`preconditions.${l}`})});let i=new Set;return e.edges.forEach((l,p)=>{let g=`${l.from}|${l.to}|${l.workflow}`;i.has(g)&&t.push({message:`Duplicate edge from "${l.from}" to "${l.to}" with workflow "${l.workflow}"`,path:`edges[${String(p)}]`}),i.add(g)}),vs(e).forEach(l=>t.push({message:l,path:"preconditions"})),t}import{parseSetCookie as wm}from"set-cookie-parser";import{z as zr}from"zod";var Is=jo(Gr(),1);import pm from"crypto";var vm=zr.record(zr.string(),ae);import{graphql as ut}from"gql.tada";var Cm=ut(`
43
+ `);import Vr from"fs";import rt from"path";import{z as J}from"zod";var na=J.object({baseUrl:J.string(),cloudBaseUrl:J.string(),preconditionApiPath:J.string(),projectId:J.string()}),aa=J.object({ripploServerUrl:J.string(),token:J.string(),webhookSecret:J.string()}),sa=na.extend(aa.shape),Fr=rt.join(process.cwd(),".ripplo"),ia=rt.join(Fr,"settings.json"),la=rt.join(Fr,"settings.local.json");function Ur(e){if(!Vr.existsSync(e))return null;let t=Vr.readFileSync(e,"utf8");return JSON.parse(t)}function ot(){let e=Ur(ia),t=Ur(la);if(e==null||t==null)return null;let r=sa.safeParse({...e,...t});return r.success?r.data:null}import{z as s}from"zod";import{z as q}from"zod";import{z as R}from"zod";var da=R.object({by:R.literal("css"),value:R.string().min(1)}),ua=R.object({by:R.literal("testId"),value:R.string().min(1)}),pa=R.object({by:R.literal("role"),exact:R.boolean().optional(),name:R.string().optional(),role:R.string().min(1)}),fa=R.object({by:R.literal("text"),exact:R.boolean().optional(),value:R.string().min(1)}),ma=R.object({by:R.literal("label"),exact:R.boolean().optional(),value:R.string().min(1)}),ha=R.object({by:R.literal("placeholder"),value:R.string().min(1)}),ga=R.object({by:R.literal("altText"),value:R.string().min(1)}),b=R.discriminatedUnion("by",[da,ua,pa,fa,ma,ha,ga]);import{z as Wr}from"zod";var U=Wr.enum(["equals","notEquals","contains","startsWith","endsWith","matches"]),K=Wr.enum(["equals","notEquals","greaterThan","greaterThanOrEqual","lessThan","lessThanOrEqual"]);import{z as j}from"zod";var ya=j.object({type:j.literal("static"),value:j.union([j.string(),j.number(),j.boolean()])}),nt=j.object({name:j.string().min(1),type:j.literal("variable")}),Z=j.discriminatedUnion("type",[ya,nt]),x=j.discriminatedUnion("type",[j.object({type:j.literal("static"),value:j.string()}),nt]),oe=j.discriminatedUnion("type",[j.object({type:j.literal("static"),value:j.number().int().nonnegative()}),nt]);var ne=q.discriminatedUnion("type",[q.object({locator:b,type:q.literal("elementVisible")}),q.object({locator:b,type:q.literal("elementNotVisible")}),q.object({expected:x,operator:U,type:q.literal("urlMatch")}),q.object({expected:x,locator:b,operator:U,type:q.literal("textMatch")}),q.object({expected:Z,operator:K,type:q.literal("variableCompare"),variable:q.string().min(1)})]);import{z as L}from"zod";var ue=L.discriminatedUnion("type",[L.object({default:L.string().optional(),type:L.literal("string")}),L.object({default:L.number().optional(),type:L.literal("number")}),L.object({default:L.boolean().optional(),type:L.literal("boolean")}),L.object({key:L.string().min(1),type:L.literal("env")})]);var wa=s.string().min(1),Y=s.array(wa).min(1),w={comment:s.string().optional(),id:s.string().min(1),label:s.string().optional(),next:s.string().optional(),timeout:s.number().int().positive().optional()},ba=s.object({...w,type:s.literal("goto"),url:x}),Sa=s.object({...w,locator:b,type:s.literal("click")}),ka=s.object({...w,locator:b,type:s.literal("fill"),value:x}),va=s.object({...w,locator:b,type:s.literal("select"),value:x}),xa=s.object({...w,locator:b,type:s.literal("hover")}),Ra=s.object({...w,key:s.string().min(1),locator:b.optional(),type:s.literal("press")}),Pa=s.object({...w,locator:b,type:s.literal("check")}),Ta=s.object({...w,locator:b,type:s.literal("uncheck")}),Ca=s.object({...w,type:s.literal("screenshot")}),Aa=s.object({...w,height:s.number().int().positive(),type:s.literal("setViewport"),width:s.number().int().positive()}),Na=s.object({...w,duration:s.number().int().positive(),type:s.literal("wait")}),Ea=s.object({...w,message:s.string().min(1),type:s.literal("fail")}),Ia=s.object({...w,type:s.literal("setVariable"),value:Z,variable:s.string().min(1)}),$a=s.object({...w,locator:b,type:s.literal("extractText"),variable:s.string().min(1)}),ja=s.object({...w,files:s.array(s.string()).min(1),locator:b,type:s.literal("upload")}),Oa=s.object({...w,locator:b,type:s.literal("dblclick")}),Va=s.object({...w,source:b,target:b,type:s.literal("drag")}),Ua=s.object({...w,locator:b,type:s.literal("scrollIntoView")}),Fa=s.object({...w,locator:b,type:s.literal("type"),value:x}),Wa=s.object({...w,locator:b,type:s.literal("focus")}),_a=s.object({...w,locator:b,type:s.literal("clear")}),La=s.object({...w,locator:b,type:s.literal("rightClick")}),Da=s.object({...w,action:s.enum(["accept","dismiss"]),promptText:s.string().optional(),type:s.literal("handleDialog")}),qa=s.object({...w,locator:b.optional(),type:s.literal("scroll"),x:s.number().int().optional(),y:s.number().int().optional()}),Ba=s.object({...w,action:s.enum(["read","write"]),type:s.literal("clipboard"),value:x.optional(),variable:s.string().min(1).optional()}),Ma=s.object({...w,permission:s.string().min(1),state:s.enum(["granted","prompt"]),type:s.literal("setPermission")}),Ga=s.object({...w,locator:b,state:s.enum(["visible","hidden","attached","detached"]).optional(),type:s.literal("waitFor")}),za=s.object({...w,expected:x,operator:U,type:s.literal("waitForUrl")}),Ha=s.object({...w,type:s.literal("waitForResponse"),urlPattern:x}),Ja=s.object({...w,type:s.literal("waitForRequest"),urlPattern:x}),Ka=s.object({...w,locator:b,type:s.literal("assertVisible")}),Ya=s.object({...w,locator:b,type:s.literal("assertNotVisible")}),Qa=s.object({...w,expected:x,locator:b,operator:U,type:s.literal("assertText")}),Xa=s.object({...w,expected:x,operator:U,type:s.literal("assertUrl")}),Za=s.object({...w,expected:oe,locator:b,operator:K,type:s.literal("assertCount")}),es=s.object({...w,expected:x,locator:b,operator:U,type:s.literal("assertValue")}),ts=s.object({...w,attribute:s.string().min(1),expected:x,locator:b,operator:U,type:s.literal("assertAttribute")}),rs=s.object({...w,locator:b,type:s.literal("assertEnabled")}),os=s.object({...w,locator:b,type:s.literal("assertDisabled")}),ns=s.object({...w,expected:x,operator:U,type:s.literal("assertTitle")}),as=s.object({...w,locator:b,type:s.literal("assertChecked")}),ss=s.object({...w,locator:b,type:s.literal("assertNotChecked")}),is=s.object({...w,locator:b,type:s.literal("assertFocused")}),ls=s.object({...w,httpOnly:s.boolean().optional(),name:x,operator:U.optional(),sameSite:s.enum(["Strict","Lax","None"]).optional(),secure:s.boolean().optional(),type:s.literal("assertCookie"),value:x.optional()}),cs=s.object({...w,bodyContains:x.optional(),headerContains:s.object({name:s.string().min(1),value:x}).optional(),status:s.number().int().positive().optional(),type:s.literal("assertResponse"),urlPattern:x}),ds=s.object({...w,alternate:Y.optional(),condition:ne,consequent:Y,type:s.literal("if")}),us=s.object({...w,body:Y,iteratorVar:s.string().min(1).optional(),times:s.number().int().positive(),type:s.literal("loop")}),ps=s.object({...w,body:Y,collection:s.union([s.array(s.record(s.string(),s.unknown())).min(1),s.object({name:s.string().min(1),type:s.literal("variable")})]),iteratorVar:s.string().min(1),type:s.literal("forEach")}),fs=s.object({...w,branches:s.array(Y).min(2),type:s.literal("parallel")}),ms=s.object({...w,body:Y,catch:Y.optional(),finally:Y.optional(),type:s.literal("try")}).refine(e=>e.catch!=null||e.finally!=null,{message:"try node must have at least one of 'catch' or 'finally'"}),hs=s.object({...w,nodes:Y,type:s.literal("group")}),Se=s.discriminatedUnion("type",[ba,Sa,ka,va,xa,Ra,Pa,Ta,Ga,za,Ha,Ja,Ka,Ya,Qa,Xa,Za,es,ts,rs,os,Ca,Aa,Na,Ea,Ia,$a,ja,Oa,Va,Ua,Fa,Wa,_a,La,Da,qa,Ba,Ma,ns,as,ss,is,ls,cs,ds,us,ps,fs,hs]),_r=s.union([Se,ms]),ae=s.object({entryNode:s.string().min(1),nodes:s.record(s.string(),_r),variables:s.record(s.string(),ue).optional(),version:s.literal(2)});function Ne(e){let t=ae.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 Fe,readFragment as lh}from"gql.tada";import{z as Ee}from"zod";var at=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 ee}from"zod";import{z as C}from"zod";var st=C.object({precondition:C.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"),Ie=C.object({satisfied:C.boolean().describe("Whether the precondition is already satisfied. If true, execution is skipped.")}).describe("Response from POST {preconditionApiUrl}/check"),$e=C.object({data:C.record(C.string(),C.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:C.string().optional().describe("Human-readable error message if success is false"),navigateTo:C.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:C.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."),it=C.object({preconditions:C.array(C.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."),lt=C.object({error:C.string().optional().describe("Human-readable error message if success is false"),success:C.boolean().describe("Whether teardown completed successfully")}).describe("Response from PUT {preconditionApiUrl}/teardown"),se=C.object({depends:C.array(C.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:C.string().min(1).describe("Human-readable description of what this precondition ensures"),returns:C.array(C.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 je}from"zod";var ct=je.object({preconditions:je.array(je.string().min(1)).describe("Ordered list of precondition names to satisfy before entering this state"),route:je.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 ke=ee.object({edges:ee.array(at).describe("Directed edges between states, each executed by a workflow"),preconditions:ee.record(ee.string(),se).describe("Named preconditions keyed by name (e.g. 'auth:admin', 'data:three-projects'). States reference these by name."),resetPrecondition:ee.string().optional().describe("Name of a precondition to run before every state setup as a global clean slate"),states:ee.record(ee.string(),ct).describe("States keyed by stable ID (kebab-case)"),version:ee.literal(3).describe("Schema version, always 3")}).describe("Ripplo State Graph v3 \u2014 models application states, edges, and executable preconditions");function dt(e){let t=ke.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 Ns(e){let t=e.preconditions,r=new Set,o=new Set,n=[];function a(i){if(o.has(i)){n.push(`Precondition "${i}" has a circular dependency`);return}if(r.has(i))return;r.add(i),o.add(i);let c=t[i];c?.depends!=null&&c.depends.forEach(l=>{a(l)}),o.delete(i)}return Object.keys(t).forEach(i=>{a(i)}),n}function ut(e){let t=[],r=new Set(Object.keys(e.preconditions)),o=new Set(Object.keys(e.states));e.edges.forEach((l,p)=>{o.has(l.from)||t.push({message:`References non-existent source state "${l.from}"`,path:`edges[${String(p)}].from`}),o.has(l.to)||t.push({message:`References non-existent target state "${l.to}"`,path:`edges[${String(p)}].to`})}),Object.entries(e.states).forEach(([l,p])=>{p.preconditions.forEach(g=>{r.has(g)||t.push({message:`References non-existent precondition "${g}"`,path:`states.${l}.preconditions`})})}),Object.entries(e.preconditions).forEach(([l,p])=>{p.depends!=null&&p.depends.forEach(g=>{r.has(g)||t.push({message:`Depends on non-existent precondition "${g}"`,path:`preconditions.${l}.depends`})})}),e.resetPrecondition!=null&&!r.has(e.resetPrecondition)&&t.push({message:`References non-existent precondition "${e.resetPrecondition}"`,path:"resetPrecondition"});let n=new Set(e.edges.flatMap(l=>[l.from,l.to]));Object.keys(e.states).forEach(l=>{n.has(l)||t.push({message:"State is not referenced by any edge (orphan)",path:`states.${l}`})});let a=new Set;Object.values(e.states).forEach(l=>{l.preconditions.forEach(p=>a.add(p))}),Object.values(e.preconditions).forEach(l=>{l.depends!=null&&l.depends.forEach(p=>a.add(p))}),e.resetPrecondition!=null&&a.add(e.resetPrecondition),Object.keys(e.preconditions).forEach(l=>{a.has(l)||t.push({message:"Defined but never used by any state or dependency",path:`preconditions.${l}`})});let i=new Set;return e.edges.forEach((l,p)=>{let g=`${l.from}|${l.to}|${l.workflow}`;i.has(g)&&t.push({message:`Duplicate edge from "${l.from}" to "${l.to}" with workflow "${l.workflow}"`,path:`edges[${String(p)}]`}),i.add(g)}),Ns(e).forEach(l=>t.push({message:l,path:"preconditions"})),t}import{parseSetCookie as zm}from"set-cookie-parser";import{z as Qr}from"zod";var Ws=Lo(Yr(),1);import Lm from"crypto";var Ym=Qr.record(Qr.string(),se);import{graphql as mt}from"gql.tada";var th=mt(`
44
44
  mutation StartRunCLI($runId: String!, $platform: String!, $agentProfileId: String) {
45
45
  startRun(runId: $runId, platform: $platform, agentProfileId: $agentProfileId) {
46
46
  id
47
47
  }
48
48
  }
49
- `),Am=ut(`
49
+ `),rh=mt(`
50
50
  mutation SubmitRunStepsCLI($runResultId: String!, $steps: [StepInput!]!) {
51
51
  submitRunSteps(runResultId: $runResultId, steps: $steps)
52
52
  }
53
- `),Nm=ut(`
53
+ `),oh=mt(`
54
54
  mutation CompleteRunCLI(
55
55
  $runResultId: String!
56
56
  $status: String!
@@ -73,14 +73,14 @@ ${n}`}}});import{z as yr}from"zod";var En=20,In=2,$n=yr.object({pattern:yr.strin
73
73
  status
74
74
  }
75
75
  }
76
- `);var Ls=Ve(`
76
+ `);var zs=Fe(`
77
77
  fragment WorkflowRun on Workflow {
78
78
  id
79
79
  slug
80
80
  name
81
81
  spec
82
82
  }
83
- `),Ds=Ve(`
83
+ `),Hs=Fe(`
84
84
  fragment ProjectRun on Project {
85
85
  id
86
86
  baseUrl
@@ -99,13 +99,13 @@ ${n}`}}});import{z as yr}from"zod";var En=20,In=2,$n=yr.object({pattern:yr.strin
99
99
  ...WorkflowRun
100
100
  }
101
101
  }
102
- `,[Ls]),zm=Ve(`
102
+ `,[zs]),Sh=Fe(`
103
103
  query ProjectRun($projectId: String!) {
104
104
  project(id: $projectId) {
105
105
  ...ProjectRun
106
106
  }
107
107
  }
108
- `,[Ds]),Hm=Ve(`
108
+ `,[Hs]),kh=Fe(`
109
109
  query AgentProfileRun($id: String!) {
110
110
  agentProfile(id: $id) {
111
111
  id
@@ -115,12 +115,12 @@ ${n}`}}});import{z as yr}from"zod";var En=20,In=2,$n=yr.object({pattern:yr.strin
115
115
  successCriteria
116
116
  }
117
117
  }
118
- `);function F(e){return{content:[{text:e,type:"text"}]}}async function Hr(e){let t=et();return t==null?F("Not logged in. Run: ripplo login"):e(t)}import{z as _}from"zod";import Ws from"fs";import Ih from"path";import{z as ue}from"zod";var ke=ue.object({additionalChecks:ue.array(ue.string()).default([]),description:ue.string(),expectedOutcome:ue.string(),name:ue.string(),spec:ne});function Ue(e){let t=Ws.readFileSync(e,"utf8"),r=JSON.parse(t);return ke.parse(r)}var qs=_.object({additionalProperties:_.unknown().optional(),properties:_.record(_.string(),_.record(_.string(),_.unknown())).optional(),required:_.array(_.string()).optional(),type:_.string().optional()}),Jr=_.object({oneOf:_.array(qs).optional()});function Kr(){let e=Ks(),t={category:"Control Flow",fields:"body, catch?, finally?",typeName:"try"},r=[...e.map(a=>Ys(a)),t],o=Qs(r);return`# Ripplo Workflow Spec v2
118
+ `);function B(e){return{content:[{text:e,type:"text"}]}}async function Xr(e){let t=ot();return t==null?B("Not logged in. Run: ripplo login"):e(t)}import{z as F}from"zod";import Js from"fs";import sg from"path";import{z as fe}from"zod";var xe=fe.object({additionalChecks:fe.array(fe.string()).default([]),description:fe.string(),expectedOutcome:fe.string(),name:fe.string(),spec:ae});function We(e){let t=Js.readFileSync(e,"utf8"),r=JSON.parse(t);return xe.parse(r)}var Ks=F.object({additionalProperties:F.unknown().optional(),properties:F.record(F.string(),F.record(F.string(),F.unknown())).optional(),required:F.array(F.string()).optional(),type:F.string().optional()}),Zr=F.object({oneOf:F.array(Ks).optional()});function eo(){let e=ri(),t={category:"Control Flow",fields:"body, catch?, finally?",typeName:"try"},r=[...e.map(a=>oi(a)),t],o=ni(r);return`# Ripplo Workflow Spec v2
119
119
 
120
120
  ## Node Types
121
121
 
122
- ${Bs.filter(a=>o.has(a)).map(a=>{let d=(o.get(a)??[]).map(l=>`- ${l.typeName} { ${l.fields} }`);return`### ${a}
123
- ${d.join(`
122
+ ${Ys.filter(a=>o.has(a)).map(a=>{let c=(o.get(a)??[]).map(l=>`- ${l.typeName} { ${l.fields} }`);return`### ${a}
123
+ ${c.join(`
124
124
  `)}`}).join(`
125
125
 
126
126
  `)}
@@ -146,20 +146,20 @@ Each workflow file in \`.ripplo/workflows/\` is a JSON file with: name, descript
146
146
  The spec contains: version (2), entryNode, nodes, variables?.
147
147
 
148
148
  Use \`get_node_schema\` for full JSON Schema of specific node types.
149
- Use \`get_spec_patterns\` for locator, condition, and variable schemas + best practices.`}function Yr({nodeTypes:e}){let r=Jr.parse(structuredClone(_.toJSONSchema(we))).oneOf??[],o=new Map;r.forEach(a=>{let i=Xr(a);i.length>0&&o.set(i,a)});let n=[];return e.forEach(a=>{if(a==="try"){n.push(ei);return}let i=o.get(a);if(i==null){n.push(`## ${a}
149
+ Use \`get_spec_patterns\` for locator, condition, and variable schemas + best practices.`}function to({nodeTypes:e}){let r=Zr.parse(structuredClone(F.toJSONSchema(Se))).oneOf??[],o=new Map;r.forEach(a=>{let i=oo(a);i.length>0&&o.set(i,a)});let n=[];return e.forEach(a=>{if(a==="try"){n.push(ii);return}let i=o.get(a);if(i==null){n.push(`## ${a}
150
150
 
151
151
  Unknown node type "${a}".`);return}n.push(`## ${a}
152
152
 
153
153
  \`\`\`json
154
154
  ${JSON.stringify(i,null,2)}
155
- \`\`\``)}),n.push(Zs),n.join(`
155
+ \`\`\``)}),n.push(si),n.join(`
156
156
 
157
- `)}function Qr(){return`# Spec Patterns & Building Blocks
157
+ `)}function ro(){return`# Spec Patterns & Building Blocks
158
158
 
159
- ${[{label:"Locator",schema:b},{label:"Condition",schema:oe},{label:"StringValueRef",schema:x},{label:"NumericValueRef",schema:re},{label:"ValueRef",schema:Z},{label:"ComparisonOperator (string)",schema:U},{label:"NumericOperator",schema:Y},{label:"VariableDef",schema:ce},{label:"WorkflowFile (top-level)",schema:ke},{label:"WorkflowSpec (the spec field)",schema:ne}].map(({label:r,schema:o})=>`### ${r}
159
+ ${[{label:"Locator",schema:b},{label:"Condition",schema:ne},{label:"StringValueRef",schema:x},{label:"NumericValueRef",schema:oe},{label:"ValueRef",schema:Z},{label:"ComparisonOperator (string)",schema:U},{label:"NumericOperator",schema:K},{label:"VariableDef",schema:ue},{label:"WorkflowFile (top-level)",schema:xe},{label:"WorkflowSpec (the spec field)",schema:ae}].map(({label:r,schema:o})=>`### ${r}
160
160
 
161
161
  \`\`\`json
162
- ${JSON.stringify(_.toJSONSchema(o),null,2)}
162
+ ${JSON.stringify(F.toJSONSchema(o),null,2)}
163
163
  \`\`\``).join(`
164
164
 
165
165
  `)}
@@ -174,27 +174,27 @@ ${JSON.stringify(_.toJSONSchema(o),null,2)}
174
174
  6. **Keep node IDs descriptive**: \`fill-email\`, \`click-submit\`, \`assert-dashboard-visible\`
175
175
  7. **Use \`group\`** to organize related steps
176
176
  8. **Use \`try/catch\`** for steps that might fail (e.g., dismissing optional modals)
177
- 9. **Always call \`validate_workflow\`** after editing a workflow file
177
+ 9. **Always call \`validate\`** after editing a workflow file
178
178
  10. **URLs in goto nodes must be relative paths** (\`/login\`), never absolute URLs \u2014 the baseUrl handles the origin
179
179
  11. **Value references use \`"type": "static"\`**, never \`"type": "literal"\`
180
180
  12. **Node IDs must match their key** in the \`nodes\` object
181
- 13. **Template variables use \`{{name}}\`**, never \`$name\` or \`\${name}\` \u2014 only double curly braces work`}var Bs=["Actions","Waits","Assertions","Variables","Control Flow","Other"],Ms=new Set(["if","loop","forEach","parallel","try","group"]),Gs=new Set(["setVariable","extractText"]),zs=new Set(["screenshot","setViewport","fail"]),Hs=new Set(["comment","id","label","next","timeout","type"]);function Js(e){return e.startsWith("assert")?"Assertions":e.startsWith("waitFor")||e==="wait"?"Waits":Ms.has(e)?"Control Flow":Gs.has(e)?"Variables":zs.has(e)?"Other":"Actions"}function Ks(){return Jr.parse(structuredClone(_.toJSONSchema(we))).oneOf??[]}function Xr(e){let t=e.properties?.type;if(t==null)return"";let r=t.const;return typeof r=="string"?r:""}function Ys(e){let t=Xr(e),r=new Set(e.required??[]),o=Object.keys(e.properties??{}).filter(n=>!Hs.has(n)).map(n=>r.has(n)?n:`${n}?`).join(", ");return{category:Js(t),fields:o,typeName:t}}function Qs(e){let t=new Map;return e.forEach(r=>{let o=t.get(r.category)??[];o.push(r),t.set(r.category,o)}),t}function Xs(){return`---
181
+ 13. **Template variables use \`{{name}}\`**, never \`$name\` or \`\${name}\` \u2014 only double curly braces work`}var Ys=["Actions","Waits","Assertions","Variables","Control Flow","Other"],Qs=new Set(["if","loop","forEach","parallel","try","group"]),Xs=new Set(["setVariable","extractText"]),Zs=new Set(["screenshot","setViewport","fail"]),ei=new Set(["comment","id","label","next","timeout","type"]);function ti(e){return e.startsWith("assert")?"Assertions":e.startsWith("waitFor")||e==="wait"?"Waits":Qs.has(e)?"Control Flow":Xs.has(e)?"Variables":Zs.has(e)?"Other":"Actions"}function ri(){return Zr.parse(structuredClone(F.toJSONSchema(Se))).oneOf??[]}function oo(e){let t=e.properties?.type;if(t==null)return"";let r=t.const;return typeof r=="string"?r:""}function oi(e){let t=oo(e),r=new Set(e.required??[]),o=Object.keys(e.properties??{}).filter(n=>!ei.has(n)).map(n=>r.has(n)?n:`${n}?`).join(", ");return{category:ti(t),fields:o,typeName:t}}function ni(e){let t=new Map;return e.forEach(r=>{let o=t.get(r.category)??[];o.push(r),t.set(r.category,o)}),t}function ai(){return`---
182
182
 
183
183
  ## Referenced Schemas
184
184
 
185
- ${[pe("Locator",b),pe("StringValueRef",x),pe("NumericValueRef",re),pe("ComparisonOperator",U),pe("NumericOperator",Y),pe("Condition",oe)].join(`
185
+ ${[me("Locator",b),me("StringValueRef",x),me("NumericValueRef",oe),me("ComparisonOperator",U),me("NumericOperator",K),me("Condition",ne)].join(`
186
186
 
187
- `)}`}var Zs=Xs();function pe(e,t){return`### ${e}
187
+ `)}`}var si=ai();function me(e,t){return`### ${e}
188
188
 
189
189
  \`\`\`json
190
- ${JSON.stringify(_.toJSONSchema(t),null,2)}
191
- \`\`\``}var ei=["## 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(`
192
- `);import{graphql as Fe,readFragment as ti}from"gql.tada";import Zr from"figures";var ve={cross:Zr.cross,tick:Zr.tick};var to=Fe(`
190
+ ${JSON.stringify(F.toJSONSchema(t),null,2)}
191
+ \`\`\``}var ii=["## 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(`
192
+ `);import{graphql as _e,readFragment as li}from"gql.tada";import no from"figures";var Re={cross:no.cross,tick:no.tick};var so=_e(`
193
193
  fragment WorkflowIdMCP on Workflow {
194
194
  id
195
195
  slug
196
196
  }
197
- `),ri=Fe(`
197
+ `),ci=_e(`
198
198
  query ProjectWorkflowIdsMCP($projectId: String!) {
199
199
  project(id: $projectId) {
200
200
  id
@@ -206,14 +206,14 @@ ${JSON.stringify(_.toJSONSchema(t),null,2)}
206
206
  }
207
207
  }
208
208
  }
209
- `,[to]),oi=Fe(`
209
+ `,[so]),di=_e(`
210
210
  mutation CreateRunMCP($workflowId: String!, $platforms: [String!]!, $concurrency: Int!) {
211
211
  createRun(workflowId: $workflowId, platforms: $platforms, concurrency: $concurrency) {
212
212
  id
213
213
  status
214
214
  }
215
215
  }
216
- `),ni=Fe(`
216
+ `),ui=_e(`
217
217
  query RunStatusMCP($id: String!) {
218
218
  run(id: $id) {
219
219
  id
@@ -240,20 +240,20 @@ ${JSON.stringify(_.toJSONSchema(t),null,2)}
240
240
  }
241
241
  }
242
242
  }
243
- `);async function ro({config:e,slugs:t}){let r=await ai({config:e,slugs:t}),o=await Promise.allSettled(r.map(async({slug:l,workflowId:p})=>({runId:await ii({config:e,workflowId:p}),slug:l}))),n=[],a=[];o.forEach((l,p)=>{let g=r[p];if(g==null)return;if(l.status==="fulfilled"){n.push(l.value);return}let f=l.reason instanceof Error?l.reason.message:String(l.reason);a.push({runId:"",slug:g.slug,summary:`ERROR creating run: ${f}`})}),(await Promise.allSettled(n.map(async({runId:l,slug:p})=>{let g=await ci({config:e,runId:l}),f=si({config:e,runId:l,slug:p}),c=`.ripplo/debug/${l}/`;return{runId:l,slug:p,summary:`View run: ${f}
244
- Debug artifacts: ${c}
243
+ `);async function io({config:e,slugs:t}){let r=await pi({config:e,slugs:t}),o=await Promise.allSettled(r.map(async({slug:l,workflowId:p})=>({runId:await mi({config:e,workflowId:p}),slug:l}))),n=[],a=[];o.forEach((l,p)=>{let g=r[p];if(g==null)return;if(l.status==="fulfilled"){n.push(l.value);return}let f=l.reason instanceof Error?l.reason.message:String(l.reason);a.push({runId:"",slug:g.slug,summary:`ERROR creating run: ${f}`})}),(await Promise.allSettled(n.map(async({runId:l,slug:p})=>{let g=await gi({config:e,runId:l}),f=fi({config:e,runId:l,slug:p}),d=`.ripplo/debug/${l}/`;return{runId:l,slug:p,summary:`View run: ${f}
244
+ Debug artifacts: ${d}
245
245
 
246
- ${g}`}}))).forEach((l,p)=>{let g=n[p];if(g==null)return;if(l.status==="fulfilled"){a.push(l.value);return}let f=l.reason instanceof Error?l.reason.message:String(l.reason);a.push({runId:g.runId,slug:g.slug,summary:`ERROR polling run ${g.runId}: ${f}`})});let d=a.map(l=>`**${l.slug}:**
246
+ ${g}`}}))).forEach((l,p)=>{let g=n[p];if(g==null)return;if(l.status==="fulfilled"){a.push(l.value);return}let f=l.reason instanceof Error?l.reason.message:String(l.reason);a.push({runId:g.runId,slug:g.slug,summary:`ERROR polling run ${g.runId}: ${f}`})});let c=a.map(l=>`**${l.slug}:**
247
247
  ${l.summary}`).join(`
248
248
 
249
249
  ---
250
250
 
251
- `);return{results:a,summary:d}}async function ai({config:e,slugs:t}){let r=await X({config:e,document:ri,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 o=(r.project.workflows??[]).map(n=>ti(to,n));return t.map(n=>{let a=o.find(i=>i.slug===n);if(a==null)throw new Error(`Workflow "${n}" not found on server. Is the dev session running and synced?`);return{slug:n,workflowId:a.id}})}function si({config:e,runId:t,slug:r}){return`${e.ripploServerUrl.replace(/:3000\b/,":3001")}/projects/${e.projectId}/workflows/${r}/runs/${t}`}async function ii({config:e,workflowId:t}){let r=await X({config:e,document:oi,variables:{concurrency:1,platforms:["chromium"],workflowId:t}});if(r.createRun==null)throw new Error("Failed to create run");return r.createRun.id}var li=2e3,eo=150;async function ci({config:e,runId:t}){let r=0;for(;r<eo;){await ui(li),r+=1;let n=(await X({config:e,document:ni,variables:{id:t}})).run;if(n==null)throw new Error(`Run ${t} not found`);if(!(n.status==="pending"||n.status==="running"))return di({results:n.results??[],runId:t,status:n.status})}return`Run ${t} timed out after polling ${String(eo)} times.`}function di({results:e,runId:t,status:r}){let o=[`Run ${t}: ${r.toUpperCase()}`];return e.forEach(n=>{o.push(` ${n.status}: ${String(n.passCount)} passed, ${String(n.failCount)} failed (${String(n.duration??0)}ms)`),n.summary!=null&&o.push(` Summary: ${n.summary}`),n.traceEntries.forEach(a=>{let i=a.status==="passed"?ve.tick:ve.cross,d=a.detail==null?"":` \u2014 ${a.detail}`;o.push(` ${i} ${a.title} (${String(a.duration)}ms)${d}`),a.assertions.forEach(l=>{let p=l.status==="passed"?ve.tick:ve.cross,g=l.detail==null?"":` \u2014 ${l.detail}`;o.push(` ${p} ${l.description}${g}`)})})}),o.join(`
252
- `)}function ui(e){return new Promise(t=>{setTimeout(t,e)})}import{z as pi}from"zod";function oo(){return fi()}function no(){return mi()}function se(e,t){let r=pi.toJSONSchema(t);return`### ${e}
251
+ `);return{results:a,summary:c}}async function pi({config:e,slugs:t}){let r=await X({config:e,document:ci,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 o=(r.project.workflows??[]).map(n=>li(so,n));return t.map(n=>{let a=o.find(i=>i.slug===n);if(a==null)throw new Error(`Workflow "${n}" not found on server. Is the dev session running and synced?`);return{slug:n,workflowId:a.id}})}function fi({config:e,runId:t,slug:r}){return`${e.ripploServerUrl.replace(/:3000\b/,":3001")}/projects/${e.projectId}/workflows/${r}/runs/${t}`}async function mi({config:e,workflowId:t}){let r=await X({config:e,document:di,variables:{concurrency:1,platforms:["chromium"],workflowId:t}});if(r.createRun==null)throw new Error("Failed to create run");return r.createRun.id}var hi=2e3,ao=150;async function gi({config:e,runId:t}){let r=0;for(;r<ao;){await wi(hi),r+=1;let n=(await X({config:e,document:ui,variables:{id:t}})).run;if(n==null)throw new Error(`Run ${t} not found`);if(!(n.status==="pending"||n.status==="running"))return yi({results:n.results??[],runId:t,status:n.status})}return`Run ${t} timed out after polling ${String(ao)} times.`}function yi({results:e,runId:t,status:r}){let o=[`Run ${t}: ${r.toUpperCase()}`];return e.forEach(n=>{o.push(` ${n.status}: ${String(n.passCount)} passed, ${String(n.failCount)} failed (${String(n.duration??0)}ms)`),n.summary!=null&&o.push(` Summary: ${n.summary}`),n.traceEntries.forEach(a=>{let i=a.status==="passed"?Re.tick:Re.cross,c=a.detail==null?"":` \u2014 ${a.detail}`;o.push(` ${i} ${a.title} (${String(a.duration)}ms)${c}`),a.assertions.forEach(l=>{let p=l.status==="passed"?Re.tick:Re.cross,g=l.detail==null?"":` \u2014 ${l.detail}`;o.push(` ${p} ${l.description}${g}`)})})}),o.join(`
252
+ `)}function wi(e){return new Promise(t=>{setTimeout(t,e)})}import{z as bi}from"zod";function lo(){return Si()}function co(){return ki()}function ie(e,t){let r=bi.toJSONSchema(t);return`### ${e}
253
253
 
254
254
  \`\`\`json
255
255
  ${JSON.stringify(r,null,2)}
256
- \`\`\``}function fi(){return[`# Ripplo State Graph Spec
256
+ \`\`\``}function Si(){return[`# Ripplo State Graph Spec
257
257
 
258
258
  The graph schema is documented below as JSON Schema (generated from Zod at runtime \u2014 always in sync with the code).
259
259
 
@@ -261,7 +261,7 @@ The graph schema is documented below as JSON Schema (generated from Zod at runti
261
261
 
262
262
  The graph lives at \`.ripplo/graph.json\`. The top-level shape is the StateGraph schema below. One graph per project.`,`## Graph Schema
263
263
 
264
- `+se("StateGraph",be),`## Preconditions (summary)
264
+ `+ie("StateGraph",ke),`## Preconditions (summary)
265
265
 
266
266
  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.).
267
267
 
@@ -274,9 +274,9 @@ Dependencies (\`depends\`) are resolved via topological sort. Cycles are rejecte
274
274
 
275
275
  Use \`get_precondition_schema\` for full precondition API schemas, setup guide, and framework-specific auth examples.`].join(`
276
276
 
277
- `)}function mi(){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\n### Request signing (Standard Webhooks)\n\nAll precondition requests are signed using the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification (HMAC-SHA256). Every request includes three headers:\n- `webhook-id` \u2014 unique message ID\n- `webhook-timestamp` \u2014 Unix timestamp (seconds)\n- `webhook-signature` \u2014 `v1,<base64_hmac>`\n\nThe precondition API **must verify these signatures** to ensure requests come from Ripplo. See the setup guide below for verification instructions.\n\nDependencies (`depends`) are resolved via topological sort with deduplication. Cycles are rejected at validation time.",se("Precondition (graph-level declaration)",ae),se("Precondition Check/Execute Request",ot),se("Check Response",Ne),se("Execute Response",Ee),se("Teardown Request",nt),se("Teardown Response",at),gi()].join(`
277
+ `)}function ki(){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\n### Request signing (Standard Webhooks)\n\nAll precondition requests are signed using the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification (HMAC-SHA256). Every request includes three headers:\n- `webhook-id` \u2014 unique message ID\n- `webhook-timestamp` \u2014 Unix timestamp (seconds)\n- `webhook-signature` \u2014 `v1,<base64_hmac>`\n\nThe precondition API **must verify these signatures** to ensure requests come from Ripplo. See the setup guide below for verification instructions.\n\nDependencies (`depends`) are resolved via topological sort with deduplication. Cycles are rejected at validation time.",ie("Precondition (graph-level declaration)",se),ie("Precondition Check/Execute Request",st),ie("Check Response",Ie),ie("Execute Response",$e),ie("Teardown Request",it),ie("Teardown Response",lt),xi()].join(`
278
278
 
279
- `)}function hi(){return`### Framework-specific auth examples
279
+ `)}function vi(){return`### Framework-specific auth examples
280
280
 
281
281
  #### Supabase (with @supabase/ssr)
282
282
 
@@ -313,7 +313,7 @@ case "auth:user": {
313
313
  - Use \`signInWithPassword()\` to get session tokens after creating the user
314
314
  - Set the cookie in Supabase's expected format: \`sb-<project-ref>-auth-token\`
315
315
  - Always include \`Path=/; SameSite=Lax\` on the Set-Cookie header
316
- - The \`navigateTo\` URL sets the browser's starting page after cookies are injected`}function gi(){return`## Precondition API Setup
316
+ - The \`navigateTo\` URL sets the browser's starting page after cookies are injected`}function xi(){return`## Precondition API Setup
317
317
 
318
318
  To enable precondition execution, the user's app must expose three REST endpoints.
319
319
 
@@ -340,7 +340,7 @@ ripplo generate-types --output apps/server/src/generated/precondition-api.ts #
340
340
 
341
341
  The generated file should be committed to the repo and the \`--output\` path should be importable from the server code without path gymnastics.
342
342
 
343
- ${yi()}
343
+ ${Ri()}
344
344
 
345
345
  ### Key principles:
346
346
 
@@ -358,7 +358,7 @@ wh.verify(rawBody, { "webhook-id": req.headers["webhook-id"], "webhook-timestamp
358
358
  6. **All logic in your app**: Ripplo just calls your endpoints.
359
359
  7. **Parallel-safe teardown**: Multiple workflows run simultaneously. Teardown must only delete data belonging to that specific run's user \u2014 use the session cookies forwarded in the teardown request to identify the user. Never bulk-delete by prefix (e.g., \`WHERE email LIKE 'ripplo-test-%'\`) \u2014 this destroys other parallel runs' active data.
360
360
 
361
- ${hi()}
361
+ ${vi()}
362
362
 
363
363
  ## Best Practices
364
364
 
@@ -368,9 +368,9 @@ ${hi()}
368
368
  4. **Use \`depends\` to avoid duplication** \u2014 if \`data:projects\` needs auth, declare \`depends: ["auth:admin"]\`
369
369
  5. **Keep precondition handlers fast** \u2014 check should be a lightweight query, execute should create minimal test data
370
370
  6. **Create fresh test users** \u2014 auth preconditions should create ephemeral users, not log in as shared accounts
371
- 7. **Validate after every edit** \u2014 call \`validate_graph\` to catch missing refs and cycles
371
+ 7. **Validate after every edit** \u2014 call \`validate\` to catch missing refs and cycles
372
372
  8. **Regenerate types after editing preconditions** \u2014 run \`ripplo generate-types\` when precondition names change
373
- 9. **Set \`ENABLE_RIPPLO_TESTING=true\` in dev, CI, and staging** \u2014 enable it anywhere you run Ripplo tests, but never in production deployments. Add it to \`.env.local\`, CI config, or your staging environment \u2014 not \`.env.production\``}function yi(){return`### Step 3: Implement the three endpoints
373
+ 9. **Set \`ENABLE_RIPPLO_TESTING=true\` in dev, CI, and staging** \u2014 enable it anywhere you run Ripplo tests, but never in production deployments. Add it to \`.env.local\`, CI config, or your staging environment \u2014 not \`.env.production\``}function Ri(){return`### Step 3: Implement the three endpoints
374
374
 
375
375
  Using the generated types (adjust import path to match your \`--output\`).
376
376
 
@@ -469,19 +469,19 @@ app.put("/api/test/preconditions/teardown", async (req, res) => {
469
469
  }
470
470
  res.json({ success: true });
471
471
  });
472
- \`\`\``}import ft from"fs";import mt from"path";import wi from"fs";import Yh from"path";import{z as V}from"zod";var pt=V.object({edges:V.array(V.object({from:V.string(),to:V.string(),workflow:V.string()})),preconditions:V.record(V.string(),V.unknown()).optional(),resetPrecondition:V.string().optional(),states:V.record(V.string(),V.object({preconditions:V.array(V.string()),route:V.string()})),version:V.literal(3)});function ao(e){let t=wi.readFileSync(e,"utf8"),r=JSON.parse(t);return pt.parse(r)}function so(e){return{edges:e.edges,preconditions:e.preconditions??{},resetPrecondition:e.resetPrecondition,states:e.states,version:e.version}}function io(e){let t=bi(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=pt.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(g=>({message:g.message,path:g.path.join(".")})),valid:!1};let o=so(r.data),n=it(o);if(!n.success)return{errors:n.errors,valid:!1};let a=[...lt(n.data)],i=mt.join(mt.dirname(e),"workflows"),d=ft.existsSync(i),l=new Set(n.data.edges.map(p=>p.workflow));return n.data.edges.forEach((p,g)=>{if(!d){a.push({message:`References workflow "${p.workflow}" but workflows directory does not exist`,path:`edges[${String(g)}].workflow`});return}let f=mt.join(i,`${p.workflow}.json`);ft.existsSync(f)||a.push({message:`References workflow "${p.workflow}" but no file exists at workflows/${p.workflow}.json`,path:`edges[${String(g)}].workflow`})}),d&&ft.readdirSync(i).filter(p=>p.endsWith(".json")).forEach(p=>{let g=p.replace(/\.json$/,"");l.has(g)||a.push({message:`Workflow file "workflows/${p}" is not referenced by any edge`,path:`workflows/${p}`})}),{errors:a,valid:a.length===0}}function bi(e){try{return{data:ao(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}import _e from"fs";import lo from"path";import{z as G}from"zod";var Si=G.object({baseUrl:G.string().optional(),cloudBaseUrl:G.string(),preconditionApiPath:G.string().optional(),projectId:G.string()});function co(e){let t=lo.join(e,".ripplo","settings.json");if(!_e.existsSync(t))return{baseUrl:void 0,errors:[{message:".ripplo/settings.json not found",path:""}],valid:!1,warnings:[]};let r=xi(t);if(!r.success)return{baseUrl:void 0,errors:[{message:r.error,path:"settings.json"}],valid:!1,warnings:[]};let o=Si.safeParse(r.data);if(!o.success)return{baseUrl:void 0,errors:o.error.issues.map(d=>({message:d.message,path:d.path.join(".")})),valid:!1,warnings:[]};let n=[],a=lo.join(e,".ripplo","graph.json");return _e.existsSync(a)&&vi(a)&&(o.data.preconditionApiPath==null||o.data.preconditionApiPath.length===0)&&n.push("Graph has preconditions but preconditionApiPath is not set in settings.json. Preconditions will not be executed. Set preconditionApiPath to the URL where your app serves precondition endpoints (can be a relative path appended to baseUrl, or an absolute URL)."),{baseUrl:o.data.baseUrl,errors:[],valid:!0,warnings:n}}async function uo(e){try{await fetch(e,{method:"HEAD",signal:AbortSignal.timeout(5e3)});return}catch{return`baseUrl (${e}) is not responding. Make sure your dev server is running on that port.`}}var ki=G.object({states:G.record(G.string(),G.object({preconditions:G.array(G.string())}))});function vi(e){try{let t=_e.readFileSync(e,"utf8"),r=JSON.parse(t),o=ki.safeParse(r);return o.success?Object.values(o.data.states).some(n=>n.preconditions.length>0):!1}catch{return!1}}function xi(e){try{let t=_e.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 po={assertAttribute:"assertion",assertChecked:"assertion",assertCookie:"assertion",assertCount:"assertion",assertDisabled:"assertion",assertEnabled:"assertion",assertFocused:"assertion",assertNotChecked:"assertion",assertNotVisible:"assertion",assertResponse:"assertion",assertText:"assertion",assertTitle:"assertion",assertUrl:"assertion",assertValue:"assertion",assertVisible:"assertion",check:"action",clear:"action",click:"action",clipboard:"other",dblclick:"action",drag:"action",extractText:"other",fail:"other",fill:"action",focus:"action",forEach:"controlFlow",goto:"other",group:"controlFlow",handleDialog:"action",hover:"action",if:"controlFlow",loop:"controlFlow",parallel:"controlFlow",press:"action",rightClick:"action",screenshot:"other",scroll:"action",scrollIntoView:"action",select:"action",setPermission:"other",setVariable:"other",setViewport:"other",try:"controlFlow",type:"action",uncheck:"action",upload:"action",wait:"other",waitFor:"other",waitForRequest:"other",waitForResponse:"other",waitForUrl:"other"},Le=new Set,ht=new Set;Object.entries(po).forEach(([e,t])=>{t==="assertion"&&Le.add(e),t==="action"&&ht.add(e)});function fo(e){let t=Ue(e),{spec:r}=t,o=Ni(r),n=Ei(r),a=Ri({orderedNodes:o,spec:r,terminalNodes:n}),i=Vi({flags:a,orderedNodes:o,terminalNodes:n,wf:t});return{flags:a,report:i}}function Ri({orderedNodes:e,spec:t,terminalNodes:r}){let o=e.filter(d=>Le.has(d.type)).length,n=e.filter(d=>ht.has(d.type)).length,a=Object.keys(t.nodes).length,i=r.some(d=>Le.has(d.type));return[...Pi(o),...Ti(i,o),...Ci(n),...Ai(o,a)]}function Pi(e){return e===0?["NO_ASSERTIONS: The workflow has zero assertion nodes. It cannot verify its expectedOutcome."]:[]}function Ti(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 Ci(e){return e===0?["NO_INTERACTIONS: The workflow has zero interaction nodes (click, fill, select, etc.). It does not exercise any user action."]:[]}function Ai(e,t){if(e>0&&t>3){let r=e/t;if(r<.15){let o=String(Math.round(r*100));return[`LOW_ASSERTION_RATIO: Only ${String(e)}/${String(t)} nodes are assertions (${o}%). Consider adding more verification.`]}}return[]}function Ni(e){let t=new Set,r=[];function o(n){if(t.has(n))return;t.add(n);let a=e.nodes[n];a!=null&&(r.push(a),a.next!=null&&o(a.next),mo(a).forEach(i=>{o(i)}))}return o(e.entryNode),r}function mo(e){switch(e.type){case"if":return[...e.consequent,...e.alternate??[]];case"loop":case"forEach":return[...e.body];case"try":return[...e.body,...e.catch??[],...e.finally??[]];case"group":return[...e.nodes];case"parallel":return e.branches.flat();case"assertVisible":case"assertNotVisible":case"assertText":case"assertUrl":case"assertCount":case"assertValue":case"click":case"fill":case"select":case"check":case"uncheck":case"press":case"hover":case"goto":case"waitFor":case"screenshot":case"setViewport":case"wait":case"fail":case"setVariable":case"extractText":case"waitForUrl":case"waitForResponse":case"waitForRequest":case"upload":case"dblclick":case"drag":case"scrollIntoView":case"type":case"focus":case"assertAttribute":case"assertEnabled":case"assertDisabled":case"assertTitle":case"assertChecked":case"assertNotChecked":case"assertFocused":case"assertCookie":case"assertResponse":case"clear":case"rightClick":case"handleDialog":case"scroll":case"clipboard":case"setPermission":return[]}}function Ei(e){return Object.values(e.nodes).filter(t=>t.next!=null?!1:mo(t).length===0)}function P(e){return e.by==="role"?e.name==null?`role=${e.role}`:`role=${e.role}[${e.name}]`:`${e.by}=${e.value}`}function z(e){return e.type==="static"?`"${e.value}"`:`{{${e.name}}}`}function Ii(e){return e.type==="static"?String(e.value):`{{${e.name}}}`}var ho=new Set,go=new Set;Object.entries(po).forEach(([e,t])=>{t==="controlFlow"&&ho.add(e),t==="assertion"&&go.add(e)});function yo(e){let t=`[${e.type}]`;return ho.has(e.type)?Oi(e,t):go.has(e.type)?ji(e,t):$i(e,t)}function $i(e,t){let r=e;switch(r.type){case"goto":return`${t} Navigate to ${z(r.url)}`;case"click":return`${t} click ${P(r.locator)}`;case"hover":return`${t} hover ${P(r.locator)}`;case"check":return`${t} check ${P(r.locator)}`;case"uncheck":return`${t} uncheck ${P(r.locator)}`;case"fill":return`${t} Fill ${P(r.locator)} with ${z(r.value)}`;case"select":return`${t} Select ${z(r.value)} in ${P(r.locator)}`;case"press":return`${t} Press "${r.key}"`;case"waitFor":{let o=r.state==null?"":` to be ${r.state}`;return`${t} Wait for ${P(r.locator)}${o}`}case"waitForUrl":return`${t} Wait for URL ${r.operator} ${z(r.expected)}`;case"waitForResponse":return`${t} Wait for response matching ${z(r.urlPattern)}`;case"waitForRequest":return`${t} Wait for request matching ${z(r.urlPattern)}`;case"extractText":return`${t} Extract text from ${P(r.locator)} into $${r.variable}`;case"screenshot":case"clipboard":return`${t} ${r.type}`;case"setViewport":return`${t} setViewport ${String(r.width)}x${String(r.height)}`;case"wait":return`${t} wait ${String(r.duration)}ms`;case"fail":return`${t} fail: ${r.message}`;case"setVariable":return`${t} setVariable ${r.variable}`;case"upload":return`${t} Upload files to ${P(r.locator)}`;case"dblclick":return`${t} Double-click ${P(r.locator)}`;case"drag":return`${t} Drag ${P(r.source)} to ${P(r.target)}`;case"scrollIntoView":return`${t} Scroll into view ${P(r.locator)}`;case"type":return`${t} Type ${z(r.value)} into ${P(r.locator)}`;case"focus":return`${t} Focus ${P(r.locator)}`;case"clear":return`${t} Clear ${P(r.locator)}`;case"rightClick":return`${t} Right-click ${P(r.locator)}`;case"handleDialog":return`${t} Handle dialog: ${r.action}`;case"scroll":return r.locator==null?`${t} Scroll page`:`${t} Scroll ${P(r.locator)}`;case"setPermission":return`${t} Set permission ${r.permission} to ${r.state}`}}function ji(e,t){let r=e;switch(r.type){case"assertVisible":return`${t} Assert visible: ${P(r.locator)}`;case"assertNotVisible":return`${t} Assert not visible: ${P(r.locator)}`;case"assertText":return`${t} Assert text of ${P(r.locator)} ${r.operator} ${z(r.expected)}`;case"assertUrl":return`${t} Assert URL ${r.operator} ${z(r.expected)}`;case"assertCount":return`${t} Assert count of ${P(r.locator)} ${r.operator} ${Ii(r.expected)}`;case"assertValue":return`${t} Assert value of ${P(r.locator)} ${r.operator} ${z(r.expected)}`;case"assertAttribute":return`${t} Assert attribute "${r.attribute}" of ${P(r.locator)} ${r.operator} ${z(r.expected)}`;case"assertEnabled":return`${t} Assert enabled: ${P(r.locator)}`;case"assertDisabled":return`${t} Assert disabled: ${P(r.locator)}`}}function Oi(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 Vi({flags:e,orderedNodes:t,terminalNodes:r,wf:o}){let n=t.filter(d=>Le.has(d.type)).length,a=t.filter(d=>ht.has(d.type)).length;return[Ui(o),Fi(t),_i(r),Li({actionCount:a,assertionCount:n,orderedNodes:t,terminalNodes:r}),...Di(e),Wi].join(`
473
- `)}function Ui(e){let t=["## Declared Intent",`**Name:** ${e.name}`,`**Description:** ${e.description}`,`**Expected Outcome:** ${e.expectedOutcome}`];if(e.additionalChecks.length>0){let r=e.additionalChecks.map(o=>`- ${o}`).join(`
472
+ \`\`\``}var uo={assertAttribute:"assertion",assertChecked:"assertion",assertCookie:"assertion",assertCount:"assertion",assertDisabled:"assertion",assertEnabled:"assertion",assertFocused:"assertion",assertNotChecked:"assertion",assertNotVisible:"assertion",assertResponse:"assertion",assertText:"assertion",assertTitle:"assertion",assertUrl:"assertion",assertValue:"assertion",assertVisible:"assertion",check:"action",clear:"action",click:"action",clipboard:"other",dblclick:"action",drag:"action",extractText:"other",fail:"other",fill:"action",focus:"action",forEach:"controlFlow",goto:"other",group:"controlFlow",handleDialog:"action",hover:"action",if:"controlFlow",loop:"controlFlow",parallel:"controlFlow",press:"action",rightClick:"action",screenshot:"other",scroll:"action",scrollIntoView:"action",select:"action",setPermission:"other",setVariable:"other",setViewport:"other",try:"controlFlow",type:"action",uncheck:"action",upload:"action",wait:"other",waitFor:"other",waitForRequest:"other",waitForResponse:"other",waitForUrl:"other"},Le=new Set,ht=new Set;Object.entries(uo).forEach(([e,t])=>{t==="assertion"&&Le.add(e),t==="action"&&ht.add(e)});function po(e){let t=We(e),{spec:r}=t,o=Ei(r),n=Ii(r),a=Pi({orderedNodes:o,spec:r,terminalNodes:n}),i=Ui({flags:a,orderedNodes:o,terminalNodes:n,wf:t});return{flags:a,report:i}}function Pi({orderedNodes:e,spec:t,terminalNodes:r}){let o=e.filter(c=>Le.has(c.type)).length,n=e.filter(c=>ht.has(c.type)).length,a=Object.keys(t.nodes).length,i=r.some(c=>Le.has(c.type));return[...Ti(o),...Ci(i,o),...Ai(n),...Ni(o,a)]}function Ti(e){return e===0?["NO_ASSERTIONS: The workflow has zero assertion nodes. It cannot verify its expectedOutcome."]:[]}function Ci(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 Ai(e){return e===0?["NO_INTERACTIONS: The workflow has zero interaction nodes (click, fill, select, etc.). It does not exercise any user action."]:[]}function Ni(e,t){if(e>0&&t>3){let r=e/t;if(r<.15){let o=String(Math.round(r*100));return[`LOW_ASSERTION_RATIO: Only ${String(e)}/${String(t)} nodes are assertions (${o}%). Consider adding more verification.`]}}return[]}function Ei(e){let t=new Set,r=[];function o(n){if(t.has(n))return;t.add(n);let a=e.nodes[n];a!=null&&(r.push(a),a.next!=null&&o(a.next),fo(a).forEach(i=>{o(i)}))}return o(e.entryNode),r}function fo(e){switch(e.type){case"if":return[...e.consequent,...e.alternate??[]];case"loop":case"forEach":return[...e.body];case"try":return[...e.body,...e.catch??[],...e.finally??[]];case"group":return[...e.nodes];case"parallel":return e.branches.flat();case"assertVisible":case"assertNotVisible":case"assertText":case"assertUrl":case"assertCount":case"assertValue":case"click":case"fill":case"select":case"check":case"uncheck":case"press":case"hover":case"goto":case"waitFor":case"screenshot":case"setViewport":case"wait":case"fail":case"setVariable":case"extractText":case"waitForUrl":case"waitForResponse":case"waitForRequest":case"upload":case"dblclick":case"drag":case"scrollIntoView":case"type":case"focus":case"assertAttribute":case"assertEnabled":case"assertDisabled":case"assertTitle":case"assertChecked":case"assertNotChecked":case"assertFocused":case"assertCookie":case"assertResponse":case"clear":case"rightClick":case"handleDialog":case"scroll":case"clipboard":case"setPermission":return[]}}function Ii(e){return Object.values(e.nodes).filter(t=>t.next!=null?!1:fo(t).length===0)}function P(e){return e.by==="role"?e.name==null?`role=${e.role}`:`role=${e.role}[${e.name}]`:`${e.by}=${e.value}`}function M(e){return e.type==="static"?`"${e.value}"`:`{{${e.name}}}`}function $i(e){return e.type==="static"?String(e.value):`{{${e.name}}}`}var mo=new Set,ho=new Set;Object.entries(uo).forEach(([e,t])=>{t==="controlFlow"&&mo.add(e),t==="assertion"&&ho.add(e)});function go(e){let t=`[${e.type}]`;return mo.has(e.type)?Vi(e,t):ho.has(e.type)?Oi(e,t):ji(e,t)}function ji(e,t){let r=e;switch(r.type){case"goto":return`${t} Navigate to ${M(r.url)}`;case"click":return`${t} click ${P(r.locator)}`;case"hover":return`${t} hover ${P(r.locator)}`;case"check":return`${t} check ${P(r.locator)}`;case"uncheck":return`${t} uncheck ${P(r.locator)}`;case"fill":return`${t} Fill ${P(r.locator)} with ${M(r.value)}`;case"select":return`${t} Select ${M(r.value)} in ${P(r.locator)}`;case"press":return`${t} Press "${r.key}"`;case"waitFor":{let o=r.state==null?"":` to be ${r.state}`;return`${t} Wait for ${P(r.locator)}${o}`}case"waitForUrl":return`${t} Wait for URL ${r.operator} ${M(r.expected)}`;case"waitForResponse":return`${t} Wait for response matching ${M(r.urlPattern)}`;case"waitForRequest":return`${t} Wait for request matching ${M(r.urlPattern)}`;case"extractText":return`${t} Extract text from ${P(r.locator)} into $${r.variable}`;case"screenshot":case"clipboard":return`${t} ${r.type}`;case"setViewport":return`${t} setViewport ${String(r.width)}x${String(r.height)}`;case"wait":return`${t} wait ${String(r.duration)}ms`;case"fail":return`${t} fail: ${r.message}`;case"setVariable":return`${t} setVariable ${r.variable}`;case"upload":return`${t} Upload files to ${P(r.locator)}`;case"dblclick":return`${t} Double-click ${P(r.locator)}`;case"drag":return`${t} Drag ${P(r.source)} to ${P(r.target)}`;case"scrollIntoView":return`${t} Scroll into view ${P(r.locator)}`;case"type":return`${t} Type ${M(r.value)} into ${P(r.locator)}`;case"focus":return`${t} Focus ${P(r.locator)}`;case"clear":return`${t} Clear ${P(r.locator)}`;case"rightClick":return`${t} Right-click ${P(r.locator)}`;case"handleDialog":return`${t} Handle dialog: ${r.action}`;case"scroll":return r.locator==null?`${t} Scroll page`:`${t} Scroll ${P(r.locator)}`;case"setPermission":return`${t} Set permission ${r.permission} to ${r.state}`}}function Oi(e,t){let r=e;switch(r.type){case"assertVisible":return`${t} Assert visible: ${P(r.locator)}`;case"assertNotVisible":return`${t} Assert not visible: ${P(r.locator)}`;case"assertText":return`${t} Assert text of ${P(r.locator)} ${r.operator} ${M(r.expected)}`;case"assertUrl":return`${t} Assert URL ${r.operator} ${M(r.expected)}`;case"assertCount":return`${t} Assert count of ${P(r.locator)} ${r.operator} ${$i(r.expected)}`;case"assertValue":return`${t} Assert value of ${P(r.locator)} ${r.operator} ${M(r.expected)}`;case"assertAttribute":return`${t} Assert attribute "${r.attribute}" of ${P(r.locator)} ${r.operator} ${M(r.expected)}`;case"assertEnabled":return`${t} Assert enabled: ${P(r.locator)}`;case"assertDisabled":return`${t} Assert disabled: ${P(r.locator)}`}}function Vi(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 Ui({flags:e,orderedNodes:t,terminalNodes:r,wf:o}){let n=t.filter(c=>Le.has(c.type)).length,a=t.filter(c=>ht.has(c.type)).length;return[Fi(o),Wi(t),_i(r),Li({actionCount:a,assertionCount:n,orderedNodes:t,terminalNodes:r}),...Di(e),qi].join(`
473
+ `)}function Fi(e){let t=["## Declared Intent",`**Name:** ${e.name}`,`**Description:** ${e.description}`,`**Expected Outcome:** ${e.expectedOutcome}`];if(e.additionalChecks.length>0){let r=e.additionalChecks.map(o=>`- ${o}`).join(`
474
474
  `);t.push(`**Additional Checks:**
475
475
  ${r}`)}return t.join(`
476
- `)}function Fi(e){return["","## Workflow Steps (execution order)",...e.map((r,o)=>`${String(o+1)}. ${yo(r)}`)].join(`
477
- `)}function _i(e){return["","## Terminal Nodes (where the workflow ends)",...e.map(r=>`- ${r.id}: ${yo(r)}`)].join(`
476
+ `)}function Wi(e){return["","## Workflow Steps (execution order)",...e.map((r,o)=>`${String(o+1)}. ${go(r)}`)].join(`
477
+ `)}function _i(e){return["","## Terminal Nodes (where the workflow ends)",...e.map(r=>`- ${r.id}: ${go(r)}`)].join(`
478
478
  `)}function Li({actionCount:e,assertionCount:t,orderedNodes:r,terminalNodes:o}){return["","## Statistics",`- Total nodes: ${String(r.length)}`,`- Action nodes: ${String(e)}`,`- Assertion nodes: ${String(t)}`,`- Terminal nodes: ${String(o.length)}`].join(`
479
479
  `)}function Di(e){return e.length===0?[]:[["","## Flags",...e.map(r=>`- ${r}`)].join(`
480
- `)]}var Wi=["","## 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(`
481
- `);function bo(e){let t=qi(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=ke.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(i=>({message:i.message,path:i.path.join(".")})),valid:!1};let o=Ce(r.data.spec);if(!o.success)return{errors:o.errors,valid:!1};let n=Bi(o.data);return n.length>0?{errors:n,valid:!1}:{errors:[],valid:!0}}function qi(e){try{return{data:Ue(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}function Bi(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(([i,d])=>{wo(d).forEach(l=>{t.has(l.id)||r.push({message:`references non-existent node "${l.id}"`,path:`nodes.${i}.${l.field}`})})});let o=new Set,n=t.has(e.entryNode)?[e.entryNode]:[],a=0;for(;a<n.length;){let i=n[a];if(a+=1,i==null||o.has(i))continue;o.add(i);let d=e.nodes[i];d!=null&&wo(d).forEach(l=>{o.has(l.id)||n.push(l.id)})}return t.forEach(i=>{o.has(i)||r.push({message:"unreachable from entryNode",path:`nodes.${i}`})}),r}function wo(e){let t=[];typeof e.next=="string"&&t.push({field:"next",id:e.next}),["consequent","alternate","body","catch","finally","nodes"].forEach(n=>{let a=e[n];Array.isArray(a)&&a.forEach((i,d)=>{typeof i=="string"&&t.push({field:`${n}[${String(d)}]`,id:i})})});let o=e.branches;return Array.isArray(o)&&o.forEach((n,a)=>{Array.isArray(n)&&n.forEach((i,d)=>{typeof i=="string"&&t.push({field:`branches[${String(a)}][${String(d)}]`,id:i})})}),t}import So from"fs";import Mi from"path";function ko(e){let t=Gi(),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. IMPORTANT: Wrap all precondition routes behind `ENABLE_RIPPLO_TESTING=true` env var \u2014 enable in dev/staging/CI but never in production. All requests are signed using Standard Webhooks (HMAC-SHA256) \u2014 add signature verification middleware using the `standardwebhooks` package. See get_precondition_schema for details."," 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. Verify they are guarded behind `ENABLE_RIPPLO_TESTING=true` env var \u2014 if the guard is missing, add it."," 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(`
482
- `)}function Gi(){let e=Mi.join(process.cwd(),".ripplo","workflows");return So.existsSync(e)?!So.readdirSync(e).some(r=>r.endsWith(".json")):!0}import xo from"fs";import zi from"path";var fe="## 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.",vo="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 Ro(e){return`# Debug a Failing Workflow
480
+ `)]}var qi=["","## 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` and `review_workflow` again."].join(`
481
+ `);import re from"fs";import le from"path";import{chromium as Yi}from"playwright";import De from"fs";import yo from"path";import{z as Q}from"zod";var Bi=Q.object({baseUrl:Q.string().optional(),cloudBaseUrl:Q.string(),preconditionApiPath:Q.string().optional(),projectId:Q.string()});function gt(e){let t=yo.join(e,".ripplo","settings.json");if(!De.existsSync(t))return{baseUrl:void 0,cloudBaseUrl:void 0,errors:[{message:".ripplo/settings.json not found",path:""}],preconditionApiPath:void 0,valid:!1,warnings:[]};let r=Gi(t);if(!r.success)return{baseUrl:void 0,cloudBaseUrl:void 0,errors:[{message:r.error,path:"settings.json"}],preconditionApiPath:void 0,valid:!1,warnings:[]};let o=Bi.safeParse(r.data);if(!o.success)return{baseUrl:void 0,cloudBaseUrl:void 0,errors:o.error.issues.map(c=>({message:c.message,path:c.path.join(".")})),preconditionApiPath:void 0,valid:!1,warnings:[]};let n=[],a=yo.join(e,".ripplo","graph.json");return De.existsSync(a)&&Mi(a)&&(o.data.preconditionApiPath==null||o.data.preconditionApiPath.length===0)&&n.push("Graph has preconditions but preconditionApiPath is not set in settings.json. Preconditions will not be executed. Set preconditionApiPath to the URL where your app serves precondition endpoints (can be a relative path appended to baseUrl, or an absolute URL)."),{baseUrl:o.data.baseUrl,cloudBaseUrl:o.data.cloudBaseUrl,errors:[],preconditionApiPath:o.data.preconditionApiPath,valid:!0,warnings:n}}async function qe(e){try{await fetch(e,{method:"HEAD",signal:AbortSignal.timeout(5e3)});return}catch{return`baseUrl (${e}) is not responding. Make sure your dev server is running on that port.`}}function Mi(e){return yt(e)>0}function yt(e){try{let t=De.readFileSync(e,"utf8"),r=JSON.parse(t),n=Q.object({preconditions:Q.record(Q.string(),Q.unknown()).optional()}).safeParse(r);return n.success?Object.keys(n.data.preconditions??{}).length:0}catch{return 0}}async function wo({baseUrl:e,preconditionApiPath:t}){let r=t.startsWith("http://")||t.startsWith("https://")?t:`${e}${t}`;try{return(await fetch(`${r}/check`,{body:JSON.stringify({precondition:"__ripplo_health_check"}),headers:{"Content-Type":"application/json"},method:"POST",signal:AbortSignal.timeout(5e3)})).ok?"Precondition endpoint accepted an unsigned request \u2014 webhook signature verification may be missing or misconfigured. All precondition endpoints must verify the webhook-id, webhook-signature, and webhook-timestamp headers using the Standard Webhooks spec. See get_precondition_schema for setup instructions.":void 0}catch{return}}function Gi(e){try{let t=De.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}}}import bt from"fs";import St from"path";import zi from"fs";import Ig from"path";import{z as V}from"zod";var wt=V.object({edges:V.array(V.object({from:V.string(),to:V.string(),workflow:V.string()})),preconditions:V.record(V.string(),V.unknown()).optional(),resetPrecondition:V.string().optional(),states:V.record(V.string(),V.object({preconditions:V.array(V.string()),route:V.string()})),version:V.literal(3)});function bo(e){let t=zi.readFileSync(e,"utf8"),r=JSON.parse(t);return wt.parse(r)}function So(e){return{edges:e.edges,preconditions:e.preconditions??{},resetPrecondition:e.resetPrecondition,states:e.states,version:e.version}}function ko(e){let t=Hi(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=wt.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(g=>({message:g.message,path:g.path.join(".")})),valid:!1};let o=So(r.data),n=dt(o);if(!n.success)return{errors:n.errors,valid:!1};let a=[...ut(n.data)],i=St.join(St.dirname(e),"workflows"),c=bt.existsSync(i),l=new Set(n.data.edges.map(p=>p.workflow));return n.data.edges.forEach((p,g)=>{if(!c){a.push({message:`References workflow "${p.workflow}" but workflows directory does not exist`,path:`edges[${String(g)}].workflow`});return}let f=St.join(i,`${p.workflow}.json`);bt.existsSync(f)||a.push({message:`References workflow "${p.workflow}" but no file exists at workflows/${p.workflow}.json`,path:`edges[${String(g)}].workflow`})}),c&&bt.readdirSync(i).filter(p=>p.endsWith(".json")).forEach(p=>{let g=p.replace(/\.json$/,"");l.has(g)||a.push({message:`Workflow file "workflows/${p}" is not referenced by any edge`,path:`workflows/${p}`})}),{errors:a,valid:a.length===0}}function Hi(e){try{return{data:bo(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}function xo(e){let t=Ji(e);if(!t.success)return{errors:[{message:t.error,path:""}],valid:!1};let r=xe.safeParse(t.data);if(!r.success)return{errors:r.error.issues.map(i=>({message:i.message,path:i.path.join(".")})),valid:!1};let o=Ne(r.data.spec);if(!o.success)return{errors:o.errors,valid:!1};let n=Ki(o.data);return n.length>0?{errors:n,valid:!1}:{errors:[],valid:!0}}function Ji(e){try{return{data:We(e),success:!0}}catch(t){return{error:t instanceof Error?t.message:"Failed to read file",success:!1}}}function Ki(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(([i,c])=>{vo(c).forEach(l=>{t.has(l.id)||r.push({message:`references non-existent node "${l.id}"`,path:`nodes.${i}.${l.field}`})})});let o=new Set,n=t.has(e.entryNode)?[e.entryNode]:[],a=0;for(;a<n.length;){let i=n[a];if(a+=1,i==null||o.has(i))continue;o.add(i);let c=e.nodes[i];c!=null&&vo(c).forEach(l=>{o.has(l.id)||n.push(l.id)})}return t.forEach(i=>{o.has(i)||r.push({message:"unreachable from entryNode",path:`nodes.${i}`})}),r}function vo(e){let t=[];typeof e.next=="string"&&t.push({field:"next",id:e.next}),["consequent","alternate","body","catch","finally","nodes"].forEach(n=>{let a=e[n];Array.isArray(a)&&a.forEach((i,c)=>{typeof i=="string"&&t.push({field:`${n}[${String(c)}]`,id:i})})});let o=e.branches;return Array.isArray(o)&&o.forEach((n,a)=>{Array.isArray(n)&&n.forEach((i,c)=>{typeof i=="string"&&t.push({field:`branches[${String(a)}][${String(c)}]`,id:i})})}),t}async function Ro(e){let t=Qi(e),r=Xi(e),o=Zi(e),n=el(),a=tl(e),i=rl(e),c=await ol(e,t);return[t,r,o,...c,a,i,n]}function Qi(e){let t=gt(e);return t.valid?{missingFields:[],type:"settings",valid:!0}:{missingFields:t.errors.map(o=>o.path).filter(o=>o.length>0),type:"settings",valid:!1}}function Xi(e){let t=le.join(e,".ripplo","settings.local.json");if(!re.existsSync(t))return{exists:!1,hasToken:!1,hasWebhookSecret:!1,type:"local-settings"};try{let r=re.readFileSync(t,"utf8"),o=JSON.parse(r);if(o==null||typeof o!="object")return{exists:!0,hasToken:!1,hasWebhookSecret:!1,type:"local-settings"};let n=Object.fromEntries(Object.entries(o)),a=typeof n.token=="string"&&n.token.length>0,i=typeof n.webhookSecret=="string"&&n.webhookSecret.length>0;return{exists:!0,hasToken:a,hasWebhookSecret:i,type:"local-settings"}}catch{return{exists:!0,hasToken:!1,hasWebhookSecret:!1,type:"local-settings"}}}function Zi(e){let t=le.join(e,".mcp.json");if(!re.existsSync(t))return{installed:!1,type:"mcp-installed"};try{let r=re.readFileSync(t,"utf8"),o=JSON.parse(r);if(o==null||typeof o!="object")return{installed:!1,type:"mcp-installed"};let a=Object.fromEntries(Object.entries(o)).mcpServers;return a==null||typeof a!="object"?{installed:!1,type:"mcp-installed"}:{installed:"ripplo"in a,type:"mcp-installed"}}catch{return{installed:!1,type:"mcp-installed"}}}function el(){let e=Yi.executablePath();return{installed:re.existsSync(e),type:"browser"}}function tl(e){let t=le.join(e,".ripplo","graph.json");if(!re.existsSync(t))return{errorCount:0,found:!1,type:"state-graph",valid:!1};let r=ko(t);return{errorCount:r.errors.length,found:!0,type:"state-graph",valid:r.valid}}function rl(e){let t=le.join(e,".ripplo","workflows");if(!re.existsSync(t))return{invalidNames:[],total:0,type:"workflows",valid:0};let r=re.readdirSync(t).filter(n=>n.endsWith(".json")).map(n=>le.join(t,n)),o=[];return r.forEach(n=>{xo(n).valid||o.push(le.basename(n,".json"))}),{invalidNames:o,total:r.length,type:"workflows",valid:r.length-o.length}}async function ol(e,t){if(!t.valid)return[];let r=gt(e),o=[];if(r.baseUrl!=null){let a=await qe(r.baseUrl)==null;o.push({baseUrl:r.baseUrl,reachable:a,type:"dev-server"})}if(r.cloudBaseUrl!=null){let a=await qe(r.cloudBaseUrl)==null;o.push({reachable:a,type:"cloud-base-url",url:r.cloudBaseUrl})}let n=await nl(r);return o.push(...n),o}async function nl(e){let t=le.join(process.cwd(),".ripplo","graph.json"),r=yt(t),o=e.preconditionApiPath!=null&&e.preconditionApiPath.length>0,n={apiPathConfigured:o,count:r,endpointReachable:void 0,type:"preconditions"};if(r===0||!o)return[n];let a=al(e.baseUrl,e.preconditionApiPath);if(a==null)return[n];let i=await qe(a)==null,c=[];if(c.push({apiPathConfigured:!0,count:r,endpointReachable:i,type:"preconditions"}),Po(e.preconditionApiPath)&&c.push({reachable:i,type:"precondition-endpoint",url:a}),!i||e.baseUrl==null)return c;let l=await wo({baseUrl:e.baseUrl,preconditionApiPath:e.preconditionApiPath});return c.push({rejectsUnsigned:l==null,type:"webhook-verification"}),c}function Po(e){return e.startsWith("http://")||e.startsWith("https://")}function al(e,t){if(Po(t))return t;if(e!=null)return`${e}${t}`}function To(e){switch(e.type){case"settings":return sl(e);case"local-settings":return dl(e);case"dev-server":return il(e);case"preconditions":return ul(e);case"webhook-verification":return ll(e);case"state-graph":return pl(e);case"workflows":return fl(e);case"browser":return cl(e);case"cloud-base-url":return e.reachable?`\u2713 Cloud: ${e.url} is reachable`:`! Cloud: ${e.url} is not reachable (ok if not deployed yet)`;case"precondition-endpoint":return e.reachable?`\u2713 Precondition endpoint: ${e.url} is reachable`:`\u2717 Precondition endpoint: ${e.url} is not reachable`;case"mcp-installed":return e.installed?"\u2713 MCP: Ripplo MCP server configured in .mcp.json":"\u2717 MCP: Ripplo MCP server not found in .mcp.json. Run: ripplo setup-mcp"}}function sl(e){return e.valid?"\u2713 Settings: Project configured":`\u2717 Settings: Missing fields: ${e.missingFields.join(", ")}`}function il(e){return e.reachable?`\u2713 Dev server: ${e.baseUrl} is reachable`:`\u2717 Dev server: ${e.baseUrl} is not responding. Make sure your dev server is running.`}function ll(e){return e.rejectsUnsigned?"\u2713 Webhook verification: Unsigned requests are correctly rejected":"\u2717 Webhook verification: Endpoint accepted an unsigned request \u2014 webhook signature verification may be missing or misconfigured. See get_precondition_schema for setup instructions."}function cl(e){return e.installed?"\u2713 Browser: Chromium installed":"\u2717 Browser: Chromium not installed. Run: npx playwright install chromium"}function dl(e){if(!e.exists)return"\u2717 Auth: settings.local.json not found. Run `ripplo login` to authenticate.";let t=[];return e.hasToken||t.push("token"),e.hasWebhookSecret||t.push("webhookSecret"),t.length===0?"\u2713 Auth: Token and webhook secret configured":`\u2717 Auth: Missing ${t.join(" and ")} in settings.local.json`}function ul(e){return e.count===0?"! Preconditions: None defined in graph":e.apiPathConfigured?e.endpointReachable===void 0?`! Preconditions: ${String(e.count)} defined (could not verify endpoint)`:`\u2713 Preconditions: ${String(e.count)} defined, endpoint configured`:`\u2717 Preconditions: ${String(e.count)} defined but preconditionApiPath is not set in settings.json`}function pl(e){return e.found?e.valid?"\u2713 State graph: Valid":`\u2717 State graph: ${String(e.errorCount)} validation error${e.errorCount===1?"":"s"}. Run validate for details.`:"\u2717 State graph: graph.json not found"}function fl(e){return e.total===0?"! Workflows: No workflow files found in .ripplo/workflows/":e.invalidNames.length===0?`\u2713 Workflows: ${String(e.total)} valid`:`\u2717 Workflows: ${String(e.invalidNames.length)} invalid (${e.invalidNames.join(", ")}). Run validate for details.`}import Co from"fs";import ml from"path";function Ao(e){let t=hl(),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. Fix any errors.","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. IMPORTANT: Wrap all precondition routes behind `ENABLE_RIPPLO_TESTING=true` env var \u2014 enable in dev/staging/CI but never in production. All requests are signed using Standard Webhooks (HMAC-SHA256) \u2014 add signature verification middleware using the `standardwebhooks` package. See get_precondition_schema for details."," 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 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. Fix any errors."," f. Check if precondition API endpoints exist at the preconditionApiPath. If not, create them following the pattern in get_precondition_schema. Verify they are guarded behind `ENABLE_RIPPLO_TESTING=true` env var \u2014 if the guard is missing, add it."," 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 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(`
482
+ `)}function hl(){let e=ml.join(process.cwd(),".ripplo","workflows");return Co.existsSync(e)?!Co.readdirSync(e).some(r=>r.endsWith(".json")):!0}import Eo from"fs";import gl from"path";var he="## 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`, 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.",No="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 Io(e){return`# Debug a Failing Workflow
483
483
 
484
- ${Hi(e)}
484
+ ${yl(e)}
485
485
 
486
486
  ## Step 1: Trigger a fresh run
487
487
 
@@ -528,7 +528,7 @@ Categorize the root cause:
528
528
 
529
529
  ## Step 4: Fix and re-run
530
530
 
531
- ${fe}
531
+ ${he}
532
532
 
533
533
  ## Important rules
534
534
 
@@ -536,15 +536,15 @@ ${fe}
536
536
  - **Evidence before changes.** Never modify the spec without citing specific evidence from the debug artifacts.
537
537
  - **Compare steps.** When a failure is unclear, diff the DOM/storage between the step before and the failing step.
538
538
  - **Check the network.** If the app returns unexpected API responses (check network.jsonl), this is likely an app bug, not a spec issue.
539
- - **Report app bugs clearly.** Include: the failing step, what the spec expected, what actually happened, the relevant console/network evidence, and the source code if applicable.`}function Hi(e){if(e!=null)return`Debug the workflow: **${e}**
539
+ - **Report app bugs clearly.** Include: the failing step, what the spec expected, what actually happened, the relevant console/network evidence, and the source code if applicable.`}function yl(e){if(e!=null)return`Debug the workflow: **${e}**
540
540
 
541
- Find it in \`.ripplo/workflows/\`. If the name doesn't match exactly, look for a slug or partial match.`;let t=Ji();return t.length===0?"No workflows found in `.ripplo/workflows/`. Create one first with `/create`.":`No specific workflow was provided. Available workflows:
541
+ Find it in \`.ripplo/workflows/\`. If the name doesn't match exactly, look for a slug or partial match.`;let t=wl();return t.length===0?"No workflows found in `.ripplo/workflows/`. Create one first with `/create`.":`No specific workflow was provided. Available workflows:
542
542
  ${t.map(r=>`- ${r}`).join(`
543
543
  `)}
544
544
 
545
- Ask the user which workflow to debug, or pick the one most likely to be failing.`}function Ji(){let e=zi.join(process.cwd(),".ripplo","workflows");return xo.existsSync(e)?xo.readdirSync(e).filter(t=>t.endsWith(".json")).map(t=>t.replace(/\.json$/,"")):[]}function Po(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?Ki({baseUrlInstruction:t,scope:e.scope}):[Qi(t),Xi(),Zi(),el(),ol(),tl(),rl(),nl()].join(`
545
+ Ask the user which workflow to debug, or pick the one most likely to be failing.`}function wl(){let e=gl.join(process.cwd(),".ripplo","workflows");return Eo.existsSync(e)?Eo.readdirSync(e).filter(t=>t.endsWith(".json")).map(t=>t.replace(/\.json$/,"")):[]}function $o(e){let t="Read `baseUrl` from `.ripplo/settings.json`. If it's not set, find the dev server URL from the project's config (package.json scripts, vite.config.ts, next.config.js, etc.) and set it in settings.json.";return e.scope!=null&&e.scope!==""?bl({baseUrlInstruction:t,scope:e.scope}):[kl(t),vl(),xl(),Rl(),Cl(),Pl(),Tl(),Al()].join(`
546
546
 
547
- `)}function Ki(e){let t=Yi(e.scope);return`# Ripplo Explore (Scoped to Changes)
547
+ `)}function bl(e){let t=Sl(e.scope);return`# Ripplo Explore (Scoped to Changes)
548
548
 
549
549
  Analyze code changes and create or update workflow specs to test the affected user flows.
550
550
 
@@ -592,16 +592,15 @@ For each confirmed flow, delegate to a sub-agent:
592
592
  a. Add them to the graph's preconditions object with appropriate depends chains.
593
593
  b. Add or update the state that this workflow transitions from, ensuring it lists the required preconditions.
594
594
  c. Add the edge connecting the source state to the target state with the workflow slug.
595
- d. Call \`validate_graph\` on .ripplo/graph.json. Fix any errors.
596
- e. Call \`validate_settings\` to verify preconditionApiPath is set. Fix any warnings.
595
+ d. Call \`validate\`. Fix any errors.
597
596
  f. Check if precondition API endpoints exist at the preconditionApiPath. If not, create them following the pattern in \`get_precondition_schema\`.
598
597
  g. Run \`ripplo generate-types\` to regenerate the typed schemas after adding preconditions.
599
598
  4. Write the workflow to \`.ripplo/workflows/<slug>.json\`.
600
- 5. Call \`validate_workflow\` on the file. Fix any errors.
599
+ 5. Call \`validate\`. Fix any errors.
601
600
  6. Call \`review_workflow\` on the file. Read the report and fix any gaps \u2014 ensure assertions verify the expectedOutcome, not just intermediate states. Re-validate after changes.
602
601
  7. Call \`run\` with the workflow slug. Report the result and include the run URL.
603
602
 
604
- ${fe}
603
+ ${he}
605
604
 
606
605
  ## Step 5: Report summary
607
606
 
@@ -609,7 +608,7 @@ After all workflows are processed, report:
609
608
  - Total workflows created/updated
610
609
  - Pass/fail status for each
611
610
  - Any flows that need user attention (app bugs, unresolvable failures)
612
- - Any flows that were skipped and why`}function Yi(e){return`The user provided this context: "${e}"
611
+ - Any flows that were skipped and why`}function Sl(e){return`The user provided this context: "${e}"
613
612
  - If it looks like a PR number or URL, run \`gh pr diff <number>\` and \`gh pr view <number>\` to get the changes and description.
614
613
  - If it looks like a branch name, run \`git diff main...<branch> --stat\` to see what changed.
615
614
  - If it equals "changes" or is empty, auto-detect:
@@ -617,7 +616,7 @@ After all workflows are processed, report:
617
616
  - If there are uncommitted changes, use those as the diff.
618
617
  - Otherwise, run \`git diff main...HEAD --stat\` to see what changed on this branch vs main.
619
618
  - If on main with no changes, ask the user what they'd like to create workflows for.
620
- - Otherwise, treat it as a description and run \`git diff main...HEAD --stat\` to find relevant changes.`}function Qi(e){return`# Ripplo Explore
619
+ - Otherwise, treat it as a description and run \`git diff main...HEAD --stat\` to find relevant changes.`}function kl(e){return`# Ripplo Explore
621
620
 
622
621
  Build a comprehensive state graph and workflow specs for this application.
623
622
 
@@ -653,7 +652,7 @@ This is a large task. **Aggressively delegate to sub-agents** to keep the main c
653
652
  - **Workflow generation**: Always use sub-agents for writing workflow specs. Launch them in parallel batches.
654
653
  - **Precondition API implementation**: Delegate endpoint implementation entirely to a sub-agent. Give it the graph's precondition names, the auth/data architecture summary from exploration, and let it handle reading source code, writing handlers, and wiring up the codegen. Do NOT read app source code for endpoint implementation in the main context.
655
654
  - **General rule**: If a task involves reading more than 2-3 files or writing more than one file, it should be a sub-agent.
656
- - **Recursive sub-agents**: Sub-agents should spawn their own sub-agents when they hit something that needs deeper exploration. An exploration agent that discovers a complex auth middleware layer should spawn a deeper agent to trace it fully. A workflow generation agent that needs to understand a component tree should spawn an agent to map it. There is no depth limit \u2014 go as deep as needed to get complete, accurate results. Each level returns a structured summary to its parent.`}function Xi(){return`## Understanding the application
655
+ - **Recursive sub-agents**: Sub-agents should spawn their own sub-agents when they hit something that needs deeper exploration. An exploration agent that discovers a complex auth middleware layer should spawn a deeper agent to trace it fully. A workflow generation agent that needs to understand a component tree should spawn an agent to map it. There is no depth limit \u2014 go as deep as needed to get complete, accurate results. Each level returns a structured summary to its parent.`}function vl(){return`## Understanding the application
657
656
 
658
657
  Explore the codebase deeply enough to discover every testable surface. **Use sub-agents for all exploration** \u2014 the main context should not read source files directly during discovery. Launch exploration agents in parallel where possible, and have each return a structured summary (routes found, components found, entities found, etc.) rather than raw file contents.
659
658
 
@@ -727,7 +726,7 @@ Map every write operation to its UI trigger:
727
726
  - Which UI component triggers each mutation
728
727
  - Form actions (if using framework form actions)
729
728
  - Optimistic updates and their rollback behavior
730
- - Which mutations are restricted to which roles`}function Zi(){return`## Extracting edges
729
+ - Which mutations are restricted to which roles`}function xl(){return`## Extracting edges
731
730
 
732
731
  From everything discovered, extract EVERY meaningful user journey as an edge.
733
732
 
@@ -755,7 +754,7 @@ Before building the graph, verify completeness:
755
754
  - **Component coverage**: Is every interactive component from the inventory covered by at least one edge? If not, add an edge or note why it's excluded.
756
755
  - **Entity coverage**: For every core entity, is there a create, update, and delete edge? Missing CRUD operations are coverage gaps.
757
756
  - **Role coverage**: If the app has multiple roles, are role-specific actions covered? (e.g., "admin can invite members" vs "member cannot")
758
- - **Empty/conditional state coverage**: Are empty states and conditional branches covered? (e.g., "first project creation from empty state" vs "additional project from populated list")`}function el(){return"## Building the graph\n\n1. Call `get_graph_documentation` to learn the schema (includes JSON Schema generated from the Zod types).\n2. Define **preconditions** at the graph level for every auth role and data requirement discovered. Name them with category prefixes: `auth:admin`, `auth:viewer`, `data:projects`, `data:empty-state`. Use `depends` when one precondition requires another (e.g., `data:three-projects` depends on `auth:admin`).\n3. Define **states** for every unique application context. Two states at the same route are distinct if they have different available edges or different UI (e.g., empty list vs populated list, dialog open vs closed). Each state references its preconditions by name.\n4. Define **edges** connecting states \u2014 one per user journey.\n5. Set `preconditionApiPath` in `.ripplo/settings.json` to where the app's precondition endpoints will live. Can be a relative path (appended to `baseUrl`) or an absolute URL.\n6. Write to `.ripplo/graph.json`, call `validate_graph`, fix errors.\n7. Call `validate_settings` to verify `preconditionApiPath` is set. Fix any warnings."}function tl(){return`## Bootstrap: verify preconditions with one workflow
757
+ - **Empty/conditional state coverage**: Are empty states and conditional branches covered? (e.g., "first project creation from empty state" vs "additional project from populated list")`}function Rl(){return"## Building the graph\n\n1. Call `get_graph_documentation` to learn the schema (includes JSON Schema generated from the Zod types).\n2. Define **preconditions** at the graph level for every auth role and data requirement discovered. Name them with category prefixes: `auth:admin`, `auth:viewer`, `data:projects`, `data:empty-state`. Use `depends` when one precondition requires another (e.g., `data:three-projects` depends on `auth:admin`).\n3. Define **states** for every unique application context. Two states at the same route are distinct if they have different available edges or different UI (e.g., empty list vs populated list, dialog open vs closed). Each state references its preconditions by name.\n4. Define **edges** connecting states \u2014 one per user journey.\n5. Set `preconditionApiPath` in `.ripplo/settings.json` to where the app's precondition endpoints will live. Can be a relative path (appended to `baseUrl`) or an absolute URL.\n6. Write to `.ripplo/graph.json`, call `validate`, fix errors."}function Pl(){return`## Bootstrap: verify preconditions with one workflow
759
758
 
760
759
  Before generating all workflows, validate that the precondition API works end-to-end.
761
760
 
@@ -769,7 +768,7 @@ Before generating all workflows, validate that the precondition API works end-to
769
768
  - **App-level bug** (the app itself is broken): note it and move on \u2014 this is not fixable from the test side.
770
769
  6. **Iterate until this first workflow passes.** This proves auth, data seeding, and the run infrastructure all work.
771
770
 
772
- **Do NOT proceed to batch workflow generation until the bootstrap workflow passes.** Every issue you fix here prevents the same failure from hitting all subsequent workflows.`}function rl(){return`## Generating workflow specs
771
+ **Do NOT proceed to batch workflow generation until the bootstrap workflow passes.** Every issue you fix here prevents the same failure from hitting all subsequent workflows.`}function Tl(){return`## Generating workflow specs
773
772
 
774
773
  Each workflow spec should be a **QA-quality test plan**, not just a happy-path click sequence. Think of it like a detailed bug report: describe exactly what happens at each step, what should be visible, what state the UI should be in. A QA engineer should be able to read the spec and know exactly what to verify.
775
774
 
@@ -810,10 +809,10 @@ For each edge, create and verify a workflow spec:
810
809
  2. Read the relevant component files to find **real locators** (test IDs, button text, form fields, aria labels). **Never fabricate locators** \u2014 always trace them to source code. Read deeply \u2014 follow imports, read sub-components, find the actual rendered elements.
811
810
  3. Add thorough assertions after every meaningful action \u2014 not just URL checks, but visibility, text content, and state verification.
812
811
  4. Write the spec to \`.ripplo/workflows/<edge-id>.json\`.
813
- 5. Call \`validate_workflow\`, fix errors.
812
+ 5. Call \`validate\`, fix errors.
814
813
  6. **Call \`run\` with the workflow slug.** This is mandatory \u2014 never skip it. After generating a batch, use \`run\` with all slugs from the batch to run them simultaneously.
815
814
  7. **Inspect the result.** Read debug artifacts from \`.ripplo/debug/<runId>/\` \u2014 check \`summary.txt\`, \`console.log\`, and \`steps/<failedIndex>/\` for DOM, storage, and screenshots.
816
- 8. ${vo}
815
+ 8. ${No}
817
816
 
818
817
  ### Parallelism
819
818
 
@@ -827,17 +826,9 @@ Each sub-agent should receive: edge details, component file paths, action sequen
827
826
 
828
827
  ### Final summary
829
828
 
830
- Report: total states, total edges, workflows generated, **pass/fail per workflow**, and any app-level bugs discovered. Group failures by cause (locator issues, precondition issues, app bugs).`}function ol(){return'## Wiring up the Precondition API\n\nOnce the graph is complete, wire up precondition endpoints **before** generating any workflow specs. The app needs these so Ripplo can check and execute preconditions at runtime.\n\n**Delegate this entire section to a sub-agent.** Pass it: the list of precondition names from the graph, the auth architecture summary (how sessions work, how users are created), the data model summary (how to seed entities), and the `preconditionApiPath`. The sub-agent handles everything below \u2014 reading source code, generating types, writing endpoint handlers, and wiring into the dev workflow. It should spawn its own sub-agents if it needs to dig deeper into the auth system, trace session creation logic, or understand how to seed specific entity types.\n\n### Step 1: Generate types\n\nRun `ripplo generate-types --output <path>` to generate Zod schemas and TypeScript types from the graph\'s precondition names. Pick an output path that\'s natural for the project\'s server code (e.g., `src/generated/precondition-api.ts` or `apps/server/src/generated/precondition-api.ts`).\n\nWire this command into the project\'s existing codegen workflow \u2014 look for how the project already runs generators (Prisma, GraphQL codegen, etc.) and add `ripplo generate-types` alongside it in a package.json script, turbo pipeline, or similar.\n\n### Step 2: Implement the endpoints\n\nCreate three endpoints in the app\'s server. Use the generated types for request validation. Refer to `get_precondition_schema` for the full API contract and example code.\n\nFor each precondition in the graph, implement the business logic:\n\n**Check endpoint** (`POST {preconditionApiPath}/check`):\n- Receives `{ "precondition": "auth:admin" }`\n- Returns `{ "satisfied": true/false }`\n- Should be a lightweight query \u2014 check session cookies, count entities, etc.\n\n**Execute endpoint** (`PUT {preconditionApiPath}/execute`):\n- Receives `{ "precondition": "auth:admin" }`\n- Returns `{ "success": true/false, "error"?: "..." }`\n- Creates test users, seeds data, etc. Set-Cookie headers are captured automatically by Ripplo.\n- **Create fresh ephemeral test users** for auth preconditions \u2014 don\'t reuse shared accounts.\n\n**Teardown endpoint** (`PUT {preconditionApiPath}/teardown`):\n- Receives `{ "preconditions": ["auth:admin", "data:projects"] }`\n- Returns `{ "success": true/false }`\n- Cleans up test data created during the run. Fire-and-forget \u2014 errors don\'t fail the run.\n\n### Implementation approach\n\n- Read the app\'s existing auth system to understand how sessions are created programmatically (not via UI login)\n- Read the data model to understand how to seed entities via direct DB access or internal API calls\n- Use the app\'s existing test utilities, factories, or seed scripts if they exist\n- Create a new router/controller for the precondition endpoints \u2014 keep test infrastructure separate from production routes\n- **Wrap all precondition routes behind an environment variable guard** (e.g., `ENABLE_RIPPLO_TESTING`). Only register the routes when this variable is explicitly set to `"true"`. These endpoints create users, seed data, and delete records \u2014 they should be enabled in development and dedicated testing/staging environments, but must never be accessible in production. Add the env var to `.env.local`, CI config, or staging environment config \u2014 not `.env.production`\n- Import the generated schemas from the output path for type-safe request parsing\n- **Common auth pitfalls to avoid:**\n - Do NOT guess auth API methods \u2014 read the actual SDK types/docs (e.g., Supabase has no `admin.createSession()`)\n - Always include `Path=/` and `SameSite=Lax` on auth cookies in Set-Cookie headers\n - For Supabase: use `admin.createUser({ email_confirm: true })` + `signInWithPassword()` to get session tokens, then set the `sb-<ref>-auth-token` cookie\n - For NextAuth: use the adapter\'s `createSession()` method and set the `next-auth.session-token` cookie\n - Call `get_precondition_schema` for framework-specific examples\n - Test the precondition API manually (curl) before triggering a full workflow run\n\n### Parallel safety\n\nMultiple workflows run simultaneously. Every precondition executor must create **isolated, non-conflicting data per run**:\n\n- **Unique identifiers**: Generate unique names/emails/slugs per run using `crypto.randomUUID()` or `Date.now()` suffixes. Never hardcode entity names like `"Test Project"` \u2014 two parallel runs will collide on unique constraints.\n- **Return dynamic IDs via `data`**: The execute response\'s `data` field passes values to the workflow. Return created entity IDs so workflows reference them dynamically (e.g., `{ "projectId": "cuid-abc123" }`).\n- **Scoped teardown**: Teardown must only delete entities created by that specific run. The teardown request includes cookies from the run\'s session \u2014 use them to identify which user/data to clean up. Never use `deleteMany` with a prefix match or `TRUNCATE` \u2014 it destroys other parallel runs\' data.\n- **Independent sessions**: Each run creates its own auth session. Never assume a singleton test user or shared session exists.\n\n### Step 3: Verify\n\nEnsure `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 nl(){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 To(){let e=new al({name:"ripplo",version:"0.1.0"});return sl(e),il(e),ll(e),cl(e),dl(e),e}function sl(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:B.string().describe("Absolute path to the workflow JSON file")}},t=>{let r=bo(t.file);if(r.valid)return F("Valid");let o=r.errors.map(n=>`${n.path}: ${n.message}`);return F(`Invalid:
831
- ${o.join(`
832
- `)}`)}),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."},()=>F(Kr())),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:B.array(B.string()).min(1).describe("Node type names to get schemas for (e.g. ['click', 'fill'])")}},t=>F(Yr({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."},()=>F(Qr())),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:B.string().describe("Absolute path to the workflow JSON file")}},t=>{let r=fo(t.file),o=r.flags.length===0?"PASS":"NEEDS REVIEW";return F(`${o}
833
-
834
- ${r.report}`)})}function il(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."},()=>F(oo())),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."},()=>F(no())),e.registerTool("validate_graph",{description:"Validate the state graph file. MUST be called after every edit to .ripplo/graph.json.",inputSchema:{file:B.string().describe("Absolute path to the graph JSON file")}},t=>{let r=io(t.file);if(r.valid)return F("Valid");let o=r.errors.map(n=>`${n.path}: ${n.message}`);return F(`Invalid:
835
- ${o.join(`
836
- `)}`)}),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:B.string().optional().describe("Working directory (defaults to cwd)")}},async t=>{let r=co(t.dir??process.cwd());if(!r.valid){let a=r.errors.map(i=>`${i.path}: ${i.message}`);return F(`Invalid:
837
- ${a.join(`
838
- `)}`)}let o=[...r.warnings];if(r.baseUrl!=null){let a=await uo(r.baseUrl);a!=null&&o.push(a)}let n=o.length>0?`
839
- Warnings:
840
- ${o.join(`
841
- `)}`:"";return F(`Valid${n}`)})}function ll(e){e.registerPrompt("explore",{argsSchema:{baseUrl:B.string().optional().describe("Base URL of the application (e.g. http://localhost:3000)"),scope:B.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:Po({baseUrl:t.baseUrl,scope:t.scope}),type:"text"},role:"user"}]}))}function cl(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:B.array(B.string()).describe("Workflow slugs to run (supports 1 or many, all run in parallel)")}},t=>Hr(async r=>{try{let o=await ro({config:r,slugs:t.slugs});return F(o.summary)}catch(o){let n=o instanceof Error?o.message:String(o),i=n.includes("fetch failed")||n.includes("Failed to connect")||n.includes("Could not reach")||n.includes("ECONNREFUSED")?"\n\nSetup checklist:\n1. Is `ripplo dev` running in a terminal?\n2. Is the dev server running? Check `baseUrl` in `.ripplo/settings.json` \u2014 is the port correct?\n3. Run the `validate_settings` tool to check for configuration issues.":"";return F(`ERROR: ${n}${i}`)}}))}function dl(e){e.registerPrompt("run",{argsSchema:{workflow:B.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(`
842
- `):`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:B.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:[ko(t.flow??"the most important user flow"),"",fe].join(`
843
- `),type:"text"},role:"user"}]})),e.registerPrompt("debug",{argsSchema:{workflow:B.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:Ro(t.workflow),type:"text"},role:"user"}]}))}async function pl(){let e=To(),t=new ul;await e.connect(t)}pl().catch(e=>{process.exit(1)});
829
+ Report: total states, total edges, workflows generated, **pass/fail per workflow**, and any app-level bugs discovered. Group failures by cause (locator issues, precondition issues, app bugs).`}function Cl(){return'## Wiring up the Precondition API\n\nOnce the graph is complete, wire up precondition endpoints **before** generating any workflow specs. The app needs these so Ripplo can check and execute preconditions at runtime.\n\n**Delegate this entire section to a sub-agent.** Pass it: the list of precondition names from the graph, the auth architecture summary (how sessions work, how users are created), the data model summary (how to seed entities), and the `preconditionApiPath`. The sub-agent handles everything below \u2014 reading source code, generating types, writing endpoint handlers, and wiring into the dev workflow. It should spawn its own sub-agents if it needs to dig deeper into the auth system, trace session creation logic, or understand how to seed specific entity types.\n\n### Step 1: Generate types\n\nRun `ripplo generate-types --output <path>` to generate Zod schemas and TypeScript types from the graph\'s precondition names. Pick an output path that\'s natural for the project\'s server code (e.g., `src/generated/precondition-api.ts` or `apps/server/src/generated/precondition-api.ts`).\n\nWire this command into the project\'s existing codegen workflow \u2014 look for how the project already runs generators (Prisma, GraphQL codegen, etc.) and add `ripplo generate-types` alongside it in a package.json script, turbo pipeline, or similar.\n\n### Step 2: Implement the endpoints\n\nCreate three endpoints in the app\'s server. Use the generated types for request validation. Refer to `get_precondition_schema` for the full API contract and example code.\n\nFor each precondition in the graph, implement the business logic:\n\n**Check endpoint** (`POST {preconditionApiPath}/check`):\n- Receives `{ "precondition": "auth:admin" }`\n- Returns `{ "satisfied": true/false }`\n- Should be a lightweight query \u2014 check session cookies, count entities, etc.\n\n**Execute endpoint** (`PUT {preconditionApiPath}/execute`):\n- Receives `{ "precondition": "auth:admin" }`\n- Returns `{ "success": true/false, "error"?: "..." }`\n- Creates test users, seeds data, etc. Set-Cookie headers are captured automatically by Ripplo.\n- **Create fresh ephemeral test users** for auth preconditions \u2014 don\'t reuse shared accounts.\n\n**Teardown endpoint** (`PUT {preconditionApiPath}/teardown`):\n- Receives `{ "preconditions": ["auth:admin", "data:projects"] }`\n- Returns `{ "success": true/false }`\n- Cleans up test data created during the run. Fire-and-forget \u2014 errors don\'t fail the run.\n\n### Implementation approach\n\n- Read the app\'s existing auth system to understand how sessions are created programmatically (not via UI login)\n- Read the data model to understand how to seed entities via direct DB access or internal API calls\n- Use the app\'s existing test utilities, factories, or seed scripts if they exist\n- Create a new router/controller for the precondition endpoints \u2014 keep test infrastructure separate from production routes\n- **Wrap all precondition routes behind an environment variable guard** (e.g., `ENABLE_RIPPLO_TESTING`). Only register the routes when this variable is explicitly set to `"true"`. These endpoints create users, seed data, and delete records \u2014 they should be enabled in development and dedicated testing/staging environments, but must never be accessible in production. Add the env var to `.env.local`, CI config, or staging environment config \u2014 not `.env.production`\n- Import the generated schemas from the output path for type-safe request parsing\n- **Common auth pitfalls to avoid:**\n - Do NOT guess auth API methods \u2014 read the actual SDK types/docs (e.g., Supabase has no `admin.createSession()`)\n - Always include `Path=/` and `SameSite=Lax` on auth cookies in Set-Cookie headers\n - For Supabase: use `admin.createUser({ email_confirm: true })` + `signInWithPassword()` to get session tokens, then set the `sb-<ref>-auth-token` cookie\n - For NextAuth: use the adapter\'s `createSession()` method and set the `next-auth.session-token` cookie\n - Call `get_precondition_schema` for framework-specific examples\n - Test the precondition API manually (curl) before triggering a full workflow run\n\n### Parallel safety\n\nMultiple workflows run simultaneously. Every precondition executor must create **isolated, non-conflicting data per run**:\n\n- **Unique identifiers**: Generate unique names/emails/slugs per run using `crypto.randomUUID()` or `Date.now()` suffixes. Never hardcode entity names like `"Test Project"` \u2014 two parallel runs will collide on unique constraints.\n- **Return dynamic IDs via `data`**: The execute response\'s `data` field passes values to the workflow. Return created entity IDs so workflows reference them dynamically (e.g., `{ "projectId": "cuid-abc123" }`).\n- **Scoped teardown**: Teardown must only delete entities created by that specific run. The teardown request includes cookies from the run\'s session \u2014 use them to identify which user/data to clean up. Never use `deleteMany` with a prefix match or `TRUNCATE` \u2014 it destroys other parallel runs\' data.\n- **Independent sessions**: Each run creates its own auth session. Never assume a singleton test user or shared session exists.\n\n### Step 3: Verify\n\nEnsure `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 Al(){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` 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 jo(){let e=new Nl({name:"ripplo",version:"0.1.0"});return El(e),Il(e),$l(e),jl(e),Ol(e),e}function El(e){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."},()=>B(eo())),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:H.array(H.string()).min(1).describe("Node type names to get schemas for (e.g. ['click', 'fill'])")}},t=>B(to({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."},()=>B(ro())),e.registerTool("review_workflow",{description:"Review the semantic quality of a workflow spec. Call AFTER validate 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:H.string().describe("Absolute path to the workflow JSON file")}},t=>{let r=po(t.file),o=r.flags.length===0?"PASS":"NEEDS REVIEW";return B(`${o}
830
+
831
+ ${r.report}`)})}function Il(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."},()=>B(lo())),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."},()=>B(co())),e.registerTool("validate",{description:"Run all health checks on the Ripplo project. Validates settings, auth, dev server connectivity, preconditions, webhook verification, state graph, workflows, and browser. MUST be called after every edit to .ripplo/ files. Fix all errors before proceeding.",inputSchema:{dir:H.string().optional().describe("Working directory (defaults to cwd)")}},async t=>{let o=(await Ro(t.dir??process.cwd())).map(n=>To(n));return B(o.join(`
832
+ `))})}function $l(e){e.registerPrompt("explore",{argsSchema:{scope:H.string().default("").describe("Narrow exploration to code changes. Pass a PR number, branch name, 'changes' for local diff, or a description. Leave empty 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:$o({scope:t.scope}),type:"text"},role:"user"}]}))}function jl(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:H.array(H.string()).describe("Workflow slugs to run (supports 1 or many, all run in parallel)")}},t=>Xr(async r=>{try{let o=await io({config:r,slugs:t.slugs});return B(o.summary)}catch(o){let n=o instanceof Error?o.message:String(o),i=n.includes("fetch failed")||n.includes("Failed to connect")||n.includes("Could not reach")||n.includes("ECONNREFUSED")?"\n\nSetup checklist:\n1. Is `ripplo dev` running in a terminal?\n2. Is the dev server running? Check `baseUrl` in `.ripplo/settings.json` \u2014 is the port correct?\n3. Run the `validate` tool to check for configuration issues.":"";return B(`ERROR: ${n}${i}`)}}))}function Ol(e){e.registerPrompt("run",{argsSchema:{workflow:H.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(`
833
+ `):`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:H.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:[Ao(t.flow??"the most important user flow"),"",he].join(`
834
+ `),type:"text"},role:"user"}]})),e.registerPrompt("debug",{argsSchema:{workflow:H.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:Io(t.workflow),type:"text"},role:"user"}]}))}async function Ul(){let e=jo(),t=new Vl;await e.connect(t)}Ul().catch(e=>{process.exit(1)});