@mirasim/computer-use 0.1.0-rc.4 → 0.1.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/platform/browser/driver-tabs.js +1 -1
- package/dist/platform/browser/driver.d.ts +1 -0
- package/dist/platform/browser/driver.js +2 -2
- package/dist/platform/driver-narrate.js +3 -3
- package/dist/platform/ownership.d.ts +3 -0
- package/dist/platform/ownership.js +1 -1
- package/dist/platform/tools/index.d.ts +1 -0
- package/dist/platform/tools/index.js +1 -1
- package/dist/platform/tools/repl-docs.js +4 -4
- package/dist/platform/tools/repl-state.d.ts +1 -0
- package/dist/platform/tools/repl.d.ts +1 -0
- package/dist/platform/tools/repl.js +17 -17
- package/dist/platform/types.d.ts +1 -0
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
import"playwright-core";import{userWantsVisible as
|
|
1
|
+
import"playwright-core";import{userWantsVisible as w}from"./acquire.js";import{pageContextId as f,pageTargetInfo as h}from"./tabs.js";import{msgOwnWindowFailed as l,NO_TAB_OPEN_NOTE as m,NO_TAB_YET_NOTE as g}from"./messages.js";import{SAVE_PICKER_JS as d}from"./save-picker.js";import{SurfaceUnreadableError as c}from"../types.js";import{ADOPTED_WHY as r,APPEARED_WHY as u,BrowserDriverCore as I,REIDENTIFIED_WHY as v}from"./driver-core.js";const b=o=>new Promise(t=>setTimeout(t,o));class D extends I{claim(t){this.mine.add(t),this.rememberIdentity(t),this.armSavePicker(t)}armSavePicker(t){this.savePickerPages.has(t)||(this.savePickerPages.add(t),t.addInitScript(d).catch(()=>{}),t.evaluate(d).catch(()=>{}))}rememberIdentity(t){h(t).then(({targetId:e})=>{e&&(this.ownedTargetIds.add(e),this.active===t&&(this.activeTargetId=e))}).catch(()=>{})}async recognise(t){const{targetId:e}=await h(t).catch(()=>({targetId:""}));return!e||!this.ownedTargetIds.has(e)?null:(this.mine.add(t),this.armSavePicker(t),e===this.activeTargetId?"same":"ours")}async ownsIsolatedPage(t){let e=this.pageCtxIds.get(t);return e===void 0&&(e=await f(t).catch(()=>""),this.pageCtxIds.set(t,e)),e!==""&&e===this.ctxId}adoptWhenReal(t){const e=()=>{if(this.tab?.isolated){this.ownsIsolatedPage(t).then(s=>{s&&(this.claim(t),this.makeActive(t,r))}).catch(()=>{});return}if(this.tab){t.opener().then(s=>{s&&this.mine.has(s)&&(this.claim(t),this.makeActive(t,r))}).catch(()=>{});return}if(!this.attached){if(this.mine.has(t)){this.makeActive(t,r);return}const s=()=>{this.managedProfileDir&&this.claim(t),this.makeActive(t,u)};this.recognise(t).then(n=>n?this.makeActive(t,n==="same"?v:r):s()).catch(s);return}this.ownsPage(t).then(s=>{s&&(this.claim(t),this.makeActive(t,r))}).catch(()=>{})};if(t.url()!=="about:blank"){e();return}const i=s=>{s!==t.mainFrame()||t.isClosed()||t.url()==="about:blank"||(clearTimeout(a),t.off("framenavigated",i),e())},a=setTimeout(()=>t.off("framenavigated",i),1500);a.unref?.(),t.on("framenavigated",i)}page(){if(this.closedItsLastTab)throw new c(m);if(!this.active)throw new c(g);if(this.active.isClosed()){const t=this.context.pages().filter(i=>!i.isClosed()&&(this.tab?this.tab.isolated?this.pageCtxIds.get(i)===this.ctxId:this.mine.has(i):!this.attached||this.ownWindowId===null||this.windowIds.get(i)===this.ownWindowId)),e=t.filter(i=>i.url()!=="about:blank");this.makeActive(e[e.length-1]??t[t.length-1]??this.active,"the tab it was on was closed")}return this.active}async openOwnWindow(){const t=new Set(this.context.pages()),e=await this.browser.newBrowserCDPSession();try{await e.send("Target.createTarget",{url:"about:blank",newWindow:!0,background:!0})}finally{await e.detach().catch(()=>{})}let i;for(let s=0;s<40&&(i=this.context.pages().find(n=>!t.has(n)&&!n.isClosed()),!i);s++)await b(25);if(!i)throw new Error(l());const a=await this.context.newCDPSession(i);try{const{windowId:s}=await a.send("Browser.getWindowForTarget");this.ownWindowId=s,this.windowIds.set(i,s),this.cfg.window==="offscreen"&&!(this.managedProfileDir&&w(this.managedProfileDir))&&await a.send("Browser.setWindowBounds",{windowId:s,bounds:{left:-4e3,top:200}})}catch{}finally{await a.detach().catch(()=>{})}return i}async ownsPage(t){if(!this.attached||this.ownWindowId===null)return!0;let e=this.windowIds.get(t);if(e===void 0)try{const i=await this.context.newCDPSession(t);try{e=(await i.send("Browser.getWindowForTarget")).windowId}finally{await i.detach().catch(()=>{})}this.windowIds.set(t,e)}catch{return!1}return e===this.ownWindowId}async ownPages(){const t=this.context.pages().filter(i=>!i.isClosed());if(this.tab?.isolated){const i=[];for(const a of t)await this.ownsIsolatedPage(a)&&i.push(a);return i}if(this.tab)return t.filter(i=>this.mine.has(i));if(!this.managedProfileDir)return t.filter(i=>this.mine.has(i));if(!this.attached||this.ownWindowId===null)return t;const e=[];for(const i of t)await this.ownsPage(i)&&e.push(i);return e}async visiblePages(){return this.managedProfileDir||this.attached||this.tab?this.ownPages():this.context.pages().filter(t=>!t.isClosed())}tabId(t){let e=this.tabIds.get(t);return e||(e=`t${this.nextTabId++}`,this.tabIds.set(t,e)),e}}export{D as BrowserDriverTabs};
|
|
@@ -26,6 +26,7 @@ export declare class BrowserDriver extends BrowserDriverVerbs {
|
|
|
26
26
|
private noTabNote;
|
|
27
27
|
private noTabObservation;
|
|
28
28
|
protected staleRefCause(): string;
|
|
29
|
+
explainMiss(query: string): string | null;
|
|
29
30
|
promptFragment(): string;
|
|
30
31
|
listTargets(): Promise<TargetInfo[]>;
|
|
31
32
|
protected noteBridgeSession(session: string | null): boolean;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{Type as i}from"typebox";import{SurfaceUnreadableError as d,text as h}from"../types.js";import{NO_TAB_OPEN_NOTE as c,NO_TAB_YET_NOTE as u}from"./messages.js";import{PASTE_VERB as b,SELECT_OPTION_VERB as p}from"../chromium/actions-value.js";import{SELECT_TEXT_VERB as w}from"../chromium/select-text.js";import{DIALOG_VERB as f,DRAG_VERB as g,UPLOAD_VERB as m}from"../chromium/actions.js";import{RUN_JS_CODE_DESC as v}from"../chromium/run-js.js";import{BrowserDriverVerbs as y}from"./driver-verbs.js";import{previewPage as k}from"../chromium/preview.js";import{withPageCapture as S}from"../chromium/shot.js";import{DOWNLOAD_VERB as T}from"./downloads.js";import{bridgeRequest as O,bridgeTabs as E,restoreBridgeTab as _}from"./bridge-tabs.js";const
|
|
1
|
+
import{Type as i}from"typebox";import{SurfaceUnreadableError as d,text as h}from"../types.js";import{NO_TAB_OPEN_NOTE as c,NO_TAB_YET_NOTE as u}from"./messages.js";import{PASTE_VERB as b,SELECT_OPTION_VERB as p}from"../chromium/actions-value.js";import{SELECT_TEXT_VERB as w}from"../chromium/select-text.js";import{DIALOG_VERB as f,DRAG_VERB as g,UPLOAD_VERB as m}from"../chromium/actions.js";import{RUN_JS_CODE_DESC as v}from"../chromium/run-js.js";import{BrowserDriverVerbs as y}from"./driver-verbs.js";import{previewPage as k}from"../chromium/preview.js";import{withPageCapture as S}from"../chromium/shot.js";import{DOWNLOAD_VERB as T}from"./downloads.js";import{bridgeRequest as O,bridgeTabs as E,restoreBridgeTab as _}from"./bridge-tabs.js";const R=5e3,N=1500,P=/^(?:[a-z][a-z\d+.-]*:\/\/|(?:localhost|\d{1,3}(?:\.\d{1,3}){3}|\[[\da-f:]+\])(?::\d+)?(?:[/?#]|$)|[\w.-]+:\d+(?:[/?#]|$))/i;async function L(l){if(!l.url())return"";let e;try{return await Promise.race([l.title().catch(()=>""),new Promise(t=>{e=setTimeout(()=>t(""),N),e.unref?.()})])}finally{e&&clearTimeout(e)}}class H extends y{unloadedSelection=null;async activeTarget(){return this.unloadedSelection?.id??super.activeTarget()}async ownershipResources(){return this.unloadedSelection?[{key:`chromium:${this.unloadedSelection.targetId}`,mode:"exclusive"}]:super.ownershipResources()}async prepareTarget(){const e=this.unloadedSelection;if(!e)return;const{page:t,viewport:s}=await _(this.browser,e.tabId,e.targetId,e.browserSession);this.tabIds.set(t,e.id),this.active=t,this.unloadedSelection=null,this.resetObservationState();const o=await t.evaluate("({ width: innerWidth, height: innerHeight })");if(o.width<=0||o.height<=0){const a=await this.perform("viewport",s??{});if(!a.ok)throw new d(`The restored tab has no usable viewport: ${a.note??"resize failed"}`);this.appendNote("Chrome restored this background tab with a zero-size viewport; applied a temporary viewport, cleared when this driver disconnects.")}this.appendNote("Chrome had unloaded this tab; restored the same tab in the background. Read its current state before continuing.")}page(){if(this.unloadedSelection)throw new d(`Tab ${this.unloadedSelection.id} has not been restored; no other page was observed or operated.`);return super.page()}adoptWhenReal(e){this.unloadedSelection||super.adoptWhenReal(e)}previewTarget(){return this.unloadedSelection||!this.browser?.isConnected()||!this.active||this.active.isClosed()?Promise.resolve(null):k(this.active,this.cdpUrl,"browser")}platform="browser";async turnEnded(e){if(this.attached||e.keepBrowserTabs===!0)return;const t=await this.ownPages().catch(()=>[]);for(const s of t)await s.close().catch(()=>{});t.length&&(this.closedItsLastTab=!0,this.resetObservationState())}extraVerbs(){return[{kind:"open_url",schema:i.Object({kind:i.Literal("open_url"),url:i.String({description:"Full URL to open. Only URLs that appeared on a page or were given in the task \u2014 never guess domains."})})},{kind:"activate",schema:i.Object({kind:i.Literal("activate")},{description:"Bring this browser tab and its window to the foreground. This changes the user's foreground window: explain the effect and obtain permission unless it is already authorized. focus() only selects the target. Embedded writes normally wait for the person to yield the window; activate() is an explicit foreground handoff, revoked by subsequent human input. Do not use it to bypass an input wait without the user handing over control."})},{kind:"back",schema:i.Object({kind:i.Literal("back")})},{kind:"reload",schema:i.Object({kind:i.Literal("reload")})},{kind:"new_tab",schema:i.Object({kind:i.Literal("new_tab"),url:i.Optional(i.String({description:"URL to open in it; omit for a blank tab"}))},{description:"Open a NEW tab of this session's own and make it the target \u2014 the current tab keeps its state, and both are listed as targets afterwards."})},{kind:"close_tab",schema:i.Object({kind:i.Literal("close_tab"),id:i.Optional(i.String({description:"Tab id from computer.surfaces(); omit to close the one you are on"})),all:i.Optional(i.Boolean({description:"Close every tab this session opened or claimed \u2014 the tidy-up at the end of a job. Never the user's own, whatever else is set. Refused together with id."})),confirmed:i.Optional(i.Boolean({description:"Only for a tab marked (user's): you ASKED the user in the conversation and they said yes. Never set it because closing would be convenient \u2014 the first refusal tells you to ask, and asking is the point."}))},{description:"Close a tab: the tabs you open outlive your turn and nothing closes them for you, so follow the tab policy the host states (if it states none, close your own once the job is done), and leave open a page that is waiting for the person (a sign-in, a security-key or 2FA prompt, a form for them to confirm). This session's own always; one of the user's (marked (user's) in the listing) only after you asked them in the conversation and they agreed \u2014 then pass confirmed: true."})},{kind:"viewport",schema:i.Object({kind:i.Literal("viewport"),width:i.Optional(i.Number({description:"Viewport width in CSS pixels (default 1200)"})),height:i.Optional(i.Number({description:"Viewport height in CSS pixels \u2014 this is the one that decides how much of a long list exists (default 1700)"})),reset:i.Optional(i.Boolean({description:"Put the tab back to the real window size"}))},{description:"Give THIS tab a viewport of your own size, so a virtualized list (a feed, a search page, a long transcript) mounts more of itself per read. Set it BEFORE opening the page: rows mount against the viewport in effect at load."})},{kind:"run_js",readOnly:!0,schema:i.Object({kind:i.Literal("run_js"),code:i.String({description:v})},{description:"Ask the PAGE something no observation carries \u2014 read its data in bulk, its stored state, or an API it exposes. This is a stranger's site holding the user's logged-in session: read and compute here, act through the interaction verbs."})},g,f,m,T,p,w,b]}async observeElements(e={}){const t=await this.noTabNote();return t?this.noTabObservation(t,e.prefix):super.observeElements(e)}async observeImage(e={}){const t=await this.noTabNote();return t?this.noTabObservation(t,e.prefix):super.observeImage(e)}async noTabNote(){return this.closedItsLastTab?c:this.unloadedSelection?null:(await this.ensure(),this.active?null:u)}noTabObservation(e,t){const s=this.takePendingNote();return h(`${t?`${t}
|
|
2
2
|
`:""}${s?`NOTE: ${s}.
|
|
3
|
-
`:""}${e}.`,{verdict:"unknown"})}staleRefCause(){return"it predates a navigation, a reload or a tab switch"}promptFragment(){return"Grounding: observations are text lists of interactive elements with refs, for the CURRENT viewport only. Act by ref from the latest observation. When what you want is NOT in the list: if you can name it, w.find('<name>') searches EVERY control on the page and hands back a handle \u2014 act on it directly, the click scrolls it into view, so do not scroll first. Scroll only to discover what you cannot name. Observations may show only changes since the previous one; refs stay valid across observations (until navigation). Use w.text() to read full page text."}async listTargets(){await this.ensure();const{unloaded:e,browserSession:t}=!this.tab&&!this.attached&&!this.managedProfileDir?await E(this.browser):{unloaded:[],browserSession:null};this.noteBridgeSession(t);const s=new Set(await this.ownPages()),o=await this.visiblePages(),a=await Promise.all(o.map(async r=>({id:this.tabId(r),kind:"tab",title:await
|
|
3
|
+
`:""}${e}.`,{verdict:"unknown"})}staleRefCause(){return"it predates a navigation, a reload or a tab switch"}explainMiss(e){const t=e.trim();if(!P.test(t))return null;const s=JSON.stringify(/^[a-z][a-z\d+.-]*:\/\//i.test(t)?t:`http://${t}`);return`mirasim-cu: computer.surface() binds a tab that is already open, by its id or title \u2014 never by URL \u2014 and opens nothing. Bind a tab and go there: const w = await computer.surface('<id>'); await w.openUrl(${s}) \u2014 or await w.newTab(${s}) to leave that tab as it is.`}promptFragment(){return"Grounding: observations are text lists of interactive elements with refs, for the CURRENT viewport only. Act by ref from the latest observation. When what you want is NOT in the list: if you can name it, w.find('<name>') searches EVERY control on the page and hands back a handle \u2014 act on it directly, the click scrolls it into view, so do not scroll first. Scroll only to discover what you cannot name. Observations may show only changes since the previous one; refs stay valid across observations (until navigation). Use w.text() to read full page text."}async listTargets(){await this.ensure();const{unloaded:e,browserSession:t}=!this.tab&&!this.attached&&!this.managedProfileDir?await E(this.browser):{unloaded:[],browserSession:null};this.noteBridgeSession(t);const s=new Set(await this.ownPages()),o=await this.visiblePages(),a=await Promise.all(o.map(async r=>({id:this.tabId(r),kind:"tab",title:await L(r)||r.url(),active:!this.unloadedSelection&&r===this.active,own:s.has(r),mine:this.mine.has(r)})));for(const r of e){const n=`unloaded:${r.tabId}:${r.browserSession}`;a.push({id:n,kind:"tab",title:`${r.title||r.url} [unloaded \u2014 use this handle to restore in background]`,active:this.unloadedSelection?.id===n,own:!1,unloaded:!0})}return a}noteBridgeSession(e){const t=super.noteBridgeSession(e);return t&&(this.unloadedSelection=null),t}async actionResources(e,t){if(e!=="close_tab")return[];const s=typeof t.id=="string"?t.id.replace(/^browser\//,""):null,o=t.all?await this.ownPages():s?(await this.visiblePages()).filter(a=>this.tabId(a)===s):[this.page()];return Promise.all(o.filter(a=>!a.isClosed()).map(a=>this.pageOwnership(a)))}async focusTarget(e){await this.ensure();const t=/^unloaded:(\d+):(.*)$/.exec(e);if(t&&!this.tab&&!this.attached&&!this.managedProfileDir&&this.browser.version()==="mirasim-bridge"){const o=Number(t[1]),a=t[2],{targetId:r}=await O(this.browser,"Mirasim.tabTarget",{tabId:o,browserSession:a});return this.unloadedSelection={id:e,tabId:o,targetId:r,browserSession:a},this.closedItsLastTab=!1,{ok:!0,note:"Selected unloaded tab; restoration follows target ownership."}}let s={ok:!1,note:`no target with id ${e}; call computer.surfaces() for current ids`};for(const o of await this.visiblePages())if(!(o.isClosed()||this.tabId(o)!==e)){this.unloadedSelection=null,this.active=o,this.closedItsLastTab=!1,s={ok:!0};break}return this.journal?.publishAction("focus",{target:e},null,s),s}async close(){this.unloadedSelection=null,await this.downloads.close(),await this.journal?.drain(R);for(const e of this.overridden)e.isClosed()||await S(e,()=>this.cdpFor(e).then(t=>t.send("Emulation.clearDeviceMetricsOverride"))).catch(()=>{});if(this.overridden.clear(),!this.managedProfileDir&&!this.tab&&this.browser)for(const e of await this.ownPages().catch(()=>[]))e.url()==="about:blank"&&await e.close().catch(()=>{});await this.browser?.close().catch(()=>{}),this.browser=null,this.resetObservationState(),this.resetPageState(),await this.disconnected()}journalVerdict(e){this.journal?.publishVerdict(e)}renderHeadLines(e,t,s=!1,o){const a=super.renderHeadLines(e,t,s,o);if(e.url==="about:blank"&&e.elements.length===0&&!e.text){const r=t.some(n=>n.title!=="about:blank");a.push(r?"NOTE: this tab is blank \u2014 your page may be in another tab; computer.surfaces() lists it \u2014 act through its handle.":"NOTE: this tab is blank and nothing has been opened in it \u2014 navigate (openUrl({url})) instead of observing again.")}return a}async blankSurfaceHint(){try{return this.page().url()!=="about:blank"?"":(await this.ownPages()).some(t=>t!==this.page()&&t.url()!=="about:blank")?"the active tab is a blank page \u2014 your page may be in another tab; computer.surfaces() lists it \u2014 act through its handle":""}catch{return""}}}export{H as BrowserDriver};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
const L=2e3,v={target:!0,coverage:!0,omissions:!0,groups:!0,ambiguity:!0};function w(
|
|
2
|
-
|
|
3
|
-
`)),t}}export{
|
|
1
|
+
const L=2e3,v={target:!0,coverage:!0,omissions:!0,groups:!0,ambiguity:!0};function w(a){if(a.length<=1)return null;const e=n=>n.own!==!1&&n.mine===!1,t=a.map(n=>`${n.active?"*":""}${n.id}:${n.title.slice(0,40)}${n.own===!1?"(user's)":e(n)?"(earlier)":""}`).join(" "),s=[...a.some(n=>n.own===!1)?[" (user's) = a tab that was already open \u2014 work in it if the task is about it, but it is not yours to close"]:[],...a.some(e)?[" (earlier) = left open by an earlier session \u2014 not your page; open yours with openUrl rather than reusing it"]:[]];return[t,...s].join(`
|
|
2
|
+
`)}const b=6;class y{observationCoverage(e){return{offscreen:e.meta.offscreen??e.offscreenElements?.length??null,covered:e.covered?.count??e.coveredElements?.length??null,capped:e.meta.capped??null,...e.covered?.modal?{modal:e.covered.modal}:{}}}targetLine(e){const t=e.application;return`Target: ${t?`${t.name} ${t.version}`:"Electron app (identity/version not reported)"} | window ${e.windowId} | CDP ${e.endpoint}`}notListedNote(e,t){const s=[];return e.offscreen&&!t&&s.push(this.offscreenNote(e.offscreen)),e.capped&&s.push("some controls exceed this observation's element cap (narrow your target, or read the page)"),s.length?`NOTE: not in the list below \u2014 ${s.join("; ")}.`:null}offscreenNote(e){return"offscreen controls (find() searches them and actions scroll them into view)"}viewportPosition(e){const t=Math.max(1,e.viewportH),s=Math.max(0,e.docH-t),n=e.scrollY>=s-1,r=Math.max(1,Math.ceil(e.docH/t));return{scrollY:Math.round(e.scrollY),viewportH:Math.round(t),docH:Math.round(e.docH),maxScrollY:Math.round(s),belowFold:Math.round(Math.max(0,s-e.scrollY)),atBottom:n,screen:n?r:Math.min(r,Math.floor(e.scrollY/t)+1),total:r}}renderHeadLines(e,t,s=!1,n=v){const r=[];e.target&&n.target&&r.push(this.targetLine(e.target)),r.push(`Title: ${e.title} | ${e.url}`),w(t)&&r.push(s?`Targets: ${t.length} surfaces, the same list as the last observation \u2014 computer.surfaces() lists them again`:`Targets: ${w(t)} (switch with focus(id), or act through the surface's handle)`);const i=this.viewportPosition(e.meta),p=i.maxScrollY>0;if(p)if(e.meta.docH>i.viewportH*1.5)r.push(`Screen ${i.screen}/${i.total} (elements below are current viewport only${i.atBottom?"":"; scroll to see more"})`);else{const o=i.belowFold>0?`; ${i.belowFold}px below the fold \u2014 scroll to see the rest`:"";r.push(`Partial view (elements below are current viewport only${o})`)}n.coverage&&e.covered?.count&&e.covered.modal?r.push(`Scope: \xAB${e.covered.modal}\xBB dialog; covered background controls are omitted.`):n.coverage&&r.push("Scope: window's visible controls.");const d=this.notListedNote(e.meta,p);if(d&&n.omissions&&r.push(d),e.meta.rowsCut&&n.ambiguity){const o=e.meta.rowsCut===1;r.push(`NOTE: ${e.meta.rowsCut} same-named or unnamed element${o?"":"s"} below ${o?"carries":"carry"} no \u27E8row\u27E9 context (this observation's row budget ran out) \u2014 tell ${o?"it":"them"} apart with page() before acting on ${o?"it":"one"}.`)}const f=new Set(e.elements.map(o=>o.ref)),u=[];let m=0,h=0,$=!1;for(const o of e.groups??[]){const l=o.members.filter(g=>f.has(g));if(l.length){if($||m+o.label.length>2e3){h++,$=!0;continue}m+=o.label.length,u.push({label:o.label,members:l})}}if(u.length&&n.groups){r.push(`GROUPS (${u.length}):`);for(const o of u)r.push(` \u27E8${o.label}\u27E9 ${o.members.join(" ")}`)}if(h&&n.groups){const o=h===1;r.push(`NOTE: ${h} more labelled group${o?"":"s"} ${o?"was":"were"} dropped at this observation's group budget \u2014 page() reads one region's structure in full.`)}const c=e.elements.filter(o=>o.rowBlank).map(o=>o.ref);if(c.length&&n.ambiguity){const o=c.length<=b?c.join(", "):`${c.length} same-named or unnamed elements`,l=c.length===1;r.push(`NOTE: ${o} ${l?"carries":"carry"} no \u27E8row\u27E9 context \u2014 the page has no text that sets ${l?"it":"them"} apart, so a page read will not add any; tell ${l?"it":"them"} apart by position (screenshot()), or by ${l?"its place":"their order"} in the list below.`)}return r}elementLine(e){return`${e.ref}: ${e.role}${e.name?` "${e.name}"`:""}${e.id&&e.id.toLowerCase()!==e.name.trim().toLowerCase()?` \u27E8id: ${e.id}\u27E9`:""}${e.help?` \u27E8help: ${e.help}\u27E9`:""}${e.row?` \u27E8row: ${e.row}\u27E9`:""}${e.state?` [${e.state}]`:""}`}changedElementLine(e,t){return e.role===t.role&&e.name===t.name&&e.id===t.id&&e.help===t.help?`~ ${this.elementLine({...t,state:""})}: [${e.state||"none"}] \u2192 [${t.state||"none"}]`:`~ ${this.elementLine(e)} \u2192 ${this.elementLine(t).slice(t.ref.length+2)}`}coveredBaselineLines(e){return e.coveredElements?.length?e.coveredElements.map(t=>this.elementLine(t)):[]}renderBodyLines(e){const t=e.elements.map(s=>this.elementLine(s));return t.push(...this.coveredBaselineLines(e)),e.text&&t.push(...e.text.split(`
|
|
3
|
+
`)),t}}export{y as ComputerUseDriverNarrate,w as targetsLine};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OwnershipResource } from "./types.ts";
|
|
2
|
+
export declare const IDLE_RELEASE_MS: number;
|
|
2
3
|
export interface OwnershipProvider {
|
|
3
4
|
claim(group: string, resources: OwnershipResource[]): Promise<void>;
|
|
4
5
|
release(group: string): Promise<void>;
|
|
@@ -11,8 +12,10 @@ export declare class OwnershipBusyError extends Error {
|
|
|
11
12
|
export declare class ComputerUseOwnership implements OwnershipProvider {
|
|
12
13
|
private readonly owner;
|
|
13
14
|
private db;
|
|
15
|
+
private started;
|
|
14
16
|
private database;
|
|
15
17
|
private gone;
|
|
18
|
+
private reused;
|
|
16
19
|
claim(group: string, resources: OwnershipResource[]): Promise<void>;
|
|
17
20
|
release(group: string): Promise<void>;
|
|
18
21
|
releaseAll(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as
|
|
1
|
+
import{execFileSync as p}from"node:child_process";import{randomUUID as u}from"node:crypto";import{mkdirSync as d,readFileSync as T}from"node:fs";import{join as a}from"node:path";import{sharedHome as l}from"./home.js";const w=2*6e4;class m extends Error{constructor(r){super(`Resource ${r} is in use by another runtime. Nothing was dispatched. Use a different target, or wait for its owner to finish and release it (an owner with no call in progress lets go within ${w/6e4} minutes); do not reset another session.`),this.name="OwnershipBusyError"}}function E(n){try{return process.kill(n,0),!0}catch(r){return r.code!=="ESRCH"}}function c(n){if(process.platform==="win32")return"";if(process.platform==="linux")try{const r=T(`/proc/${n}/stat`,"utf8");return r.slice(r.lastIndexOf(")")+2).split(" ")[19]??""}catch{}try{return p("ps",["-o","lstart=","-p",String(n)],{encoding:"utf8",timeout:2e3,stdio:["ignore","pipe","ignore"],env:{...process.env,LC_ALL:"C",TZ:"UTC"}}).trim()}catch{return""}}class f{owner=u();db=null;started=null;async database(){if(this.db)return this.db;const r=a(l(),"ownership");d(r,{recursive:!0,mode:448});const{DatabaseSync:o}=await import("node:sqlite"),e=new o(a(r,"claims.sqlite"));e.exec("PRAGMA busy_timeout=1000; CREATE TABLE IF NOT EXISTS claims (resource TEXT NOT NULL, owner TEXT NOT NULL, group_id TEXT NOT NULL, pid INTEGER NOT NULL, mode TEXT NOT NULL, PRIMARY KEY(resource, owner, group_id)); CREATE TABLE IF NOT EXISTS executors (owner TEXT NOT NULL, runtime INTEGER NOT NULL, pid INTEGER NOT NULL, PRIMARY KEY(owner, pid)); CREATE TABLE IF NOT EXISTS runtimes (owner TEXT PRIMARY KEY, pid INTEGER NOT NULL, started TEXT NOT NULL)"),this.started??=c(process.pid),e.prepare("INSERT OR REPLACE INTO runtimes VALUES (?,?,?)").run(this.owner,process.pid,this.started);for(const s of e.prepare("SELECT owner,pid FROM runtimes").all())E(s.pid)||e.prepare("DELETE FROM runtimes WHERE owner=?").run(s.owner);const t=e.prepare("SELECT owner,runtime,pid FROM executors").all();for(const s of t)!E(s.runtime)&&!E(s.pid)&&e.prepare("DELETE FROM executors WHERE owner=? AND pid=?").run(s.owner,s.pid);return this.db=e,e}gone(r,o,e){return E(e)&&!this.reused(r,o,e)?!1:!r.prepare("SELECT pid FROM executors WHERE owner=?").all(o).some(s=>E(s.pid))}reused(r,o,e){const t=r.prepare("SELECT started FROM runtimes WHERE owner=? AND pid=?").get(o,e);if(!t?.started)return!1;const s=c(e);return s!==""&&s!==t.started}async claim(r,o){const e=await this.database();e.exec("BEGIN IMMEDIATE");try{for(const t of o){const s=e.prepare("SELECT owner,pid,mode FROM claims WHERE resource=? AND owner<>?").all(t.key,this.owner);for(const i of s)if(this.gone(e,i.owner,i.pid))e.prepare("DELETE FROM claims WHERE owner=?").run(i.owner),e.prepare("DELETE FROM executors WHERE owner=?").run(i.owner),e.prepare("DELETE FROM runtimes WHERE owner=?").run(i.owner);else if(t.mode==="exclusive"||i.mode==="exclusive")throw new m(t.key);e.prepare("INSERT INTO claims VALUES (?,?,?,?,?) ON CONFLICT(resource,owner,group_id) DO UPDATE SET mode=CASE WHEN claims.mode='exclusive' THEN 'exclusive' ELSE excluded.mode END").run(t.key,this.owner,r,process.pid,t.mode)}e.exec("COMMIT")}catch(t){throw e.exec("ROLLBACK"),t}}async release(r){this.db?.prepare("DELETE FROM claims WHERE owner=? AND group_id=?").run(this.owner,r)}async releaseAll(){this.db&&(this.db.prepare("DELETE FROM claims WHERE owner=?").run(this.owner),this.db.prepare("DELETE FROM runtimes WHERE owner=?").run(this.owner),this.db.close(),this.db=null)}async trackProcess(r){(await this.database()).prepare("INSERT OR IGNORE INTO executors VALUES (?,?,?)").run(this.owner,process.pid,r)}}export{f as ComputerUseOwnership,w as IDLE_RELEASE_MS,m as OwnershipBusyError};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SurfaceGoneError as
|
|
1
|
+
import{SurfaceGoneError as l,SurfaceUnreadableError as p,spec as f,text as i}from"../types.js";import{withoutBlankOptionals as d}from"./normalize.js";import{createReplTools as m}from"./repl.js";import{argComplaint as h}from"./validate.js";function g(e,r){const a=e.execute.bind(e);return f({...e,async execute(u){const t=r.epoch,o=r.p.then(async()=>{if(t!==r.epoch)return i("This queued call was cancelled by a runtime reset; bind fresh handles before continuing.",{error:"runtime_reset"});const c=d(u,e.parameters),n=h(e.parameters,e.name,c);if(n)return i(n,{error:"bad_arguments"});try{return await a(c)}catch(s){if(s instanceof l)return i(s.message,{error:"surface_gone"});if(s instanceof p)return i(s.message,{error:"surface_unreadable"});throw s}});return r.p=o.then(()=>{},()=>{}),o}})}function v(e,r={}){const a=Array.isArray(e)?e:[e];for(const{driver:n}of a)r.diagnostics&&n.supportsDiagnostics&&n.enableDiagnostics();const[u,t]=m(a,{docsDelivered:r.docsDelivered,ownership:r.ownership,idleReleaseMs:r.idleReleaseMs}),o={p:Promise.resolve(),epoch:0},c=t.execute.bind(t);return t.execute=async n=>(o.epoch++,c(n)),[g(u,o),t]}export{v as createComputerUseTools};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{fnName as u}from"./api.js";const
|
|
2
|
-
`)}function l(e){const a=e.extraVerbs();return{head:["API reference \u2014 computer_repl (this text comes once per session; computer.help() returns it again)","","Everything is async \u2014 the verbs AND computer.surfaces() / computer.surface(\u2026): await each. A verb that","fails THROWS with the reason, which stops the run; the journal in the result says what ran. Refs (e12)","come from observe()/find() and stay valid until the surface changes; never invent one, and never carry one in from","another session \u2014 a fresh session re-mints them, so the same e12 names a different element. A target `t` is "+"a find() handle, a ref string, or a point {x, y} read off a screenshot"+".","","Bind a surface first \u2014 every verb below is a method on the handle it returns, never a name of its own:",' await computer.surfaces() \u2192 [handle] \u2014 every surface (tab / window / app) as {id, kind, title, active}; `id` is an opaque key, what the platform knows the surface by lives in `title` (macOS: "TextEdit (com.apple.TextEdit) \u2014 Untitled"; Windows: the window title, then the program name \u2014 "Untitled - Notepad \u2014 Notepad")'," await computer.surface(name) \u2192 handle \u2014 by id, by whole title, or by a piece of the title; several matches or none is a refusal that lists the candidates, never a guess",' await computer.surface() \u2192 handle \u2014 the ONE surface, when there is only one. Not "whatever is in front": with more than one open it refuses and lists them, so a script always says which it meant'," Bind once per surface and keep the handle in a top-level declaration \u2014 it stays valid for later calls, and re-binding costs a read each time."," A browser tab marked unloaded:true is still discoverable. Using its handle restores that exact tab in the background after acquiring ownership; merely listing tabs never reloads it.",""],reads:[...e.interactionNotes(),"Reads (return data to your code; none of them is shown to you except through `return` or computer.log):",e.readsOffscreen?" w.observe({screenshot?}) \u2192 {url, title, elements:[{ref, role, name, state, rect, offscreen?}], groups?, headings?, partial?, coverage, target?} \u2014 every control, viewport and beyond; filter the array rather than asking it to narrow. `rect` {x, y, w, h} is the control's box \u2014 compare boxes to answer layout questions (which row a control sits on, whether it moved) without a screenshot. `groups` is the page's OWN labelling of those controls \u2014 [{label, members:[ref]}] over that same set, off-viewport members included \u2014 present where the author wrote one, and the only place some facts exist (a feed labels each post's button row with its counts, bookmarks included, which no control name and no page text carries). `headings` (web surfaces) is [{name, level, rect}] for the page's headings, never listed as controls \u2014 the fixed point to measure a control's box against, since a click can scroll the container under every control in a card. `partial` (a reason) says the read stopped before covering the surface, so what it did not list may still be there. screenshot:true also attaches the picture of the same screen (one call, two reads), for when you need to SEE where something is":" w.observe({screenshot?}) \u2192 {url, title, elements:[{ref, role, name, state, rect}], coverage} \u2014 every control the window SHOWS (`rect` {x, y, w, h} is its box \u2014 compare boxes to answer layout questions without a screenshot); one scrolled out of view is counted in the observation and never listed, so no ref names it until a scroll brings it in; filter the array rather than asking it to narrow. screenshot:true also attaches the picture of the same screen"," coverage reports exact {offscreen, covered, capped, modal?} counts (null when unreported) even when repeated context notes are omitted. Electron target reports the CDP endpoint, windowId and, when its user agent carries them, the app's own name/version; never infer the app version from the tool or helper path."," w.find(name, {role?, within?, "+(e.readsOffscreen?"visible?, ":"")+"near?}) \u2192 {ref, name, matches, others, incidental?} | null \u2014 by visible name, "+(e.readsOffscreen?"whole surface":"everything the window shows \u2014 NOT what is scrolled out of it, which this platform's read does not contain")+" (an exact name beats a partial one, a partial the query is most of beats one it is only a fragment of, and a control beats a text label); a handle that re-resolves itself when used, so a loop survives a re-render; `matches` says how many controls carried the name and `others` names the runners-up, so an ambiguous name is visible BEFORE you act on the pick; `incidental: true` says NOTHING here is NAMED that and the pick is only a long name that happens to contain the word \u2014 the one `matches: 1` not to trust; narrow it with role (button / textbox / \u2026), within (a ref or handle \u2014 matches inside that element's box, e.g. one sheet or one message)"+(e.readsOffscreen?" visible (on screen only)":"")+" and near (a labelled group restricts candidates to its members; otherwise prefer the control closest to the named element). Duplicate group labels or same-named controls within a group still need disambiguation"," w.text() \u2192 the surface's full readable text",...e.readValue?[" w.value(t) \u2192 ONE control's whole value as a string \u2014 a long field, a clipped label, the text you are about to edit. The listing shows a value cut to what a line holds; this is the rest of it, for one element, where text() re-reads the entire surface"]:[],...e.readPage?[" w.page({ref?, depth?, node?}) \u2192 the content tree as text, with a completeness receipt on its first line"]:[]," w.screenshot({ref?, padding?|region:{x,y,width,height}?|fullPage?, path?}) \u2192 {path, host, width, height, note, capture?} \u2014 the image is attached to this result for you to see AND written to a file the code can keep: to `path` (absolute, never overwrites an existing file, extension must match the surface's format \u2014 .jpg on web surfaces, .png on macOS and Windows) or else to a rolling temp directory; `host` names the machine the path is on; no image \u2192 {path: null, note: why}. ref is a ref string and crops to a visible element's box; padding keeps its surroundings. fullPage is web-only. On macOS and Windows, ref/region preserve native detail; region uses FULL screenshot pixels, so take a full screenshot first. Windows padding and crop bounds use physical pixels; macOS padding uses full-screenshot pixels when available, otherwise window points. capture reports the window, crop, units, pointsPerPixel, and toFullScreenshot (null without a full screenshot or when window geometry changed). Windows refuses a region based on old window geometry; take a new full screenshot. Convert crop pixel p to full screenshot coordinates with toFullScreenshot.origin + p * toFullScreenshot.scale on each axis. A crop never changes point-action coordinates; use a current mapping or act by ref.",...e.supportsDiagnostics?[` w.diagnostics({id?}) \u2192 ${e.diagnosticsAbout.empty} \u2014 the newest 20, with the ones new since your last read marked "new"`]:[]," computer.log(\u2026) \u2192 a line in the journal; the one member that is not on a surface",""],actions:["Actions (every one is w.verb(\u2026) on a bound handle, which switches to that surface first):"," w.click(t, {button?, count?}) w.hover(t)"," w.setText(t, text) \u2014 replace the target field's ENTIRE value, even after selectText; never a selection/caret edit. Returns true when read-back matches; throws WriteVerificationError on mismatch, stopping the run unless caught."," w.setTextBestEffort(t, text) \u2014 the same whole-field replacement, returning true/false without throwing on mismatch. Both return undefined when read-back is unavailable; that is not verification."," click and hover also take a POINT \u2014 w.click({x, y}) \u2014 in the coordinates of the last screenshot, for a control you"," can see and cannot name. It checks no identity (no stale-ref gate, no name, no surface check) and so acts on"," whatever is at that spot NOW: take observe({screenshot: true}) and read the coordinates off that picture."," click / hover / keys / scroll / focus answer with what they did, as text \u2014 the same note the journal shows, e.g. which"," rung a click ended on. A verb that FAILS throws instead, so a returned string always describes something that happened."," w.keys(chord, count?, {focus?}) e.g. w.keys('Enter'), w.keys('Backspace', 3), w.keys('Enter', {focus: field}) \u2014 one key or chord, never a text string. setText replaces a whole field; use the platform's paste/type verb to edit at a selection or caret."," keys go wherever the surface has the keyboard focus. Pass focus: <target> to REQUIRE it be there: if it is not,"," nothing is pressed, including when focus is unknown or ambiguous. Omit focus only to send without that check. Focusing stays explicit (click / setText / selectText with a caret)."," w.scroll({direction?: up|down|left|right, count?: screens (default 1), to?: top|bottom, ref?}) w.wait({} | {ms} | {text} | {text_gone} | {control} | {control_gone} | {url} | {url_gone} | {loaded}) \u2192 ok"," count covers that many screens in ONE call \u2014 the note says how far it actually got, which is less once the end is reached;"," count and to: are refused together, since to: already goes as far as the surface scrolls."," Browser/Electron list scrollable regions with refs. With several regions, pass ref:<region or a control inside it>; a bare scroll refuses and lists the choices. ref:'window' explicitly scrolls the page. Reaching an edge never switches to another region.",...e.scrollsInto?[" w.scroll({into: t}) instead brings ONE control (a ref or a find() handle) to the middle of the viewport and says whether anything still covers it. It takes a covered control's ref too \u2014 the way out from under a sticky header or a bottom strip that a screen of scrolling overshoots."]:[]," a wait polls until the condition holds or it times out, and says which: {text\u2026} searches the readable text,",' {control\u2026} the CONTROLS by name ("until the Stop button is gone" \u2014 the honest way to wait out a long answer),'," {url}/{url_gone} where the surface IS (substring, so a query string needs no spelling out), and {loaded:true} that"," the DOCUMENT finished. Reach for it before reporting that a page has nothing on it: a document that never finishes"," reads as an empty page and no content condition can tell you which it was. It is NOT rendered \u2014 a single-page app"," reaches loaded with nothing drawn yet (measured), so follow it with the CONTENT wait, which is w.wait({}):",' it polls until the readable text stops changing, so it is the one "the page is ready" condition that needs no word'," from the page and no knowledge of the site. Use it after a navigation, a tab switch inside a page, or a search \u2014 a"," single-page app mounts its rows well after {loaded}, and an observation taken before that reads as an empty page."," Also after a click that REDRAWS content (a chart, a list) and before you screenshot or read it: the control's own state"," updates at once, the content a beat later, so a read straight after the click can pair the new button with the old view."," Every condition takes ms as its OWN polling budget (default 16s): w.wait({text:'Done', ms:3000}) gives up in three seconds,"," give or take the one read already in flight when it runs out \u2014 on a heavy surface a single read is seconds, and the result says so when that happened."," wait() returns true/false; awaiting it alone does NOT stop on timeout. Use expect() for a required postcondition, or check the result: if (!await w.wait({...})) throw new Error('condition not met');"," which is what a GUESS at the wording is worth \u2014 an unguarded guess that no page ever matches costs the full 16s."," w.focus() \u2014 select w as the target for this runtime; target selection is not an OS-activation guarantee (see platform interaction notes)"," w.release() \u2014 release this target and its input resources for other sessions. Claims span calls until release, reset, turn end or process exit. Observe again before acting after handoff; old refs are invalidated. Separate windows can still share app/account/document state; ownership does not isolate those or a human editor."," w.expect({text, control, url, value, equals, ms}) \u2014 verify exactly ONE condition; value takes a ref/find handle and equals:string. Read-only polling, default 3000 ms, maximum 16000 ms. Returns true when verified; otherwise throws before subsequent steps. It never repeats the preceding action. Use it to verify save/submit results, not screen-change verdicts."," w.setText(t, text, {ifValue:previousValue}) \u2014 optional precondition: refuse before writing if the field changed or cannot be read. This is a checked precondition, not an atomic transaction with a human or another app.","Action journal evidence: [verified] confirms the stated value/condition; [dispatched] only confirms delivery; [blocked] means no dispatch; [uncertain] may already have acted. Never replay an uncertain send/submit/delete; inspect its actual result first."],extras:[...a.length?[" This platform's verbs \u2014 each returns what it did as text (the journal keeps only its first line, so a listing is read from the return value):"," In these signatures, ? marks an optional property; every unmarked property is required. Omit the ? in JavaScript calls.",...a.map(s=>p(s.kind,s.schema))]:[],...a.some(s=>s.kind==="select_option")?[" selectOption example: const choice = await w.find('Priority', {role:'combobox'}); if (!choice || choice.matches !== 1 || choice.incidental) throw new Error('Priority is missing or ambiguous'); await w.selectOption({ref:choice.ref, option:'High'});"]:[],...a.some(s=>s.kind==="run_js")?[' runJs is the exception: it returns the VALUE the code produced, whole (the journal shows a short preview); an exception thrown in the page comes back as the string "threw: \u2026" instead of stopping the run.']:[],...a.some(s=>s.kind==="download")?[` download also returns a VALUE: const watch = await w.download(); await w.click(await w.find('Download')); const result = await w.download({id:watch.id}); return result; result.downloads carries the actual path, filename, host and state. Read/upload only complete receipts. Query the same ID after a timeout; several candidates mean ambiguous, never the newest file. Re-arming this tab ends its previous collection; stop:true explicitly ends collection without canceling any download. On this session's own tabs a page's save dialog (showSaveFilePicker) never opens: its file arrives as a download marked source "save-picker".`]:[]],tail:[" w.act(kind, {\u2026}) \u2014 any verb by its wire name, arguments forwarded whole; returns its text like the platform verbs","","Observation changes use + for added controls, - for removed controls, and ~ for a surviving ref whose state or label changed. Scope and grouping explanations repeat only when their context changes; observe() still returns the complete available data.","The changed/frozen verdict compares this run's end with the previous run's end: a run whose last action puts the","surface back where it started reads as unchanged \u2014 return your own intermediate observe() results when you need proof.","","Rules that are enforced, not advised:"," \xB7 one verb in flight \u2014 a second one before the first resolves is refused by name (one focus, one caret)"," \xB7 a ref read on surface A is refused on surface B \u2014 act on it through A's handle"," \xB7 a handle whose surface is gone throws: re-acquire with computer.surfaces()"," \xB7 100 actions and 60 s per call; reads, find(), waits, verification and computer.log() also spend this budget"," \xB7 nothing outside this API exists: no require, import, fetch, process, timers, console","","Persistence: top-level const/let/var/function/class declarations live until computer_repl_reset. Keep a","handle in one and reuse it; collect results in a variable across calls; return only what you need.","Batch at verified task boundaries, with room for observation and recovery. A helper's nested verbs also count: a loop of 35 find/setText/expect sequences exceeds 100 actions. Split it into smaller calls.","Keep a top-level checkpoint after each confirmed stage. After an uncertain save/submit, inspect or expect its result before resuming; replaying the whole batch can submit twice. A dispatched checkpoint is not proof of success. Reset or runtime restart loses checkpoints, so recover from surface facts then.","Returned values are capped at 12000 characters and a cut JSON value is not parseable. Read a large result once into a top-level variable, return a small slice per call, and check the final count/identity; do not re-read or replay the task to retrieve the next slice."]}}function g(e){const a=l(e);return[...a.head,...a.reads,...a.actions,...a.extras,...a.tail].join(`
|
|
3
|
-
`)}function
|
|
4
|
-
`)}export{
|
|
1
|
+
import{fnName as u}from"./api.js";import{IDLE_RELEASE_MS as p}from"../ownership.js";const g="Run JavaScript in this session's PERSISTENT runtime to look at and act on the GUI \u2014 web pages in a browser, Electron apps and native macOS / Windows apps, whichever this server mounts \u2014 the one tool for both. The code is an async function body: `await` every verb, `return` a value to get it back as JSON, and top-level const/let/function declarations SURVIVE to later calls \u2014 keep handles and collected data in them instead of re-reading. Surfaces (tabs / windows / apps) are HANDLES and EVERY verb lives on one \u2014 there are no top-level verbs. Bind first: computer.surface('<id or title>') names one, computer.surface() alone takes the only surface, or the one this session opened when other tabs are open too, computer.surfaces() lists them. Then w.observe() w.find(name) w.click(t) w.setText(t, text) w.keys(chord, n) w.scroll({\u2026}) w.wait({\u2026}) w.text() w.page({\u2026}) w.screenshot({\u2026}) and this platform's own verbs, each switching to that surface as needed; a ref read on one surface is refused on another. Bind ONCE \u2014 keep the handle in a top-level variable and later calls reuse it. The first bound operation claims the target across calls; await w.release() when its task is done. Another runtime's claim refuses input before dispatch. The first result carries the full API reference (computer.help() returns it again) \u2014 use only what it documents. One verb at a time (one focus, one caret): a failed action or expect() throws and stops the run; wait() instead returns false on timeout, so check it or use expect() for a required condition. The result is the step journal plus one observation of the surface it acted on. Per call: 100 actions, 60 s. No require/import/fetch/process/timers. Before using an operation that may affect the user's foreground, real pointer, keyboard focus or system clipboard (including automatic fallback), explain its target and effects and obtain permission unless existing permission covers them. Do not silently escalate a background operation; inspect computer.help() for platform-specific effects. Before sending a message or publishing a post, let the user confirm its content and recipients or destination, unless they already approved those exact details.",v=`Reset computer_repl's persistent JavaScript runtime: every binding \u2014 handles, variables, functions \u2014 is discarded, and the next computer_repl call starts fresh and returns the API reference again. Nothing on the surfaces changes: no tab, window or app is closed. This runtime's target ownership is released after any active call stops. Use it only when the runtime is wedged or its state is wrong, never as a way to "start over" on the screen.`;function f(e,a){const o=a,s=Object.keys(o.properties??{}).filter(t=>t!=="kind"),n=new Set(o.required??[]),r=t=>n.has(t)?t:`${t}?`,h=s.filter(t=>t!=="ref"&&t!=="name").map(t=>o.properties?.[t]?.description).find(Boolean),i=o.description??h??"",c=/^.*?[.!?](?=\s+[A-Z(`]|$)/s.exec(i)?.[0]??i,d=s.filter(t=>t==="ref"||t==="name").flatMap(t=>o.properties?.[t]?.description?[` ${r(t)}: ${o.properties[t].description}`]:[]);return[` w.${u(e)}({${s.map(r).join(", ")}})${c?` \u2014 ${c.trim()}`:""}`,...d,...n.has("ref")&&s.includes("name")?[" name does not replace the required ref."]:[]].join(`
|
|
2
|
+
`)}function l(e){const a=e.extraVerbs();return{head:["API reference \u2014 computer_repl (this text comes once per session; computer.help() returns it again)","","Everything is async \u2014 the verbs AND computer.surfaces() / computer.surface(\u2026): await each. A verb that","fails THROWS with the reason, which stops the run; the journal in the result says what ran. Refs (e12)","come from observe()/find() and stay valid until the surface changes; never invent one, and never carry one in from","another session \u2014 a fresh session re-mints them, so the same e12 names a different element. A target `t` is "+"a find() handle, a ref string, or a point {x, y} read off a screenshot"+".","","Bind a surface first \u2014 every verb below is a method on the handle it returns, never a name of its own:",' await computer.surfaces() \u2192 [handle] \u2014 every surface (tab / window / app) as {id, kind, title, active}; `id` is an opaque key, what the platform knows the surface by lives in `title` (macOS: "TextEdit (com.apple.TextEdit) \u2014 Untitled"; Windows: the window title, then the program name \u2014 "Untitled - Notepad \u2014 Notepad")'," await computer.surface(name) \u2192 handle \u2014 by id, by whole title, or by a piece of the title; several matches or none is a refusal that lists the candidates, never a guess",' await computer.surface() \u2192 handle \u2014 the ONE surface, when there is only one. Not "whatever is in front": with more than one open it refuses and lists them, so a script always says which it meant'," Bind once per surface and keep the handle in a top-level declaration \u2014 it stays valid for later calls, and re-binding costs a read each time."," A browser tab marked unloaded:true is still discoverable. Using its handle restores that exact tab in the background after acquiring ownership; merely listing tabs never reloads it.",""],reads:[...e.interactionNotes(),"Reads (return data to your code; none of them is shown to you except through `return` or computer.log):",e.readsOffscreen?" w.observe({screenshot?}) \u2192 {url, title, elements:[{ref, role, name, state, rect, offscreen?}], groups?, headings?, partial?, coverage, target?} \u2014 every control, viewport and beyond; filter the array rather than asking it to narrow. `rect` {x, y, w, h} is the control's box \u2014 compare boxes to answer layout questions (which row a control sits on, whether it moved) without a screenshot. `groups` is the page's OWN labelling of those controls \u2014 [{label, members:[ref]}] over that same set, off-viewport members included \u2014 present where the author wrote one, and the only place some facts exist (a feed labels each post's button row with its counts, bookmarks included, which no control name and no page text carries). `headings` (web surfaces) is [{name, level, rect}] for the page's headings, never listed as controls \u2014 the fixed point to measure a control's box against, since a click can scroll the container under every control in a card. `partial` (a reason) says the read stopped before covering the surface, so what it did not list may still be there. screenshot:true also attaches the picture of the same screen (one call, two reads), for when you need to SEE where something is":" w.observe({screenshot?}) \u2192 {url, title, elements:[{ref, role, name, state, rect}], coverage} \u2014 every control the window SHOWS (`rect` {x, y, w, h} is its box \u2014 compare boxes to answer layout questions without a screenshot); one scrolled out of view is counted in the observation and never listed, so no ref names it until a scroll brings it in; filter the array rather than asking it to narrow. screenshot:true also attaches the picture of the same screen"," coverage reports exact {offscreen, covered, capped, modal?} counts (null when unreported) even when repeated context notes are omitted. Electron target reports the CDP endpoint, windowId and, when its user agent carries them, the app's own name/version; never infer the app version from the tool or helper path."," w.find(name, {role?, within?, "+(e.readsOffscreen?"visible?, ":"")+"near?}) \u2192 {ref, name, matches, others, incidental?} | null \u2014 by visible name, "+(e.readsOffscreen?"whole surface":"everything the window shows \u2014 NOT what is scrolled out of it, which this platform's read does not contain")+" (an exact name beats a partial one, a partial the query is most of beats one it is only a fragment of, and a control beats a text label); a handle that re-resolves itself when used, so a loop survives a re-render; `matches` says how many controls carried the name and `others` names the runners-up, so an ambiguous name is visible BEFORE you act on the pick; `incidental: true` says NOTHING here is NAMED that and the pick is only a long name that happens to contain the word \u2014 the one `matches: 1` not to trust; narrow it with role (button / textbox / \u2026), within (a ref or handle \u2014 matches inside that element's box, e.g. one sheet or one message)"+(e.readsOffscreen?" visible (on screen only)":"")+" and near (a labelled group restricts candidates to its members; otherwise prefer the control closest to the named element). Duplicate group labels or same-named controls within a group still need disambiguation"," w.text() \u2192 the surface's full readable text",...e.readValue?[" w.value(t) \u2192 ONE control's whole value as a string \u2014 a long field, a clipped label, the text you are about to edit. The listing shows a value cut to what a line holds; this is the rest of it, for one element, where text() re-reads the entire surface"]:[],...e.readPage?[" w.page({ref?, depth?, node?}) \u2192 the content tree as text, with a completeness receipt on its first line"]:[]," w.screenshot({ref?, padding?|region:{x,y,width,height}?|fullPage?, path?}) \u2192 {path, host, width, height, note, capture?} \u2014 the image is attached to this result for you to see AND written to a file the code can keep: to `path` (absolute, never overwrites an existing file, extension must match the surface's format \u2014 .jpg on web surfaces, .png on macOS and Windows) or else to a rolling temp directory; `host` names the machine the path is on; no image \u2192 {path: null, note: why}. ref is a ref string and crops to a visible element's box; padding keeps its surroundings. fullPage is web-only. On macOS and Windows, ref/region preserve native detail; region uses FULL screenshot pixels, so take a full screenshot first. Windows padding and crop bounds use physical pixels; macOS padding uses full-screenshot pixels when available, otherwise window points. capture reports the window, crop, units, pointsPerPixel, and toFullScreenshot (null without a full screenshot or when window geometry changed). Windows refuses a region based on old window geometry; take a new full screenshot. Convert crop pixel p to full screenshot coordinates with toFullScreenshot.origin + p * toFullScreenshot.scale on each axis. A crop never changes point-action coordinates; use a current mapping or act by ref.",...e.supportsDiagnostics?[` w.diagnostics({id?}) \u2192 ${e.diagnosticsAbout.empty} \u2014 the newest 20, with the ones new since your last read marked "new"`]:[]," computer.log(\u2026) \u2192 a line in the journal; the one member that is not on a surface",""],actions:["Actions (every one is w.verb(\u2026) on a bound handle, which switches to that surface first):"," w.click(t, {button?, count?}) w.hover(t)"," w.setText(t, text) \u2014 replace the target field's ENTIRE value, even after selectText; never a selection/caret edit. Returns true when read-back matches; throws WriteVerificationError on mismatch, stopping the run unless caught."," w.setTextBestEffort(t, text) \u2014 the same whole-field replacement, returning true/false without throwing on mismatch. Both return undefined when read-back is unavailable; that is not verification."," click and hover also take a POINT \u2014 w.click({x, y}) \u2014 in the coordinates of the last screenshot, for a control you"," can see and cannot name. It checks no identity (no stale-ref gate, no name, no surface check) and so acts on"," whatever is at that spot NOW: take observe({screenshot: true}) and read the coordinates off that picture."," click / hover / keys / scroll / focus answer with what they did, as text \u2014 the same note the journal shows, e.g. which"," rung a click ended on. A verb that FAILS throws instead, so a returned string always describes something that happened."," w.keys(chord, count?, {focus?}) e.g. w.keys('Enter'), w.keys('Backspace', 3), w.keys('Enter', {focus: field}) \u2014 one key or chord, never a text string. setText replaces a whole field; use the platform's paste/type verb to edit at a selection or caret."," keys go wherever the surface has the keyboard focus. Pass focus: <target> to REQUIRE it be there: if it is not,"," nothing is pressed, including when focus is unknown or ambiguous. Omit focus only to send without that check. Focusing stays explicit (click / setText / selectText with a caret)."," w.scroll({direction?: up|down|left|right, count?: screens (default 1), to?: top|bottom, ref?}) w.wait({} | {ms} | {text} | {text_gone} | {control} | {control_gone} | {url} | {url_gone} | {loaded}) \u2192 ok"," count covers that many screens in ONE call \u2014 the note says how far it actually got, which is less once the end is reached;"," count and to: are refused together, since to: already goes as far as the surface scrolls."," Browser/Electron list scrollable regions with refs. With several regions, pass ref:<region or a control inside it>; a bare scroll refuses and lists the choices. ref:'window' explicitly scrolls the page. Reaching an edge never switches to another region.",...e.scrollsInto?[" w.scroll({into: t}) instead brings ONE control (a ref or a find() handle) to the middle of the viewport and says whether anything still covers it. It takes a covered control's ref too \u2014 the way out from under a sticky header or a bottom strip that a screen of scrolling overshoots."]:[]," a wait polls until the condition holds or it times out, and says which: {text\u2026} searches the readable text,",' {control\u2026} the CONTROLS by name ("until the Stop button is gone" \u2014 the honest way to wait out a long answer),'," {url}/{url_gone} where the surface IS (substring, so a query string needs no spelling out), and {loaded:true} that"," the DOCUMENT finished. Reach for it before reporting that a page has nothing on it: a document that never finishes"," reads as an empty page and no content condition can tell you which it was. It is NOT rendered \u2014 a single-page app"," reaches loaded with nothing drawn yet (measured), so follow it with the CONTENT wait, which is w.wait({}):",' it polls until the readable text stops changing, so it is the one "the page is ready" condition that needs no word'," from the page and no knowledge of the site. Use it after a navigation, a tab switch inside a page, or a search \u2014 a"," single-page app mounts its rows well after {loaded}, and an observation taken before that reads as an empty page."," Also after a click that REDRAWS content (a chart, a list) and before you screenshot or read it: the control's own state"," updates at once, the content a beat later, so a read straight after the click can pair the new button with the old view."," Every condition takes ms as its OWN polling budget (default 16s): w.wait({text:'Done', ms:3000}) gives up in three seconds,"," give or take the one read already in flight when it runs out \u2014 on a heavy surface a single read is seconds, and the result says so when that happened."," wait() returns true/false; awaiting it alone does NOT stop on timeout. Use expect() for a required postcondition, or check the result: if (!await w.wait({...})) throw new Error('condition not met');"," which is what a GUESS at the wording is worth \u2014 an unguarded guess that no page ever matches costs the full 16s."," w.focus() \u2014 select w as the target for this runtime; target selection is not an OS-activation guarantee (see platform interaction notes)",` w.release() \u2014 release this target and its input resources for other sessions. Claims span calls until release, reset, turn end, process exit or ${p/6e4} minutes with no call (the next call takes the target back unless another session has it by then). Observe again before acting after handoff; old refs are invalidated. Separate windows can still share app/account/document state; ownership does not isolate those or a human editor.`," w.expect({text, control, url, value, equals, ms}) \u2014 verify exactly ONE condition; value takes a ref/find handle and equals:string. Read-only polling, default 3000 ms, maximum 16000 ms. Returns true when verified; otherwise throws before subsequent steps. It never repeats the preceding action. Use it to verify save/submit results, not screen-change verdicts."," w.setText(t, text, {ifValue:previousValue}) \u2014 optional precondition: refuse before writing if the field changed or cannot be read. This is a checked precondition, not an atomic transaction with a human or another app.","Action journal evidence: [verified] confirms the stated value/condition; [dispatched] only confirms delivery; [blocked] means no dispatch; [uncertain] may already have acted. Never replay an uncertain send/submit/delete; inspect its actual result first."],extras:[...a.length?[" This platform's verbs \u2014 each returns what it did as text (the journal keeps only its first line, so a listing is read from the return value):"," In these signatures, ? marks an optional property; every unmarked property is required. Omit the ? in JavaScript calls.",...a.map(s=>f(s.kind,s.schema))]:[],...a.some(s=>s.kind==="select_option")?[" selectOption example: const choice = await w.find('Priority', {role:'combobox'}); if (!choice || choice.matches !== 1 || choice.incidental) throw new Error('Priority is missing or ambiguous'); await w.selectOption({ref:choice.ref, option:'High'});"]:[],...a.some(s=>s.kind==="run_js")?[' runJs is the exception: it returns the VALUE the code produced, whole (the journal shows a short preview); an exception thrown in the page comes back as the string "threw: \u2026" instead of stopping the run.']:[],...a.some(s=>s.kind==="download")?[` download also returns a VALUE: const watch = await w.download(); await w.click(await w.find('Download')); const result = await w.download({id:watch.id}); return result; result.downloads carries the actual path, filename, host and state. Read/upload only complete receipts. Query the same ID after a timeout; several candidates mean ambiguous, never the newest file. Re-arming this tab ends its previous collection; stop:true explicitly ends collection without canceling any download. On this session's own tabs a page's save dialog (showSaveFilePicker) never opens: its file arrives as a download marked source "save-picker".`]:[]],tail:[" w.act(kind, {\u2026}) \u2014 any verb by its wire name, arguments forwarded whole; returns its text like the platform verbs","","Observation changes use + for added controls, - for removed controls, and ~ for a surviving ref whose state or label changed. Scope and grouping explanations repeat only when their context changes; observe() still returns the complete available data.","The changed/frozen verdict compares this run's end with the previous run's end: a run whose last action puts the","surface back where it started reads as unchanged \u2014 return your own intermediate observe() results when you need proof.","","Rules that are enforced, not advised:"," \xB7 one verb in flight \u2014 a second one before the first resolves is refused by name (one focus, one caret)"," \xB7 a ref read on surface A is refused on surface B \u2014 act on it through A's handle"," \xB7 a handle whose surface is gone throws: re-acquire with computer.surfaces()"," \xB7 100 actions and 60 s per call; reads, find(), waits, verification and computer.log() also spend this budget"," \xB7 nothing outside this API exists: no require, import, fetch, process, timers, console","","Persistence: top-level const/let/var/function/class declarations live until computer_repl_reset. Keep a","handle in one and reuse it; collect results in a variable across calls; return only what you need.","Batch at verified task boundaries, with room for observation and recovery. A helper's nested verbs also count: a loop of 35 find/setText/expect sequences exceeds 100 actions. Split it into smaller calls.","Keep a top-level checkpoint after each confirmed stage. After an uncertain save/submit, inspect or expect its result before resuming; replaying the whole batch can submit twice. A dispatched checkpoint is not proof of success. Reset or runtime restart loses checkpoints, so recover from surface facts then.","Returned values are capped at 12000 characters and a cut JSON value is not parseable. Read a large result once into a top-level variable, return a small slice per call, and check the final count/identity; do not re-read or replay the task to retrieve the next slice."]}}function b(e){const a=l(e);return[...a.head,...a.reads,...a.actions,...a.extras,...a.tail].join(`
|
|
3
|
+
`)}function y(e){const a=l(e);return[`Reaching a second platform: ${e.platform} \u2014 everything else in the reference above still holds, and these are the reads and verbs that are its own.`,"",...a.reads,...a.extras].join(`
|
|
4
|
+
`)}export{g as REPL_DESCRIPTION,v as RESET_DESCRIPTION,b as apiReference,y as platformReference};
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import{createContext as
|
|
2
|
-
`);if(!e.length)throw new Error("this driver has no surface right now");if(n===void 0){if(e.length===1)return e[0];throw new Error(`${e.length} surfaces are open \u2014 name one, computer.surface('<id or title>'):
|
|
3
|
-
${
|
|
4
|
-
`);throw new Error(`no surface matches ${JSON.stringify(
|
|
5
|
-
${
|
|
6
|
-
${
|
|
7
|
-
${
|
|
1
|
+
import{createContext as Ue,runInContext as Fe}from"node:vm";import{Type as f}from"typebox";import{spec as ve,text as x}from"../types.js";import{MAX_ACTIONS as be,MAX_MS as ye,SYNC_MAX_MS as $e,RETURN_CAP as ie,MAX_REF_OWNERS as Je,trimObservation as Be}from"./repl-budget.js";import{createComputerUseApi as He,noteOf as Se,renderJournal as We,stringify as qe,verbGate as Ge}from"./api.js";import{prepareCode as Ke}from"./repl-code.js";import{apiReference as Re,platformReference as Xe,REPL_DESCRIPTION as ze,RESET_DESCRIPTION as Ye}from"./repl-docs.js";import{ComputerUseOwnership as Qe,IDLE_RELEASE_MS as Ve,OwnershipBusyError as ke}from"../ownership.js";const Ee=new Set(["observe","find","text","value","page","screenshot","diagnostics","targets","log","verb","release"]),Ze=new Set(["dialog","close_tab"]);function ct(F,D={}){if(!F.length)throw new Error("createReplTools needs at least one platform");let M=null;const b=D.ownership??new Qe,y=new WeakMap,X=new WeakMap;let N=null,j=!1,oe=Promise.resolve(),J=!1,C=null,B=null;const Te=D.idleReleaseMs??Ve;let H=null,P=null,$,z=!!D.docsDelivered,Y=!!D.docsDelivered,I=null;const k=()=>{if(!I)throw new Error("no computer_repl call is in progress \u2014 every verb must be awaited inside the call that started it");return I},v=new Map,O=new Map,L=new WeakMap;for(const{driver:e}of F)e.onDisconnect(async()=>{for(const n of O.values()){if(L.get(n)?.slice.driver!==e)continue;const t=y.get(n);t&&await b.release(t),y.delete(n);for(const[i,s]of v)s===n.id&&v.delete(i)}});const Q=F.length>1;let m=null,S=null;const Oe=e=>{const n=m?.id;if(n){for(const t of e)v.delete(t),v.set(t,n);for(const t of v.keys()){if(v.size<=Je)break;v.delete(t)}}},_e=e=>{if(!m)return;const n=v.get(e);if(n&&n!==m.id)throw new Error(`ref ${e} was read on surface ${n}, not on ${m.id} (${m.title}) \u2014 act on it through computer.surface('${n}'), or observe ${m.id} and use one of its refs`)},V=Ge(e=>I?.push("verb","",!1,e)),u=F.map(e=>({...e,id:e.driver.platform,api:He(e.driver,k,{onRefs:Oe,onTarget:_e,beforeAction:async(n,t)=>{if(!m)throw new Error("An action needs a bound surface.");const i=y.get(m);if(!i)throw new Error("The target was released; observe it before acting again.");try{await b.claim(i,await e.driver.actionResources(n,t))}catch(s){throw s instanceof ke&&(N=s.message),s}}}),docsDue:!D.docsDelivered,lastMiss:null}));if($=u[0],Q)for(const e of u)e.driver.surfaceIdPrefix=`${e.id}/`;const ae=u[0].api,Z=async()=>{const e=[],n=S?[S]:u;for(const t of n){let i;try{i=await t.driver.listTargets()}catch(s){if(n.length===1)throw s;const l=s instanceof Error?s.message:String(s);t.lastMiss!==l&&I?.push("targets",t.id,!1,`${t.id} is not reachable: ${l}`),t.lastMiss=l;continue}t.lastMiss=null;for(const s of i)e.push({...s,id:Q?`${t.id}/${s.id}`:s.id,slice:t,inner:s.id})}return e};class ee extends Error{}const te=async e=>{const n=L.get(e);if($=n.slice,P=e,await n.slice.driver.isActiveTarget(n.inner)){await le(e,!0);return}k().charge("focus");const t=await n.slice.driver.focusTarget(n.inner);if(k().push("focus",e.id,t.ok,Se(t)||`\u2192 ${e.title}`),!t.ok)throw O.delete(e.id),new ee(`surface ${e.id} (${e.title}) is gone from this session \u2014 closed, replaced or out of reach; when that is known, a NOTE in this result or the one before it names which (it is said once, when the loss is seen). Call computer.surfaces() and re-acquire what you need.`);await le(e,!1)},le=async(e,n)=>{const{slice:t,inner:i}=L.get(e),s=t.driver;let l=await s.ownershipResources();const c=y.get(e);if((!c||c!==l[0]?.key)&&!await s.isActiveTarget(i)){const d=await s.focusTarget(i);if(!d.ok)throw new ee(`Surface ${e.id} disappeared while binding; list surfaces and re-acquire it.`);if(k().push("focus",e.id,!0,Se(d)||`\u2192 ${e.title}`),l=await s.ownershipResources(),!await s.isActiveTarget(i))throw new Error("The target kept changing while binding; observe before continuing.")}const h=l[0]?.key;if(!h)throw new Error("The driver did not identify a resource to claim.");const E=!!c&&c!==h;if(E&&!l.some(d=>X.get(e)?.has(d.key)))throw O.delete(e.id),y.delete(e),X.delete(e),await b.release(c),new Error("This handle now resolves to a different target. List surfaces and bind the intended one again.");try{await b.claim(h,l)}catch(d){throw d instanceof ke&&(N=d.message),d}E&&await b.release(c),y.set(e,h),X.set(e,new Set(l.map(d=>d.key))),!c&&!n&&s.invalidateObservation(),await s.prepareTarget()},re=e=>{const n=O.get(e.id);if(n)return n.title=e.title,n.kind=e.kind,n.active=e.active,n.own=e.own,n.mine=e.mine,n.unloaded=e.unloaded,n;const t={id:e.id,kind:e.kind,title:e.title,active:e.active,...e.own===void 0?{}:{own:e.own},...e.mine===void 0?{}:{mine:e.mine},...e.unloaded?{unloaded:!0}:{}};L.set(t,{slice:e.slice,inner:e.inner});const i={};for(const[s,l]of Object.entries(e.slice.api))typeof l!="function"||s==="log"||s==="focus"||s==="targets"||(i[s]=async(...c)=>{m=t;try{try{await te(t)}catch(h){if(s==="wait"&&h instanceof ee)return`${t.id} is gone \u2014 closed or replaced, so there is nothing left on it to wait for. computer.surfaces() lists what remains`;throw h}return await l(...c)}finally{m=null}});return i.focus=async()=>{m=t;try{await te(t)}finally{m=null}},i.release=async()=>{k().charge("release");const s=y.get(t);await e.slice.driver.releaseInputResources(),s&&await b.release(s),y.delete(t),e.slice.driver.invalidateObservation();for(const[l,c]of v)c===t.id&&v.delete(l);return k().push("release",t.id,!0,"ownership released; observe again before continuing"),"Ownership released. Other sessions may now use this target; observe before continuing."},Object.assign(t,V(i)),O.set(e.id,t),t},xe=(e,n)=>{const t=a=>a.mine?" (yours \u2014 opened by this session)":a.own&&a.mine===!1?" (left open by an earlier session)":"",i=a=>a.mine?0:a.own?1:2,s=a=>[...a].sort((p,T)=>i(p)-i(T)).map(p=>` ${p.id} ${p.title}${t(p)}`).join(`
|
|
2
|
+
`);if(!e.length)throw new Error("this driver has no surface right now");if(n===void 0){if(e.length===1)return e[0];const a=e.filter(T=>T.mine),p=a.filter(T=>T.active);if(p.length===1)return p[0];if(a.length===1)return a[0];throw new Error(`${e.length} surfaces are open \u2014 name one, computer.surface('<id or title>'):
|
|
3
|
+
${s(e)}`)}const l=String(n).trim(),c=e.filter(a=>a.id===l);if(c.length===1)return c[0];const h=l.toLowerCase(),E=e.filter(a=>a.title.toLowerCase()===h),d=E.length?E:e.filter(a=>a.title.toLowerCase().includes(h));if(d.length===1)return d[0];const q=d.filter(a=>a.primary);if(q.length===1)return q[0];if(!d.length){const a=[...new Set(u.map(p=>p.driver.explainMiss(l)).filter(Boolean))].join(`
|
|
4
|
+
`);throw new Error(`no surface matches ${JSON.stringify(l)} \u2014 the open ones are:
|
|
5
|
+
${s(e)}${a?`
|
|
6
|
+
${a}`:""}`)}throw new Error(`${d.length} surfaces match ${JSON.stringify(l)} \u2014 name one exactly:
|
|
7
|
+
${s(d)}`)},Ce=async e=>{try{return await ce(e)}catch(n){const t=e.toLowerCase(),i=(await Z()).filter(s=>s.active&&(s.id===e||s.title.toLowerCase().includes(t)));if(i.length===1)return i[0];throw n}},ce=async e=>{const n=await Z(),t=e===void 0?"":String(e).trim();if(t&&!n.some(i=>i.id===t||i.title.toLowerCase().includes(t.toLowerCase())))for(const i of S?[S]:u){const s=await i.driver.unlistedTarget(t);if(s)return{...s,id:Q?`${i.id}/${s.id}`:s.id,slice:i,inner:s.id}}return xe(n,e)},Ie=V({surfaces:async()=>{k().charge("targets");const e=await Z();return k().push("targets","",!0,`${e.length} surfaces`),e.map(re)},surface:async e=>re(await ce(e)),log:ae.log,help:()=>H=u.map(e=>Re(e.driver)).join(`
|
|
8
8
|
|
|
9
|
-
`)}),
|
|
10
|
-
${
|
|
11
|
-
This computer_repl runtime was restarted (idle), so top-level bindings and refs from before are gone; the API reference you already have still applies \u2014 computer.help() returns it again.`,pinned:!0})),
|
|
12
|
-
`),{verdict:"unknown",ledgerLines:
|
|
13
|
-
`),{verdict:"unknown",ledgerLines:
|
|
14
|
-
`)}${
|
|
15
|
-
NOTE: ${
|
|
16
|
-
Observation skipped: the bound target was released, closed or replaced. List surfaces and bind the intended target before continuing.`,{verdict:"unknown",ledgerLines:
|
|
17
|
-
`),...
|
|
9
|
+
`)}),Le=new Set(Object.keys(ae)),de=(e,n)=>{const t=/^(?:ReferenceError: )?(\w+) is not defined$/.exec(e);return t?Le.has(t[1])?`${e} \u2014 verbs live on a surface handle, not on the top level: const w = await computer.surface('<id or title>'); await w.${t[1]}(\u2026). computer.surface() with no argument binds the only surface, or the one this session opened among several; computer.surfaces() lists them.`:`${e} \u2014 `+(n?"this call started a FRESH runtime, so nothing declared in an earlier call survives (the server was restarted, or its host reaped it while idle). ":"no call on this runtime declared it at the top level, and nothing is predefined but computer. ")+"If it is a surface handle, bind it: const w = await computer.surface('<id or title>').":e},Ae=()=>Ue({computer:Ie,console:void 0}),De=e=>Object.entries(e).flatMap(([n,t])=>t&&typeof t=="object"&&L.has(t)&&O.get(t.id)===t?[`${n} \u2192 ${t.id} \xAB${t.title}\xBB`]:[]),W=ve({name:"computer_repl",label:"Computer REPL",description:ze,promptSnippet:"computer_repl runs JavaScript over the GUI in a persistent session: verbs, surface handles, one observation per call.",promptGuidelines:["Plan several actions per computer_repl call \u2014 one call costs one step and one observation whatever it does inside.","Keep surface handles and collected data in top-level declarations; they persist across calls.","Re-find by name inside a loop rather than reusing a ref you read earlier: find() handles re-resolve, raw refs do not survive a re-render."],parameters:f.Object({code:f.String({description:"JavaScript, run as the body of an async function in the persistent runtime. `await` every verb; `return` what you want back. Bind once \u2014 first call: `const w = await computer.surface();` \u2014 binding opens nothing (a browser page is opened with `await w.openUrl(url)` once bound), then later calls use `w` directly: `for (const e of (await w.observe()).elements.filter(e => e.role === 'checkbox')) await w.click(e.ref);`"}),purpose:f.Optional(f.String({maxLength:500,description:"Briefly describe the intended task when requesting app access. Shown to the user as the agent's stated purpose; it does not narrow the app permission. Do not include secrets."})),interaction:f.Optional(f.Union([f.Literal("background"),f.Literal("desktop")],{description:"Native apps default to background \u2014 macOS: driver mouse/activation/clipboard routes are blocked, including fallback; Windows: limited UIA setText/click/scroll by ref, with automatic focus and real-input fallback disabled; unsupported routes are refused. Use desktop only after explaining effects and obtaining permission covering this call. This option is not user consent."})),platform:f.Optional(f.String({description:'For a HOST that mounts several platforms in one session: the platform this call is about ("browser", "electron", "macos", "windows"). computer.surfaces() and computer.surface() then see only its surfaces, and the observation is of it; the other platforms stay mounted and untouched.'})),target:f.Optional(f.String({description:"Bind this surface before the code runs \u2014 the same string computer.surface() takes (an id, a whole title, or a piece of one). For a HOST that decides which surface a call is about: the code can still bind whatever it likes, and this only settles where a run that binds nothing lands."})),observation:f.Optional(f.Union([f.Literal("full"),f.Literal("brief"),f.Literal("none")],{description:'How much of the end-of-run observation to render. "full" (default) is the element list and the text excerpt; "brief" keeps the head (title, targets, verdict) and drops the listing; "none" leaves only the action journal. The observation is always TAKEN either way \u2014 the change verdict and the stale-ref bookkeeping are unaffected \u2014 and a NOTE line always survives, so a swallowed click is never hidden by this. Reach for it when the window holds content you do not want in the transcript, or when your own `return` value is the answer.'}))}),async execute(e){N=null,H=null;const n=String(e.code??"");if(!n.trim())return x("computer_repl needs code to run.",{error:"bad_args"});const t=String(e.platform??"").trim();if(S=t?u.find(r=>r.id===t)??null:null,t&&!S)return x(`platform ${JSON.stringify(t)} is not mounted in this session (mounted: ${u.map(r=>r.id).join(", ")}).`,{error:"bad_args"});S&&($=S);const i=e.purpose;for(const r of u)r.driver.interactionMode=e.interaction==="desktop"?"desktop":"background",r.driver.consentPurpose=typeof i=="string"&&i.trim().slice(0,500)||null,r.driver.permissionRefusal=null;const s=e.observation,l=s==="brief"||s==="none"?s:"full",c=[],h=[],E=Date.now(),d=()=>u.reduce((r,o)=>r+o.driver.waitedOnUserMs,0),q=d();let a=0,p=null,T=!1,G=null;const ue={fields:0,verified:0,ended:null};I={round:ue,charge(r){if(G??=Date.now(),j)throw new Error("The runtime was reset; this run stops before its next operation.");if(++a>be)throw new Error(`step cap reached (${be} actions) at ${r} \u2014 split the work across calls, or narrow the loop`);if(Date.now()-E-(d()-q)>ye)throw new Error(`time cap reached (${ye/1e3}s) at ${r} \u2014 the surface may be slower than the code assumes`)},push(r,o,R,_,we,ge){!we&&R&&!Ee.has(r)&&!Ze.has(r)&&(p=r),!we&&!R&&!Ee.has(r)&&(T=!0);const me=G===null?void 0:Date.now()-G;G=null,c.push({i:c.length+1,kind:r,target:o,ok:R,note:_,...ge?{outcome:ge}:{},...me!==void 0?{ms:me}:{}})},attach(r){h.push(r)}};const fe=!M;M??=Ae();let K,w=null;try{const r=String(e.target??"").trim();r&&await te(re(await Ce(r))),K=await Fe(Ke(n),M,{filename:"computer_repl",timeout:$e})}catch(r){r.code==="ERR_SCRIPT_EXECUTION_TIMEOUT"&&(r=new Error(`the code ran for ${$e/1e3}s without awaiting anything and was stopped \u2014 a loop that never awaits blocks the whole runtime; await a verb (or w.wait) inside it`)),w=de(r instanceof Error?`${r.name==="Error"?"":`${r.name}: `}${r.message}`:String(r),fe)}finally{try{await V.drain()}catch(r){w??=de(r instanceof Error?r.message:String(r),fe)}I=null}const he=Date.now()-E,pe=(he/1e3).toFixed(1),U=r=>({runMs:he,...r!==void 0?{observeMs:r}:{},steps:c.map(o=>({kind:o.kind,target:o.target,ok:o.ok,...o.outcome?{outcome:o.outcome}:{},...o.ms!==void 0?{ms:o.ms}:{},note:o.note.slice(0,160)}))}),g=[w?`Run STOPPED after ${c.length} steps (${pe}s): ${w}`:`Run completed ${c.length} steps in ${pe}s.`,...We(c)];if(w&&c.length>0&&g.push("Nothing was returned: the throw ended the call before `return`. What survives a stop \u2014 top-level bindings (read them in your next call) and anything already passed to log() (in the journal above)."),w){const r=De(M);r.length&&g.push(`Still bound: ${r.join(", ")} \u2014 use ${r.length===1?"it":"them"} directly in the next call.`)}if(K!==void 0){const r=qe(K);g.push(`Returned: ${r.length>ie&&K!==H?`${r.slice(0,ie)}\u2026 (cut at ${ie} of ${r.length} chars \u2014 return a slice, a count or a filtered list instead of the whole thing)`:r}`)}const A=u.filter(r=>r.docsDue&&(r===$||u.length===1)).map(r=>{r.docsDue=!1;const o=!z;return z=!0,{type:"text",text:`
|
|
10
|
+
${o?Re(r.driver):Xe(r.driver)}`,pinned:!0}});Y&&(Y=!1,A.push({type:"text",text:`
|
|
11
|
+
This computer_repl runtime was restarted (idle), so top-level bindings and refs from before are gone; the API reference you already have still applies \u2014 computer.help() returns it again.`,pinned:!0})),p?$.driver.noteWriteRound(ue):T&&$.driver.noteContentRead();const Pe=p?{afterAction:p}:{},se=u.find(r=>r.driver.permissionRefusal)?.driver.permissionRefusal;if(H&&c.length===0){const r=x(g.join(`
|
|
12
|
+
`),{verdict:"unknown",ledgerLines:g.length,timing:U(),...w?{error:"run_stopped"}:{}});return r.content.push(...A),r}if(se||N){const r=se||N;w?.includes(r)||g.push(`Observation skipped: ${r}`);const o=x(g.join(`
|
|
13
|
+
`),{verdict:"unknown",ledgerLines:g.length,timing:U(),...w?{error:"run_stopped"}:{},...se?{permissionRefused:!0}:{ownershipRefused:!0}});return o.content.push(...h,...A),o}try{if(j||P&&(!y.has(P)||!await $.driver.isActiveTarget(L.get(P).inner))){const R=$.driver.takeNoteForResult(),_=x(`${g.join(`
|
|
14
|
+
`)}${R?`
|
|
15
|
+
NOTE: ${R}.`:""}
|
|
16
|
+
Observation skipped: the bound target was released, closed or replaced. List surfaces and bind the intended target before continuing.`,{verdict:"unknown",ledgerLines:g.length,timing:U(),...w?{error:"run_stopped"}:{}});return _.content.push(...h,...A),_}const r=Date.now(),o=await $.driver.observeElements({prefix:g.join(`
|
|
17
|
+
`),...Pe});return o.details.ledgerLines=g.length,o.details.timing=U(Date.now()-r),Be(o,l),o.content.push(...h,...A),w&&(o.details.error="run_stopped"),o}catch(r){if(r.surfaceGone)throw r;const o=r.message,R=w&&w.includes(o)?"for the same reason":`${o} \u2014 observe again before the next action`,_=x(`${g.join(`
|
|
18
18
|
`)}
|
|
19
|
-
(re-observation failed: ${
|
|
19
|
+
(re-observation failed: ${R}.)`,{verdict:"unknown",ledgerLines:g.length,timing:U(),...w?{error:"run_stopped"}:{}});return _.content.push(...h,...A),_}}}),Me=W.execute.bind(W),ne=()=>{C&&clearTimeout(C),C=null},Ne=()=>{C=null,!J&&(B=(async()=>{try{for(const e of u)await e.driver.releaseInputResources();await b.releaseAll()}catch{}})().finally(()=>{B=null}))};W.execute=async e=>{ne(),B&&await B,J=!0;let n;oe=new Promise(t=>{n=t});try{return await Me(e)}finally{try{if(j){for(const t of u)await t.driver.releaseInputResources();await b.releaseAll(),j=!1}S=null;for(const t of u)t.driver.endCall(),t.driver.consentPurpose=null,t.driver.permissionRefusal=null}finally{J=!1,n(),ne(),C=setTimeout(Ne,Te),C.unref?.()}}};const je=ve({name:"computer_repl_reset",label:"Computer REPL reset",description:Ye,promptSnippet:"computer_repl_reset discards the persistent JavaScript bindings; surfaces are untouched.",parameters:f.Object({}),async execute(e){const n=oe;if(ne(),J)j=!0;else{for(const t of u)await t.driver.releaseInputResources();await b.releaseAll()}M=null,P=null,O.clear(),v.clear();for(const t of u)t.docsDue=!0;return z=!1,Y=!1,e?.waitForIdle&&await n,x("The JavaScript runtime was reset: every binding is gone. The next computer_repl call starts fresh and returns the API reference. No surface was touched \u2014 tabs, windows and apps are as they were.",{reset:!0})}});return[W,je]}export{ct as createReplTools};
|
package/dist/platform/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirasim/computer-use",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.5",
|
|
4
4
|
"description": "Computer use for any agent: an MCP server that drives browsers, macOS apps, Windows apps and Electron apps through their accessibility trees.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
"access": "public"
|
|
71
71
|
},
|
|
72
72
|
"optionalDependencies": {
|
|
73
|
-
"@mirasim/computer-use-darwin-arm64": "0.1.0-rc.
|
|
74
|
-
"@mirasim/computer-use-darwin-x64": "0.1.0-rc.
|
|
75
|
-
"@mirasim/computer-use-win32-x64": "0.1.0-rc.
|
|
76
|
-
"@mirasim/computer-use-win32-arm64": "0.1.0-rc.
|
|
73
|
+
"@mirasim/computer-use-darwin-arm64": "0.1.0-rc.5",
|
|
74
|
+
"@mirasim/computer-use-darwin-x64": "0.1.0-rc.5",
|
|
75
|
+
"@mirasim/computer-use-win32-x64": "0.1.0-rc.5",
|
|
76
|
+
"@mirasim/computer-use-win32-arm64": "0.1.0-rc.5"
|
|
77
77
|
}
|
|
78
78
|
}
|