momentic 0.0.21 → 0.0.22

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.
Files changed (2) hide show
  1. package/bin/cli.js +1 -1
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -22,7 +22,7 @@ You have already executed the following commands successfully (most recent liste
22
22
  `).forEach(l=>n.push(` ${l}`))):n.push("PAGE CONTENT CHANGE: <TOO_LONG_TO_DISPLAY/>"):n.push("PAGE CONTENT CHANGE: <NONE/>")}n.push("-".repeat(10))}),n.push(`STARTING URL: ${this.browser.baseURL}`),n.join(`
23
23
  `)}getListHistory(){return Ps`Here are the commands that you have successfully executed:
24
24
  ${this.commandHistory.filter(e=>e.type==="AI_ACTION").map(e=>`- ${e.serializedCommand}`).join(`
25
- `)}`}async executeCommand(e,o,r=!1){let n=this.commandHistory[this.commandHistory.length-1];if(!r&&(!n||n.state!=="PENDING"))throw new R("InternalWebAgentError","Executing command but there is no pending entry in the history");if(this.closed)throw new Error("Attempting to execute command on a closed controller");let s;try{let i=Date.now();s=await this.executePresetStep(e,o);let a=Date.now()-i;this.logger.debug({result:s,duration:a},"Got execution result")}catch(i){throw i instanceof Error?new Re(`Failed to execute command: ${i}`,{cause:i}):new Re("Unexpected throw from executing command",{cause:new Error(`${i}`)})}return s.succeedImmediately&&!r&&(this.pendingInstructions.pop(),this.pendingInstructions.length>0&&(s.succeedImmediately=!1)),s.elementInteracted&&"target"in e&&e.target&&!e.target.elementDescriptor&&(e.target.elementDescriptor=s.elementInteracted.trim()),r||(n.generatedStep=e,n.serializedCommand=Se(e),n.state="DONE"),s}async executeAssertion(e){let o=await this.getBrowserState(),r=await this.browser.url(),n;if(e.useVision)n={goal:e.assertion,url:r,screenshot:await this.browser.screenshot(),browserState:"",history:"",numPrevious:-1,lastCommand:null};else{let i=this.getSerializedHistory(r,o);n={goal:e.assertion,url:r,browserState:o,history:i,lastCommand:this.lastExecutedCommand,numPrevious:this.commandHistory.length}}let s=await this.generator.getAssertionResult(n,e.useVision,e.disableCache);if(s.relevantElements&&Promise.all(s.relevantElements.map(i=>this.browser.highlight({id:i}))),!s.result)throw new R("AssertionFailureError",s.thoughts);return{succeedImmediately:!1,thoughts:s.thoughts,urlAfterCommand:r}}async wrapElementTargetingCommand(e,o,r,n,s=1){if(!e.a11yData&&!e.elementDescriptor)throw new R("InternalWebAgentError","Cannot target element with no target data or element descriptor");let i=e.a11yData&&je(e.a11yData);e.a11yData||(this.logger.debug("No cached locator data for target, prompting AI for fresh location"),s--,e.a11yData=Ge.parse(await this.locateElement(e.elementDescriptor,o,r)));try{let a=await n(e.a11yData);return i?this.logger.debug({target:e},"Successfully used cached target to perform action"):this.logger.debug({target:e},"Successfully generated and used new a11y target information"),a}catch(a){if(s>0&&e.elementDescriptor)return this.logger.warn({err:a,target:e},"Failed to execute action with cached target, retrying with AI"),e.a11yData=void 0,this.wrapElementTargetingCommand(e,o,r,n,s);if(a instanceof R)throw a;let l=`Failed to find ${e.elementDescriptor?`${e.elementDescriptor}`:"element"}: ${a instanceof Error?a.message:a}`;throw this.logger.error({err:a,target:e},l),new R("ActionFailureError",l,{cause:a})}}async executePresetStep(e,o){try{return await this.executePresetStepHelper(e,o)}catch(r){throw r instanceof R?r:new R("InternalWebAgentError",r instanceof Error?r.message:`${r}`,{cause:r})}}async executePresetStepHelper(e,o){switch(e.type){case"SUCCESS":return e.condition?.assertion.trim()?this.executeAssertion(e.condition):{succeedImmediately:!1,urlAfterCommand:await this.browser.url()};case"AI_ASSERTION":return this.executeAssertion(e);case"NAVIGATE":if(!Ot(e.url)&&!Mt(e.url,this.browser.baseURL))throw new R("ActionFailureError",`Invalid URL: ${e.url}`);await this.browser.navigate({url:e.url});break;case"CAPTCHA":let r=await this.browser.solveCaptcha();r&&(await this.wrapElementTargetingCommand({elementDescriptor:"the captcha image solution input"},e.useVision,o,l=>this.browser.click(l)),await this.browser.type(r,{clearContent:!0,pressKeysSequentially:!1}));break;case"GO_BACK":await this.browser.goBack();break;case"GO_FORWARD":await this.browser.goForward();break;case"SCROLL_DOWN":case"SCROLL_UP":let n;return e.target&&(e.target.elementDescriptor.trim()||e.target.a11yData)&&(n=await this.wrapElementTargetingCommand(e.target,e.useVision,o,l=>this.browser.hover(l))),e.type==="SCROLL_UP"?await this.browser.scrollUp(e.deltaY):await this.browser.scrollDown(e.deltaY),{succeedImmediately:!1,urlAfterCommand:await this.browser.url(),elementInteracted:n};case"WAIT":await this.browser.wait(e.delay*1e3);break;case"REFRESH":await this.browser.refresh();break;case"CLICK":{let l=await this.browser.url(),d=await this.wrapElementTargetingCommand(e.target,e.useVision,o,u=>this.browser.click(u,{doubleClick:e.doubleClick,rightClick:e.rightClick,force:e.force})),c={urlAfterCommand:await this.browser.url(),succeedImmediately:!1,elementInteracted:d};return ce(l,c.urlAfterCommand)&&(c.succeedImmediately=!0,c.succeedImmediatelyReason="URL changed"),c}case"SELECT_OPTION":{let l=await this.wrapElementTargetingCommand(e.target,!1,o,d=>this.browser.selectOption(d,e.option));return{succeedImmediately:!1,urlAfterCommand:await this.browser.url(),elementInteracted:l}}case"TAB":await this.browser.switchToPage(e.url);break;case"COOKIE":if(!e.value)break;await this.browser.setCookie(e.value);break;case"LOCAL_STORAGE":if(!e.value||!e.key)break;await this.browser.setLocalStorage(e.key,e.value);break;case"TYPE":{let l=await this.browser.url(),d=await this.wrapElementTargetingCommand(e.target,e.useVision,o,u=>this.browser.click(u,{force:e.force}));await this.browser.type(e.value,{clearContent:e.clearContent,pressKeysSequentially:e.pressKeysSequentially}),e.pressEnter&&await this.browser.press("Enter");let c={urlAfterCommand:await this.browser.url(),succeedImmediately:!1,elementInteracted:d};return ce(l,c.urlAfterCommand)&&(c.succeedImmediately=!0,c.succeedImmediatelyReason="URL changed"),c}case"HOVER":{let l=await this.wrapElementTargetingCommand(e.target,e.useVision,o,d=>this.browser.hover(d));return{succeedImmediately:!1,urlAfterCommand:await this.browser.url(),elementInteracted:l}}case"PRESS":let s=await this.browser.url();await this.browser.press(e.value);let i={urlAfterCommand:await this.browser.url(),succeedImmediately:!1};return ce(s,i.urlAfterCommand)&&(i.succeedImmediately=!0,i.succeedImmediatelyReason="URL changed"),i;case"REQUEST":{let l=e.timeout??30,d=null,c=new AbortController,u=Object.fromEntries(Object.entries(e.headers||{}).filter(([v,b])=>v&&b)),m=new URLSearchParams;Object.entries(e.params||{}).filter(([v,b])=>v&&b).forEach(([v,b])=>{m.append(v,b)});let p;if(Ot(e.url)&&(p=e.url),Mt(e.url,this.browser.baseURL)&&(p=new URL(e.url,this.browser.baseURL).toString()),!p)throw new R("ActionFailureError",`Invalid URL: ${e.url}`);let f=async()=>{try{d=await fetch(`${p}?${m.toString()}`,{headers:u,method:e.method,body:e.body,signal:c.signal})}catch(v){this.logger.error({err:v},"Failed to make fetch request")}},w=async()=>{await new Promise(v=>setTimeout(v,l*1e3)),c.abort()};await Promise.race([w(),f()]);let g=d;if(!g)throw new R("ActionFailureError",`Fetch request timed out after ${l} seconds`);if(!g.ok)throw new R("ActionFailureError",`Fetch request failed with status ${g.status}`);let N={status:g.status,headers:g.headers};return g.headers.get("content-type")?.includes("json")?N.json=await g.json():N.text=await g.text(),{succeedImmediately:!1,urlAfterCommand:await this.browser.url(),data:N}}default:return(l=>{throw"If Typescript complains about the line below, you missed a case or break in the switch above"})(e)}return{succeedImmediately:!1,urlAfterCommand:await this.browser.url()}}async getReverseMappedTarget(e,o,r){return(await this.generator.getReverseMappedDescription({browserState:e.browserState,goal:`${o}`},r)).phrase}};async function gr({socket:t,generator:e,logger:o,emitScreenshots:r,rootController:n}){let s=o.child({package:"web-agent"}),i=t.handshake.query.testId,a=t.handshake.query.baseUrl;if(!i)throw new Error("Socket connection is missing testID");let l=t.handshake.query.orgId,d=zs(),c=p=>{t.emit("screenshot",{...p,url:a})},u=n;if(u)u.browser.setActiveFrame(Je);else{let p=await x.init({baseUrl:a,logger:s,sendScreenshotsDuringLoad:r?c:void 0});u=new Y({browser:p,generator:e,config:me,logger:s})}r&&Yt(t,d,o),k.registerSession(u,d);let m=x.USER_AGENT;return t.emit("session",{url:a,userAgent:m,viewport:await u.browser.viewport()}),{sessionId:d,testId:i,orgId:l}}var yr=[pr,Xt,fr,hr];var Sr=t=>{let{logger:e}=t,o=new Us(t.baseServer,{cors:{origin:"*",methods:["GET","POST"]}});return o.on("connection",async r=>{let n;try{n=await gr({...t,socket:r})}catch(s){e.error({event:"connection",type:"websocket",err:s},"Failed to setup connection"),r.emit("error",{message:`${s}`});return}yr.forEach(s=>Fs(s,{socket:r,metadata:n,...t}))}),o},Fs=(t,e)=>{let o=t.createHandler(e),r=(...n)=>{e.logger.debug({event:t.event,metadata:e.metadata,args:n},"Websocket event");let s=i=>{e.logger.error({event:t.event,type:"websocket",args:n,err:i instanceof Error?i:new Error(`${i}`)},"Unhandled exception in socket handler"),e.socket.emit("error",{message:`${i}`})};try{let i=o.apply(void 0,n);i&&typeof i.catch=="function"&&i.catch(s)}catch(i){s(i)}};e.socket.on(t.event,r)};import $s from"fetch-retry";var Hs=$s(global.fetch),ue="v1",pe=class{baseURL;apiKey;constructor(e){this.baseURL=e.baseURL,this.apiKey=e.apiKey}async getElementLocation(e,o){let r=await this.sendRequest(`/${ue}/web-agent/locate-element`,{browserState:e.browserState,goal:e.goal,disableCache:o});return Ct.parse(r)}async getElementLocationWithVision(e,o){let r=await this.sendRequest(`/${ue}/web-agent/visual-locate`,{goal:e.goal,screenshot:e.screenshot?.toString("base64"),hintActivatedScreenshot:e.hintActivatedScreenshot?.toString("base64"),disableCache:o});return Ct.parse(r)}async getAssertionResult(e,o,r){if(o){let s=await this.sendRequest(`/${ue}/web-agent/assertion`,{url:e.url,goal:e.goal,screenshot:e.screenshot?.toString("base64"),disableCache:r,vision:!0});return vt.parse(s)}let n=await this.sendRequest(`/${ue}/web-agent/assertion`,{url:e.url,browserState:e.browserState,goal:e.goal,history:e.history,numPrevious:e.numPrevious,lastCommand:e.lastCommand,disableCache:r,vision:!1});return vt.parse(n)}async getProposedCommand(e,o){let r=await this.sendRequest(`/${ue}/web-agent/next-command`,{url:e.url,browserState:e.browserState,goal:e.goal,history:e.history,numPrevious:e.numPrevious,lastCommand:e.lastCommand,disableCache:o});return So.parse(r)}async getGranularGoals(e,o){let r=await this.sendRequest(`/${ue}/web-agent/split-goal`,{url:e.url,goal:e.goal,disableCache:o});return wo.parse(r)}async getReverseMappedDescription(e,o){let r=await this.sendRequest(`/${ue}/web-agent/reverse-mapped-description`,{goal:e.goal,browserState:e.browserState,disableCache:o});return bo.parse(r)}async sendRequest(e,o){let r=await Hs(`${this.baseURL}${e}`,{retries:1,retryDelay:1e3,method:"POST",body:JSON.stringify(o),headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`}});if(!r.ok)throw new Error(`Request to ${e} failed with status ${r.status}: ${await r.text()}`);return r.json()}};import{existsSync as Bs,readFileSync as Gs,writeFileSync as Ut}from"fs";import Ft from"path";import{parse as js,stringify as Vs}from"yaml";var _=process.env.MOMENTIC_DIR??ie;function wr(t,e){let o=Oo(t);for(let[s,i]of Object.entries(o.modules)){let a=Me(s);Ut(Ft.join(_,V,`${a}.yaml`),i,"utf-8")}let r=Me(e),n=Ft.join(_,`${r}.yaml`);return Ut(n,o.test,"utf-8"),`${r}.yaml`}function br(t,e){if(!Bs(e))throw new Error(`Test not found at path: ${Ft}`);let o=Gs(e,"utf-8"),n={...js(o),...t},s=Vs(n);Ut(e,s,"utf-8")}import{readFileSync as ct,readdirSync as Ar,writeFileSync as Er}from"fs";import{join as $t}from"path";import{v4 as Ws}from"uuid";import{parse as dt,stringify as vr}from"yaml";var ke=$t(_,V);function Cr(t,e){let o=rt(ke,e).path,r=ct(o,"utf-8"),s={...dt(r),...t},i=vr(s);Er(o,i,"utf-8")}function Tr(t,e){let o=Me(t),r=$t(ke,`${o}.yaml`),n={fileType:"momentic/module",schemaVersion:z,moduleId:Ws(),name:t,steps:e},s=vr(n);return Er(r,s,"utf-8"),{type:"RESOLVED_MODULE",moduleId:n.moduleId,name:n.name,steps:n.steps}}function Rr(){let t=[];for(let e in Ar(ke)){if(!e.endsWith(".yaml"))continue;let o=ct(e,"utf-8"),r=dt(o),n={name:r.name,moduleId:r.moduleId,numSteps:r.steps.length};t.push(n)}return t}function xr(){let t=[];for(let e of Ar(ke)){if(!e.endsWith(".yaml"))continue;let o=$t(ke,e),r=ct(o,"utf-8"),n=dt(r);try{let s=Tt.parse(n);t.push(s)}catch(s){y.warn({err:s},"Error parsing module, skipping...")}}return t}function Ir(t){let e=rt(V,t).path,o=ct(e,"utf-8"),r=dt(o);return Tt.parse(r)}import{Router as Ks}from"express";function G(t){return function(...e){let o=e[e.length-1],r=t(...e);Promise.resolve(r).catch(o)}}var ze=Ks();ze.get("/",G((t,e)=>{let r=xr().map(n=>({...n,type:"RESOLVED_MODULE"}));e.status(200).json(r)}));ze.get("/metadata",G((t,e)=>{let o=Rr();e.status(200).json(o)}));ze.post("/",G((t,e)=>{let o;try{o=fo.parse(t.body)}catch(n){e.status(400).json({error:`Invalid request body: ${n}`});return}try{De(o.name)}catch(n){e.status(400).json({error:`Invalid module name: ${n}`});return}let r=Tr(o.name,o.steps);e.status(201).json(r)}));ze.get("/:moduleId",G((t,e)=>{if(!t.params.moduleId){e.status(400).json({error:"Missing moduleId in url path."});return}let o=Ir(t.params.moduleId);e.json(o)}));var Lr=ze;import{Router as qs}from"express";import{existsSync as Ys}from"fs";import{join as Ht}from"path";import{v4 as Xs}from"uuid";var Ue=qs();Ue.get("/",G((t,e)=>{let o=Ne(_).filter(r=>r.localOnly);e.status(200).json(o)}));Ue.post("/",G((t,e)=>{let o;try{o=ho.parse(t.body)}catch(a){e.status(400).json({error:`Invalid request body: ${a}`});return}try{De(o.name)}catch(a){e.status(400).json({error:a.message});return}let n={id:Xs(),name:o.name,baseUrl:o.baseUrl,schemaVersion:z,advanced:{disableAICaching:!1,availableAsModule:!0},retries:0,steps:[],localOnly:!0},s=wr(n,o.name),i={...n,testPath:s};e.status(201).json(i)}));Ue.get("/:testPath",G(async(t,e)=>{if(!t.params.testPath){e.status(400).json({error:"Missing testPath in url path."});return}let o=Ht(_,t.params.testPath);if(!Ys(o)){e.status(400).json({error:`Test not found at path: ${o}`});return}let r=await nt(o,Ht(_,V));e.status(200).json(r)}));Ue.patch("/:testPath",G((t,e)=>{if(!t.params.testPath){e.status(400).json({error:"Missing testPath in url path."});return}let o;try{o=po.parse(t.body)}catch(s){e.status(400).json({error:`Invalid request body: ${s}`});return}let r=[],n={};for(let s of o.steps){if(s.type!=="RESOLVED_MODULE"){r.push(s);continue}r.push({type:"MODULE",moduleId:s.moduleId}),n[s.moduleId]=s.steps}for(let[s,i]of Object.entries(n))Cr({steps:i},s);br({steps:r},Ht(_,t.params.testPath)),e.status(201).json({message:"ok"})}));var Or=Ue;var mt=process.env.MOMENTIC_SERVER??"https://api.momentic.ai";var Js,Dr=t=>{Js=t};var Mr,Nr=async t=>{if(Mr)return;let e=await fetch(`${mt}/v1/auth/check`,{headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}});if(!e.ok)throw new Error(`Error checking API key with server (code ${e.status}): ${await e.text()}`);let o=await e.json(),{orgId:r}=Io.parse(o);Mr=r};async function Pr(t,e,o,r){if(!ei(_)||!ti(_).isDirectory()){let c=ri.isAbsolute(_);throw new Error(`Root folder ${_} does not exist${c?"":` in the current directory ("${process.cwd()}")`}. Please run \`npx momentic init\` if you wish to setup Momentic here for the first time.`)}y.info("Checking API key"),await Nr(t);let s=`http://localhost:${e}`;o&&(s=`http://localhost:${o}`);let i=ni(r);await new Promise(c=>{i.listen(e,()=>{y.info(`Server is running at http://localhost:${e}`),c()})});let[l,d]=await si(s,t,()=>{y.info("Browser closed, closing app."),i.close(),process.exit(0)});await ii(i,l,d)}function ni(t){let e=Bt();e.use(Zs()),e.use(Qs.json({limit:"50mb"}));let o=Bt.Router();if(o.use("/tests",Or),o.use("/modules",Lr),e.use("/api",o),e.use((n,s,i)=>{n.path!=="/healthcheck"&&y.debug({url:n.url,path:n.path,query:n.query,method:n.method,body:n.body,headers:n.rawHeaders,client:n.ip},"Received desktop-server request"),s.on("close",()=>{s.statusCode>=400&&y.error({url:n.url,method:n.method,statusCode:s.statusCode},"Request completed in error")}),i()}),e.use((n,s,i,a)=>{y.error({stack:n.stack,msg:n.message,url:s.url,method:s.method},"Unhandled exception leading to 500 on desktop-server"),i.status(500).send("Internal Server Error"),a(n)}),t){let n=Bt.static(t);e.use("/",n),e.use("*",n)}return oi.createServer(e)}async function si(t,e,o){let r=await x.init({baseUrl:t,logger:y,browserArgs:{headless:!1,handleSIGTERM:!0},contextArgs:{bypassCSP:!0,viewport:{width:1440,height:900}},localMode:!0,localAppUrl:t,onClose:o}),n=new pe({baseURL:mt,apiKey:e}),s=new Y({browser:r,config:me,generator:n,logger:y});return Dr(s),[s,n]}async function ii(t,e,o){Sr({baseServer:t,generator:o,logger:y,emitScreenshots:!1,rootController:e})}import{existsSync as Ni}from"fs";import Pi,{dirname as _i}from"path";import{fileURLToPath as ki}from"url";var _r="0.0.21";var X="v1",Ee=class{baseURL;apiKey;constructor(e){this.baseURL=e.baseURL,this.apiKey=e.apiKey}async getRun(e){let o=await this.sendRequest(`/${X}/runs/${e}`,{method:"GET"});return Ro.parse(o)}async createRun(e){let o=await this.sendRequest(`/${X}/runs`,{method:"POST",body:e});return To.parse(o)}async updateRun(e,o){await this.sendRequest(`/${X}/runs/${e}`,{method:"PATCH",body:o})}async getTest(e){let o=await this.sendRequest(`/${X}/tests/${e}`,{method:"GET"});return Eo.parse(o)}async getAllTestIds(){let e=await this.sendRequest(`/${X}/tests`,{method:"GET"});return vo.parse(e)}async getTestYAMLExport(e){let o=await this.sendRequest(`/${X}/tests/export`,{method:"POST",body:e});return Co.parse(o)}async updateTestWithYAML(e){await this.sendRequest(`/${X}/tests/update`,{method:"POST",body:e})}async queueTests(e){let o=await this.sendRequest(`/${X}/tests/queue`,{method:"POST",body:e});return Ao.parse(o)}async uploadScreenshot(e){let o=await this.sendRequest(`/${X}/screenshots`,{method:"POST",body:e});return xo.parse(o)}async sendRequest(e,o){let r=await fetch(`${this.baseURL}${e}`,{method:o.method,body:o.body?JSON.stringify(o.body):void 0,headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`}});if(!r.ok)throw new Error(`Request to ${e} failed with status ${r.status}: ${await r.text()}`);return r.status===204?r.text():r.json()}};import{existsSync as di,mkdirSync as Gt,statSync as mi}from"fs";import{join as kr}from"path";import li from"chalk";import ci from"readline/promises";async function ve(t,e){if(process.env.CI)return!0;let o=ci.createInterface({input:process.stdin,output:process.stdout});t=`${t} (y/N) `;let r=e?li.bold.yellow(t):t,n=await o.question(r);return o.close(),n.toLowerCase()==="y"}var ut=ie,he=kr(ie,V),jt=kr(ie,uo);function ui(t){return di(t)&&mi(t).isDirectory()}async function Fe(t=!1){ui(ut)||(!t&&!await ve(`A '${ie}' folder was not found in the current directory. Setup the required Momentic folder structure?`)&&(y.error("Setup cancelled"),process.exit(1)),Gt(ut),Gt(he),Gt(jt),y.info("Setup complete!"))}import{registry as Vt}from"playwright-core/lib/server";function pi(t){let e=[],o=[];for(let r of t){let n=Vt.findExecutable(r);!n||n.installType==="none"?e.push(r):o.push(n)}return o}async function zr(){let t=pi(["chromium"]);await Vt.installDeps(t,!1),await Vt.install(t,!1)}import{Argument as Wt,Option as $e}from"commander";var He=new $e("--api-key <key>","API key for authentication. If not supplied, attempts to read the MOMENTIC_API_KEY env var.").env("MOMENTIC_API_KEY").makeOptionMandatory(!0),pt=new $e("--server <server>","Momentic server to use. Leave unchanged unless using Momentic on-premise.").default("https://api.momentic.ai"),Be=new $e("-y, --yes","Skip confirmation prompts.").env("CI").default(!1),Ur=new $e("--no-report","Skip reporting test results to Momentic Cloud when running with the --local flag.").default(!0),Kt=new $e("-a --all","Select all tests in your organization from Momentic Cloud. Cannot be used together with <tests> arguments.").default(!1).preset(!0),Fr=new Wt("<tests...>",`One or more test paths to pull from Momentic Cloud.
25
+ `)}`}async executeCommand(e,o,r=!1){let n=this.commandHistory[this.commandHistory.length-1];if(!r&&(!n||n.state!=="PENDING"))throw new R("InternalWebAgentError","Executing command but there is no pending entry in the history");if(this.closed)throw new Error("Attempting to execute command on a closed controller");let s;try{let i=Date.now();s=await this.executePresetStep(e,o);let a=Date.now()-i;this.logger.debug({result:s,duration:a},"Got execution result")}catch(i){throw i instanceof Error?new Re(`Failed to execute command: ${i}`,{cause:i}):new Re("Unexpected throw from executing command",{cause:new Error(`${i}`)})}return s.succeedImmediately&&!r&&(this.pendingInstructions.pop(),this.pendingInstructions.length>0&&(s.succeedImmediately=!1)),s.elementInteracted&&"target"in e&&e.target&&!e.target.elementDescriptor&&(e.target.elementDescriptor=s.elementInteracted.trim()),r||(n.generatedStep=e,n.serializedCommand=Se(e),n.state="DONE"),s}async executeAssertion(e){let o=await this.getBrowserState(),r=await this.browser.url(),n;if(e.useVision)n={goal:e.assertion,url:r,screenshot:await this.browser.screenshot(),browserState:"",history:"",numPrevious:-1,lastCommand:null};else{let i=this.getSerializedHistory(r,o);n={goal:e.assertion,url:r,browserState:o,history:i,lastCommand:this.lastExecutedCommand,numPrevious:this.commandHistory.length}}let s=await this.generator.getAssertionResult(n,e.useVision,e.disableCache);if(s.relevantElements&&Promise.all(s.relevantElements.map(i=>this.browser.highlight({id:i}))),!s.result)throw new R("AssertionFailureError",s.thoughts);return{succeedImmediately:!1,thoughts:s.thoughts,urlAfterCommand:r}}async wrapElementTargetingCommand(e,o,r,n,s=1){if(!e.a11yData&&!e.elementDescriptor)throw new R("InternalWebAgentError","Cannot target element with no target data or element descriptor");let i=e.a11yData&&je(e.a11yData);e.a11yData||(this.logger.debug("No cached locator data for target, prompting AI for fresh location"),s--,e.a11yData=Ge.parse(await this.locateElement(e.elementDescriptor,o,r)));try{let a=await n(e.a11yData);return i?this.logger.debug({target:e},"Successfully used cached target to perform action"):this.logger.debug({target:e},"Successfully generated and used new a11y target information"),a}catch(a){if(s>0&&e.elementDescriptor)return this.logger.warn({err:a,target:e},"Failed to execute action with cached target, retrying with AI"),e.a11yData=void 0,this.wrapElementTargetingCommand(e,o,r,n,s);if(a instanceof R)throw a;let l=`Failed to find ${e.elementDescriptor?`${e.elementDescriptor}`:"element"}: ${a instanceof Error?a.message:a}`;throw this.logger.error({err:a,target:e},l),new R("ActionFailureError",l,{cause:a})}}async executePresetStep(e,o){try{return await this.executePresetStepHelper(e,o)}catch(r){throw r instanceof R?r:new R("InternalWebAgentError",r instanceof Error?r.message:`${r}`,{cause:r})}}async executePresetStepHelper(e,o){switch(e.type){case"SUCCESS":return e.condition?.assertion.trim()?this.executeAssertion(e.condition):{succeedImmediately:!1,urlAfterCommand:await this.browser.url()};case"AI_ASSERTION":return this.executeAssertion(e);case"NAVIGATE":if(!Ot(e.url)&&!Mt(e.url,this.browser.baseURL))throw new R("ActionFailureError",`Invalid URL: ${e.url}`);await this.browser.navigate({url:e.url});break;case"CAPTCHA":let r=await this.browser.solveCaptcha();r&&(await this.wrapElementTargetingCommand({elementDescriptor:"the captcha image solution input"},e.useVision,o,l=>this.browser.click(l)),await this.browser.type(r,{clearContent:!0,pressKeysSequentially:!1}));break;case"GO_BACK":await this.browser.goBack();break;case"GO_FORWARD":await this.browser.goForward();break;case"SCROLL_DOWN":case"SCROLL_UP":let n;return e.target&&(e.target.elementDescriptor.trim()||e.target.a11yData)&&(n=await this.wrapElementTargetingCommand(e.target,e.useVision,o,l=>this.browser.hover(l))),e.type==="SCROLL_UP"?await this.browser.scrollUp(e.deltaY):await this.browser.scrollDown(e.deltaY),{succeedImmediately:!1,urlAfterCommand:await this.browser.url(),elementInteracted:n};case"WAIT":await this.browser.wait(e.delay*1e3);break;case"REFRESH":await this.browser.refresh();break;case"CLICK":{let l=await this.browser.url(),d=await this.wrapElementTargetingCommand(e.target,e.useVision,o,u=>this.browser.click(u,{doubleClick:e.doubleClick,rightClick:e.rightClick,force:e.force})),c={urlAfterCommand:await this.browser.url(),succeedImmediately:!1,elementInteracted:d};return ce(l,c.urlAfterCommand)&&(c.succeedImmediately=!0,c.succeedImmediatelyReason="URL changed"),c}case"SELECT_OPTION":{let l=await this.wrapElementTargetingCommand(e.target,!1,o,d=>this.browser.selectOption(d,e.option));return{succeedImmediately:!1,urlAfterCommand:await this.browser.url(),elementInteracted:l}}case"TAB":await this.browser.switchToPage(e.url);break;case"COOKIE":if(!e.value)break;await this.browser.setCookie(e.value);break;case"LOCAL_STORAGE":if(!e.value||!e.key)break;await this.browser.setLocalStorage(e.key,e.value);break;case"TYPE":{let l=await this.browser.url(),d=await this.wrapElementTargetingCommand(e.target,e.useVision,o,u=>this.browser.click(u,{force:e.force}));await this.browser.type(e.value,{clearContent:e.clearContent,pressKeysSequentially:e.pressKeysSequentially}),e.pressEnter&&await this.browser.press("Enter");let c={urlAfterCommand:await this.browser.url(),succeedImmediately:!1,elementInteracted:d};return ce(l,c.urlAfterCommand)&&(c.succeedImmediately=!0,c.succeedImmediatelyReason="URL changed"),c}case"HOVER":{let l=await this.wrapElementTargetingCommand(e.target,e.useVision,o,d=>this.browser.hover(d));return{succeedImmediately:!1,urlAfterCommand:await this.browser.url(),elementInteracted:l}}case"PRESS":let s=await this.browser.url();await this.browser.press(e.value);let i={urlAfterCommand:await this.browser.url(),succeedImmediately:!1};return ce(s,i.urlAfterCommand)&&(i.succeedImmediately=!0,i.succeedImmediatelyReason="URL changed"),i;case"REQUEST":{let l=e.timeout??30,d=null,c=new AbortController,u=Object.fromEntries(Object.entries(e.headers||{}).filter(([v,b])=>v&&b)),m=new URLSearchParams;Object.entries(e.params||{}).filter(([v,b])=>v&&b).forEach(([v,b])=>{m.append(v,b)});let p;if(Ot(e.url)&&(p=e.url),Mt(e.url,this.browser.baseURL)&&(p=new URL(e.url,this.browser.baseURL).toString()),!p)throw new R("ActionFailureError",`Invalid URL: ${e.url}`);let f=async()=>{try{d=await fetch(`${p}?${m.toString()}`,{headers:u,method:e.method,body:e.body,signal:c.signal})}catch(v){this.logger.error({err:v},"Failed to make fetch request")}},w=async()=>{await new Promise(v=>setTimeout(v,l*1e3)),c.abort()};await Promise.race([w(),f()]);let g=d;if(!g)throw new R("ActionFailureError",`Fetch request timed out after ${l} seconds`);if(!g.ok)throw new R("ActionFailureError",`Fetch request failed with status ${g.status}`);let N={status:g.status,headers:g.headers};return g.headers.get("content-type")?.includes("json")?N.json=await g.json():N.text=await g.text(),{succeedImmediately:!1,urlAfterCommand:await this.browser.url(),data:N}}default:return(l=>{throw"If Typescript complains about the line below, you missed a case or break in the switch above"})(e)}return{succeedImmediately:!1,urlAfterCommand:await this.browser.url()}}async getReverseMappedTarget(e,o,r){return(await this.generator.getReverseMappedDescription({browserState:e.browserState,goal:`${o}`},r)).phrase}};async function gr({socket:t,generator:e,logger:o,emitScreenshots:r,rootController:n}){let s=o.child({package:"web-agent"}),i=t.handshake.query.testId,a=t.handshake.query.baseUrl;if(!i)throw new Error("Socket connection is missing testID");let l=t.handshake.query.orgId,d=zs(),c=p=>{t.emit("screenshot",{...p,url:a})},u=n;if(u)u.browser.setActiveFrame(Je);else{let p=await x.init({baseUrl:a,logger:s,sendScreenshotsDuringLoad:r?c:void 0});u=new Y({browser:p,generator:e,config:me,logger:s})}r&&Yt(t,d,o),k.registerSession(u,d);let m=x.USER_AGENT;return t.emit("session",{url:a,userAgent:m,viewport:await u.browser.viewport()}),{sessionId:d,testId:i,orgId:l}}var yr=[pr,Xt,fr,hr];var Sr=t=>{let{logger:e}=t,o=new Us(t.baseServer,{cors:{origin:"*",methods:["GET","POST"]}});return o.on("connection",async r=>{let n;try{n=await gr({...t,socket:r})}catch(s){e.error({event:"connection",type:"websocket",err:s},"Failed to setup connection"),r.emit("error",{message:`${s}`});return}yr.forEach(s=>Fs(s,{socket:r,metadata:n,...t}))}),o},Fs=(t,e)=>{let o=t.createHandler(e),r=(...n)=>{e.logger.debug({event:t.event,metadata:e.metadata,args:n},"Websocket event");let s=i=>{e.logger.error({event:t.event,type:"websocket",args:n,err:i instanceof Error?i:new Error(`${i}`)},"Unhandled exception in socket handler"),e.socket.emit("error",{message:`${i}`})};try{let i=o.apply(void 0,n);i&&typeof i.catch=="function"&&i.catch(s)}catch(i){s(i)}};e.socket.on(t.event,r)};import $s from"fetch-retry";var Hs=$s(global.fetch),ue="v1",pe=class{baseURL;apiKey;constructor(e){this.baseURL=e.baseURL,this.apiKey=e.apiKey}async getElementLocation(e,o){let r=await this.sendRequest(`/${ue}/web-agent/locate-element`,{browserState:e.browserState,goal:e.goal,disableCache:o});return Ct.parse(r)}async getElementLocationWithVision(e,o){let r=await this.sendRequest(`/${ue}/web-agent/visual-locate`,{goal:e.goal,screenshot:e.screenshot?.toString("base64"),hintActivatedScreenshot:e.hintActivatedScreenshot?.toString("base64"),disableCache:o});return Ct.parse(r)}async getAssertionResult(e,o,r){if(o){let s=await this.sendRequest(`/${ue}/web-agent/assertion`,{url:e.url,goal:e.goal,screenshot:e.screenshot?.toString("base64"),disableCache:r,vision:!0});return vt.parse(s)}let n=await this.sendRequest(`/${ue}/web-agent/assertion`,{url:e.url,browserState:e.browserState,goal:e.goal,history:e.history,numPrevious:e.numPrevious,lastCommand:e.lastCommand,disableCache:r,vision:!1});return vt.parse(n)}async getProposedCommand(e,o){let r=await this.sendRequest(`/${ue}/web-agent/next-command`,{url:e.url,browserState:e.browserState,goal:e.goal,history:e.history,numPrevious:e.numPrevious,lastCommand:e.lastCommand,disableCache:o});return So.parse(r)}async getGranularGoals(e,o){let r=await this.sendRequest(`/${ue}/web-agent/split-goal`,{url:e.url,goal:e.goal,disableCache:o});return wo.parse(r)}async getReverseMappedDescription(e,o){let r=await this.sendRequest(`/${ue}/web-agent/reverse-mapped-description`,{goal:e.goal,browserState:e.browserState,disableCache:o});return bo.parse(r)}async sendRequest(e,o){let r=await Hs(`${this.baseURL}${e}`,{retries:1,retryDelay:1e3,method:"POST",body:JSON.stringify(o),headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`}});if(!r.ok)throw new Error(`Request to ${e} failed with status ${r.status}: ${await r.text()}`);return r.json()}};import{existsSync as Bs,readFileSync as Gs,writeFileSync as Ut}from"fs";import Ft from"path";import{parse as js,stringify as Vs}from"yaml";var _=process.env.MOMENTIC_DIR??ie;function wr(t,e){let o=Oo(t);for(let[s,i]of Object.entries(o.modules)){let a=Me(s);Ut(Ft.join(_,V,`${a}.yaml`),i,"utf-8")}let r=Me(e),n=Ft.join(_,`${r}.yaml`);return Ut(n,o.test,"utf-8"),`${r}.yaml`}function br(t,e){if(!Bs(e))throw new Error(`Test not found at path: ${Ft}`);let o=Gs(e,"utf-8"),n={...js(o),...t},s=Vs(n);Ut(e,s,"utf-8")}import{readFileSync as ct,readdirSync as Ar,writeFileSync as Er}from"fs";import{join as $t}from"path";import{v4 as Ws}from"uuid";import{parse as dt,stringify as vr}from"yaml";var ke=$t(_,V);function Cr(t,e){let o=rt(ke,e).path,r=ct(o,"utf-8"),s={...dt(r),...t},i=vr(s);Er(o,i,"utf-8")}function Tr(t,e){let o=Me(t),r=$t(ke,`${o}.yaml`),n={fileType:"momentic/module",schemaVersion:z,moduleId:Ws(),name:t,steps:e},s=vr(n);return Er(r,s,"utf-8"),{type:"RESOLVED_MODULE",moduleId:n.moduleId,name:n.name,steps:n.steps}}function Rr(){let t=[];for(let e in Ar(ke)){if(!e.endsWith(".yaml"))continue;let o=ct(e,"utf-8"),r=dt(o),n={name:r.name,moduleId:r.moduleId,numSteps:r.steps.length};t.push(n)}return t}function xr(){let t=[];for(let e of Ar(ke)){if(!e.endsWith(".yaml"))continue;let o=$t(ke,e),r=ct(o,"utf-8"),n=dt(r);try{let s=Tt.parse(n);t.push(s)}catch(s){y.warn({err:s},"Error parsing module, skipping...")}}return t}function Ir(t){let e=rt(V,t).path,o=ct(e,"utf-8"),r=dt(o);return Tt.parse(r)}import{Router as Ks}from"express";function G(t){return function(...e){let o=e[e.length-1],r=t(...e);Promise.resolve(r).catch(o)}}var ze=Ks();ze.get("/",G((t,e)=>{let r=xr().map(n=>({...n,type:"RESOLVED_MODULE"}));e.status(200).json(r)}));ze.get("/metadata",G((t,e)=>{let o=Rr();e.status(200).json(o)}));ze.post("/",G((t,e)=>{let o;try{o=fo.parse(t.body)}catch(n){e.status(400).json({error:`Invalid request body: ${n}`});return}try{De(o.name)}catch(n){e.status(400).json({error:`Invalid module name: ${n}`});return}let r=Tr(o.name,o.steps);e.status(201).json(r)}));ze.get("/:moduleId",G((t,e)=>{if(!t.params.moduleId){e.status(400).json({error:"Missing moduleId in url path."});return}let o=Ir(t.params.moduleId);e.json(o)}));var Lr=ze;import{Router as qs}from"express";import{existsSync as Ys}from"fs";import{join as Ht}from"path";import{v4 as Xs}from"uuid";var Ue=qs();Ue.get("/",G((t,e)=>{let o=Ne(_).filter(r=>r.localOnly);e.status(200).json(o)}));Ue.post("/",G((t,e)=>{let o;try{o=ho.parse(t.body)}catch(a){e.status(400).json({error:`Invalid request body: ${a}`});return}try{De(o.name)}catch(a){e.status(400).json({error:a.message});return}let n={id:Xs(),name:o.name,baseUrl:o.baseUrl,schemaVersion:z,advanced:{disableAICaching:!1,availableAsModule:!0},retries:0,steps:[],localOnly:!0},s=wr(n,o.name),i={...n,testPath:s};e.status(201).json(i)}));Ue.get("/:testPath",G(async(t,e)=>{if(!t.params.testPath){e.status(400).json({error:"Missing testPath in url path."});return}let o=Ht(_,t.params.testPath);if(!Ys(o)){e.status(400).json({error:`Test not found at path: ${o}`});return}let r=await nt(o,Ht(_,V));e.status(200).json(r)}));Ue.patch("/:testPath",G((t,e)=>{if(!t.params.testPath){e.status(400).json({error:"Missing testPath in url path."});return}let o;try{o=po.parse(t.body)}catch(s){e.status(400).json({error:`Invalid request body: ${s}`});return}let r=[],n={};for(let s of o.steps){if(s.type!=="RESOLVED_MODULE"){r.push(s);continue}r.push({type:"MODULE",moduleId:s.moduleId}),n[s.moduleId]=s.steps}for(let[s,i]of Object.entries(n))Cr({steps:i},s);br({steps:r},Ht(_,t.params.testPath)),e.status(201).json({message:"ok"})}));var Or=Ue;var mt=process.env.MOMENTIC_SERVER??"https://api.momentic.ai";var Js,Dr=t=>{Js=t};var Mr,Nr=async t=>{if(Mr)return;let e=await fetch(`${mt}/v1/auth/check`,{headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`}});if(!e.ok)throw new Error(`Error checking API key with server (code ${e.status}): ${await e.text()}`);let o=await e.json(),{orgId:r}=Io.parse(o);Mr=r};async function Pr(t,e,o,r){if(!ei(_)||!ti(_).isDirectory()){let c=ri.isAbsolute(_);throw new Error(`Root folder ${_} does not exist${c?"":` in the current directory ("${process.cwd()}")`}. Please run \`npx momentic init\` if you wish to setup Momentic here for the first time.`)}y.info("Checking API key"),await Nr(t);let s=`http://localhost:${e}`;o&&(s=`http://localhost:${o}`);let i=ni(r);await new Promise(c=>{i.listen(e,()=>{y.info(`Server is running at http://localhost:${e}`),c()})});let[l,d]=await si(s,t,()=>{y.info("Browser closed, closing app."),i.close(),process.exit(0)});await ii(i,l,d)}function ni(t){let e=Bt();e.use(Zs()),e.use(Qs.json({limit:"50mb"}));let o=Bt.Router();if(o.use("/tests",Or),o.use("/modules",Lr),e.use("/api",o),e.use((n,s,i)=>{n.path!=="/healthcheck"&&y.debug({url:n.url,path:n.path,query:n.query,method:n.method,body:n.body,headers:n.rawHeaders,client:n.ip},"Received desktop-server request"),s.on("close",()=>{s.statusCode>=400&&y.error({url:n.url,method:n.method,statusCode:s.statusCode},"Request completed in error")}),i()}),e.use((n,s,i,a)=>{y.error({stack:n.stack,msg:n.message,url:s.url,method:s.method},"Unhandled exception leading to 500 on desktop-server"),i.status(500).send("Internal Server Error"),a(n)}),t){let n=Bt.static(t);e.use("/",n),e.use("*",n)}return oi.createServer(e)}async function si(t,e,o){let r=await x.init({baseUrl:t,logger:y,browserArgs:{headless:!1,handleSIGTERM:!0},contextArgs:{bypassCSP:!0,viewport:null,deviceScaleFactor:void 0},localMode:!0,localAppUrl:t,onClose:o}),n=new pe({baseURL:mt,apiKey:e}),s=new Y({browser:r,config:me,generator:n,logger:y});return Dr(s),[s,n]}async function ii(t,e,o){Sr({baseServer:t,generator:o,logger:y,emitScreenshots:!1,rootController:e})}import{existsSync as Ni}from"fs";import Pi,{dirname as _i}from"path";import{fileURLToPath as ki}from"url";var _r="0.0.22";var X="v1",Ee=class{baseURL;apiKey;constructor(e){this.baseURL=e.baseURL,this.apiKey=e.apiKey}async getRun(e){let o=await this.sendRequest(`/${X}/runs/${e}`,{method:"GET"});return Ro.parse(o)}async createRun(e){let o=await this.sendRequest(`/${X}/runs`,{method:"POST",body:e});return To.parse(o)}async updateRun(e,o){await this.sendRequest(`/${X}/runs/${e}`,{method:"PATCH",body:o})}async getTest(e){let o=await this.sendRequest(`/${X}/tests/${e}`,{method:"GET"});return Eo.parse(o)}async getAllTestIds(){let e=await this.sendRequest(`/${X}/tests`,{method:"GET"});return vo.parse(e)}async getTestYAMLExport(e){let o=await this.sendRequest(`/${X}/tests/export`,{method:"POST",body:e});return Co.parse(o)}async updateTestWithYAML(e){await this.sendRequest(`/${X}/tests/update`,{method:"POST",body:e})}async queueTests(e){let o=await this.sendRequest(`/${X}/tests/queue`,{method:"POST",body:e});return Ao.parse(o)}async uploadScreenshot(e){let o=await this.sendRequest(`/${X}/screenshots`,{method:"POST",body:e});return xo.parse(o)}async sendRequest(e,o){let r=await fetch(`${this.baseURL}${e}`,{method:o.method,body:o.body?JSON.stringify(o.body):void 0,headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`}});if(!r.ok)throw new Error(`Request to ${e} failed with status ${r.status}: ${await r.text()}`);return r.status===204?r.text():r.json()}};import{existsSync as di,mkdirSync as Gt,statSync as mi}from"fs";import{join as kr}from"path";import li from"chalk";import ci from"readline/promises";async function ve(t,e){if(process.env.CI)return!0;let o=ci.createInterface({input:process.stdin,output:process.stdout});t=`${t} (y/N) `;let r=e?li.bold.yellow(t):t,n=await o.question(r);return o.close(),n.toLowerCase()==="y"}var ut=ie,he=kr(ie,V),jt=kr(ie,uo);function ui(t){return di(t)&&mi(t).isDirectory()}async function Fe(t=!1){ui(ut)||(!t&&!await ve(`A '${ie}' folder was not found in the current directory. Setup the required Momentic folder structure?`)&&(y.error("Setup cancelled"),process.exit(1)),Gt(ut),Gt(he),Gt(jt),y.info("Setup complete!"))}import{registry as Vt}from"playwright-core/lib/server";function pi(t){let e=[],o=[];for(let r of t){let n=Vt.findExecutable(r);!n||n.installType==="none"?e.push(r):o.push(n)}return o}async function zr(){let t=pi(["chromium"]);await Vt.installDeps(t,!1),await Vt.install(t,!1)}import{Argument as Wt,Option as $e}from"commander";var He=new $e("--api-key <key>","API key for authentication. If not supplied, attempts to read the MOMENTIC_API_KEY env var.").env("MOMENTIC_API_KEY").makeOptionMandatory(!0),pt=new $e("--server <server>","Momentic server to use. Leave unchanged unless using Momentic on-premise.").default("https://api.momentic.ai"),Be=new $e("-y, --yes","Skip confirmation prompts.").env("CI").default(!1),Ur=new $e("--no-report","Skip reporting test results to Momentic Cloud when running with the --local flag.").default(!0),Kt=new $e("-a --all","Select all tests in your organization from Momentic Cloud. Cannot be used together with <tests> arguments.").default(!1).preset(!0),Fr=new Wt("<tests...>",`One or more test paths to pull from Momentic Cloud.
26
26
 
27
27
  A test path is a lowercased version of your test name where spaces are replaced with underscores: 'npx momentic pull hello-world'.`).argOptional(),$r=new Wt("<tests...>",`One or more test identifiers.
28
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "momentic",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "The Momentic SDK for Node.js",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",