@duckmind/dm-windows-x64 0.60.6 → 0.60.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dm.exe +0 -0
- package/extensions/.dm-extensions.json +70 -123
- package/extensions/dm-9router-ext/src/index.js +410 -5
- package/extensions/dm-caveman/extensions/caveman.js +283 -12
- package/extensions/dm-cliproxy/index.js +182 -2
- package/extensions/dm-cliproxy/scripts/check-config-migration.js +66 -8
- package/extensions/dm-cliproxy/src/apply.js +228 -1
- package/extensions/dm-cliproxy/src/cache.js +42 -1
- package/extensions/dm-cliproxy/src/commands.js +50 -2
- package/extensions/dm-cliproxy/src/compat.js +81 -1
- package/extensions/dm-cliproxy/src/config.js +192 -2
- package/extensions/dm-cliproxy/src/conflicts.js +46 -1
- package/extensions/dm-cliproxy/src/fetch-models.js +190 -1
- package/extensions/dm-cliproxy/src/fetch-usage.js +41 -1
- package/extensions/dm-cliproxy/src/log.js +23 -1
- package/extensions/dm-cliproxy/src/status-quota.js +77 -1
- package/extensions/dm-cliproxy/src/ui-frame.js +50 -1
- package/extensions/dm-cliproxy/src/ui-hub/hub.js +199 -2
- package/extensions/dm-cliproxy/src/ui-hub/index.js +26 -2
- package/extensions/dm-cliproxy/src/ui-hub/shell.js +46 -1
- package/extensions/dm-cliproxy/src/ui-hub/view-diagnostics.js +69 -1
- package/extensions/dm-cliproxy/src/ui-hub/view-models.js +379 -1
- package/extensions/dm-cliproxy/src/ui-hub/view-usage.js +106 -1
- package/extensions/dm-cliproxy/src/ui-picker/catalog.js +41 -1
- package/extensions/dm-cliproxy/src/ui-picker/mutate.js +115 -1
- package/extensions/dm-cliproxy/src/ui-picker/prompt-confirm.js +35 -1
- package/extensions/dm-cliproxy/src/ui-picker/prompt-name.js +62 -1
- package/extensions/dm-cliproxy/src/ui-picker/providers.js +45 -1
- package/extensions/dm-cliproxy/src/ui-picker/render-text.js +34 -1
- package/extensions/dm-cliproxy/src/ui-picker/rows.js +57 -1
- package/extensions/dm-cliproxy/src/ui-setup.js +260 -2
- package/extensions/dm-cliproxy/src/ui-usage.js +151 -1
- package/extensions/dm-cliproxy/src/usage-shared-cache.js +97 -1
- package/extensions/dm-context/src/context.js +144 -1
- package/extensions/dm-context/src/index.js +339 -7
- package/extensions/dm-context/src/utils.js +9 -1
- package/extensions/dm-cua/bin/browser-cua.mjs +73 -8
- package/extensions/dm-cua/index.js +75 -6
- package/extensions/dm-cua/src/browser-cua-lib.mjs +490 -6
- package/extensions/dm-cua/src/browser-install.mjs +331 -2
- package/extensions/dm-fff/src/index.js +688 -12
- package/extensions/dm-fff/src/query.js +60 -1
- package/extensions/dm-goal/src/goal.js +894 -23
- package/extensions/dm-image2/index.js +103 -9
- package/extensions/dm-image2/src/image-lib.mjs +1275 -8
- package/extensions/dm-subagents/install.mjs +62 -8
- package/extensions/dm-subagents/src/agents/agent-management.js +1190 -36
- package/extensions/dm-subagents/src/agents/agent-memory.js +216 -6
- package/extensions/dm-subagents/src/agents/agent-scope.js +5 -1
- package/extensions/dm-subagents/src/agents/agent-selection.js +20 -1
- package/extensions/dm-subagents/src/agents/agent-serializer.js +120 -5
- package/extensions/dm-subagents/src/agents/agents.js +1139 -11
- package/extensions/dm-subagents/src/agents/chain-serializer.js +299 -11
- package/extensions/dm-subagents/src/agents/frontmatter.js +65 -6
- package/extensions/dm-subagents/src/agents/identity.js +29 -1
- package/extensions/dm-subagents/src/agents/proactive-skills.js +141 -1
- package/extensions/dm-subagents/src/agents/skills.js +614 -6
- package/extensions/dm-subagents/src/extension/config.js +35 -2
- package/extensions/dm-subagents/src/extension/control-notices.js +69 -4
- package/extensions/dm-subagents/src/extension/doctor.js +172 -15
- package/extensions/dm-subagents/src/extension/fanout-child.js +158 -231
- package/extensions/dm-subagents/src/extension/index.js +521 -359
- package/extensions/dm-subagents/src/extension/rpc.js +266 -7
- package/extensions/dm-subagents/src/extension/schemas.js +275 -1
- package/extensions/dm-subagents/src/extension/tool-description.js +111 -6
- package/extensions/dm-subagents/src/intercom/intercom-bridge.js +126 -4
- package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +452 -5
- package/extensions/dm-subagents/src/intercom/result-intercom.js +319 -3
- package/extensions/dm-subagents/src/profiles/profiles.js +458 -3
- package/extensions/dm-subagents/src/runs/background/async-execution.js +834 -40
- package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +435 -14
- package/extensions/dm-subagents/src/runs/background/async-resume.js +334 -8
- package/extensions/dm-subagents/src/runs/background/async-status.js +313 -12
- package/extensions/dm-subagents/src/runs/background/chain-append.js +245 -2
- package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +136 -1
- package/extensions/dm-subagents/src/runs/background/completion-batcher.js +94 -1
- package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +54 -1
- package/extensions/dm-subagents/src/runs/background/control-channel.js +190 -1
- package/extensions/dm-subagents/src/runs/background/fleet-view.js +483 -17
- package/extensions/dm-subagents/src/runs/background/notify.js +129 -3
- package/extensions/dm-subagents/src/runs/background/parallel-groups.js +34 -1
- package/extensions/dm-subagents/src/runs/background/result-watcher.js +236 -6
- package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +76 -4
- package/extensions/dm-subagents/src/runs/background/run-status.js +427 -23
- package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +487 -4
- package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +306 -9
- package/extensions/dm-subagents/src/runs/background/subagent-runner.js +2849 -73
- package/extensions/dm-subagents/src/runs/background/top-level-async.js +5 -1
- package/extensions/dm-subagents/src/runs/background/wait.js +206 -11
- package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +1013 -12
- package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +980 -101
- package/extensions/dm-subagents/src/runs/foreground/execution.js +1165 -45
- package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +3157 -222
- package/extensions/dm-subagents/src/runs/shared/acceptance.js +835 -3
- package/extensions/dm-subagents/src/runs/shared/chain-outputs.js +104 -1
- package/extensions/dm-subagents/src/runs/shared/completion-guard.js +116 -3
- package/extensions/dm-subagents/src/runs/shared/dm-args.js +208 -1
- package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +90 -1
- package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +282 -1
- package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +148 -1
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +305 -1
- package/extensions/dm-subagents/src/runs/shared/model-fallback.js +194 -1
- package/extensions/dm-subagents/src/runs/shared/model-scope.js +65 -1
- package/extensions/dm-subagents/src/runs/shared/nested-events.js +851 -8
- package/extensions/dm-subagents/src/runs/shared/nested-path.js +41 -1
- package/extensions/dm-subagents/src/runs/shared/nested-render.js +105 -1
- package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +81 -4
- package/extensions/dm-subagents/src/runs/shared/run-history.js +51 -4
- package/extensions/dm-subagents/src/runs/shared/single-output.js +149 -8
- package/extensions/dm-subagents/src/runs/shared/structured-output.js +58 -1
- package/extensions/dm-subagents/src/runs/shared/subagent-control.js +166 -5
- package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +329 -13
- package/extensions/dm-subagents/src/runs/shared/tool-budget.js +73 -1
- package/extensions/dm-subagents/src/runs/shared/turn-budget.js +47 -4
- package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +196 -1
- package/extensions/dm-subagents/src/runs/shared/worktree.js +435 -3
- package/extensions/dm-subagents/src/shared/artifacts.js +92 -2
- package/extensions/dm-subagents/src/shared/atomic-json.js +55 -1
- package/extensions/dm-subagents/src/shared/child-transcript.js +167 -5
- package/extensions/dm-subagents/src/shared/file-coalescer.js +25 -1
- package/extensions/dm-subagents/src/shared/fork-context.js +147 -4
- package/extensions/dm-subagents/src/shared/formatters.js +98 -7
- package/extensions/dm-subagents/src/shared/jsonl-writer.js +56 -2
- package/extensions/dm-subagents/src/shared/model-info.js +62 -1
- package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.js +68 -1
- package/extensions/dm-subagents/src/shared/session-identity.js +6 -1
- package/extensions/dm-subagents/src/shared/session-tokens.js +39 -2
- package/extensions/dm-subagents/src/shared/settings.js +198 -11
- package/extensions/dm-subagents/src/shared/status-format.js +53 -1
- package/extensions/dm-subagents/src/shared/types.js +184 -6
- package/extensions/dm-subagents/src/shared/utils.js +462 -2
- package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +288 -1
- package/extensions/dm-subagents/src/slash/prompt-workflows.js +297 -7
- package/extensions/dm-subagents/src/slash/slash-bridge.js +118 -1
- package/extensions/dm-subagents/src/slash/slash-commands.js +1287 -31
- package/extensions/dm-subagents/src/slash/slash-live-state.js +240 -4
- package/extensions/dm-subagents/src/tui/render-helpers.js +64 -1
- package/extensions/dm-subagents/src/tui/render.js +1542 -4
- package/extensions/dm-usage/index.js +1294 -9
- package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +40 -9
- package/extensions/greedysearch-dm/bin/cdp-headless.mjs +5 -2
- package/extensions/greedysearch-dm/bin/cdp-visible.mjs +5 -2
- package/extensions/greedysearch-dm/bin/cdp.mjs +896 -30
- package/extensions/greedysearch-dm/bin/gschrome.mjs +30 -2
- package/extensions/greedysearch-dm/bin/kill-visible.mjs +7 -2
- package/extensions/greedysearch-dm/bin/launch-visible.mjs +13 -2
- package/extensions/greedysearch-dm/bin/launch.mjs +282 -10
- package/extensions/greedysearch-dm/bin/mcp.mjs +386 -361
- package/extensions/greedysearch-dm/bin/search.mjs +620 -540
- package/extensions/greedysearch-dm/bin/visible.mjs +22 -2
- package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +329 -579
- package/extensions/greedysearch-dm/extractors/chatgpt.mjs +301 -583
- package/extensions/greedysearch-dm/extractors/common.mjs +408 -32
- package/extensions/greedysearch-dm/extractors/consensus.mjs +376 -365
- package/extensions/greedysearch-dm/extractors/consent.mjs +303 -14
- package/extensions/greedysearch-dm/extractors/gemini.mjs +228 -592
- package/extensions/greedysearch-dm/extractors/google-ai.mjs +78 -499
- package/extensions/greedysearch-dm/extractors/logically.mjs +270 -347
- package/extensions/greedysearch-dm/extractors/perplexity.mjs +243 -581
- package/extensions/greedysearch-dm/extractors/selectors.mjs +32 -1
- package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +130 -317
- package/extensions/greedysearch-dm/index.js +123 -23
- package/extensions/greedysearch-dm/src/fetcher.mjs +576 -2
- package/extensions/greedysearch-dm/src/formatters/results.js +95 -10
- package/extensions/greedysearch-dm/src/formatters/sources.js +57 -1
- package/extensions/greedysearch-dm/src/formatters/synthesis.js +49 -1
- package/extensions/greedysearch-dm/src/github.mjs +222 -7
- package/extensions/greedysearch-dm/src/reddit.mjs +145 -14
- package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +340 -9
- package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +112 -4
- package/extensions/greedysearch-dm/src/search/chrome.mjs +486 -285
- package/extensions/greedysearch-dm/src/search/constants.mjs +109 -8
- package/extensions/greedysearch-dm/src/search/defaults.mjs +10 -1
- package/extensions/greedysearch-dm/src/search/engines.mjs +79 -9
- package/extensions/greedysearch-dm/src/search/fetch-source.mjs +441 -349
- package/extensions/greedysearch-dm/src/search/file-sources.mjs +29 -7
- package/extensions/greedysearch-dm/src/search/minimize.mjs +86 -1
- package/extensions/greedysearch-dm/src/search/output.mjs +51 -5
- package/extensions/greedysearch-dm/src/search/paths.mjs +48 -1
- package/extensions/greedysearch-dm/src/search/pdf.mjs +63 -2
- package/extensions/greedysearch-dm/src/search/port-pid.mjs +69 -1
- package/extensions/greedysearch-dm/src/search/progress.mjs +109 -2
- package/extensions/greedysearch-dm/src/search/query.mjs +21 -1
- package/extensions/greedysearch-dm/src/search/recovery.mjs +49 -1
- package/extensions/greedysearch-dm/src/search/research.mjs +2227 -458
- package/extensions/greedysearch-dm/src/search/scale-aware.mjs +61 -11
- package/extensions/greedysearch-dm/src/search/simple-research.mjs +396 -805
- package/extensions/greedysearch-dm/src/search/sources.mjs +412 -1
- package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +127 -12
- package/extensions/greedysearch-dm/src/search/synthesis.mjs +202 -12
- package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +209 -23
- package/extensions/greedysearch-dm/src/tools/shared.js +226 -10
- package/extensions/greedysearch-dm/src/utils/content.mjs +35 -4
- package/extensions/greedysearch-dm/src/utils/helpers.js +22 -1
- package/extensions/greedysearch-dm/src/utils/node-runtime.mjs +10 -1
- package/extensions/greedysearch-dm/src/utils/system-cmds.mjs +61 -1
- package/package.json +1 -1
|
@@ -1,6 +1,490 @@
|
|
|
1
|
-
import
|
|
2
|
-
${A.trim()}`:""}`})}),M.on("close",(h,$)=>{d({status:h===0?"ok":"failed",exitCode:h,message:A.trim()||($?`installer terminated by signal ${$}`:"")})}),w=setTimeout(()=>{M.kill(),d({status:"timeout",exitCode:null,message:`installer timed out after ${L}ms`})},L)})}function im(m){if(m.startsWith("homebrew-"))return"Homebrew";if(m.startsWith("winget-"))return"winget";return m.split("-")[0]}function hm(m){let o=m.message?`: ${m.message}`:"";return`${m.id} ${m.status}${o}`}async function b({env:m=process.env,platformName:o=H(),uid:i=typeof process.getuid==="function"?process.getuid():void 0,findExecutable:L=()=>v({env:m,platformName:o,homeDir:W()}),runCommand:T=(A,w)=>om(A,w,{env:m}),sleep:n=(A)=>new Promise((w)=>{setTimeout(w,A)})}={}){let A=L();if(A)return{status:"existing",path:A,method:null,attempts:[]};if(!e(m))throw Error("Chrome was not found and automatic installation is disabled by DM_CUA_AUTO_INSTALL.");let w=r({platformName:o,uid:i});if(w.length===0)throw Error(`Chrome was not found and no automatic Chrome installer is defined for ${o}.`);let M=[];for(let h of w){let $={status:"ok",exitCode:0,message:""};for(let D of h.commands)if($=await T(D.command,D.args),$.status!=="ok")break;if($.status!=="ok"){M.push({id:h.id,status:$.status,exitCode:$.exitCode,message:$.message});continue}let U;for(let D=0;D<K;D+=1){if(U=L(),U)break;if(D<K-1)await n(t)}if(U)return M.push({id:h.id,status:"installed",exitCode:$.exitCode,message:$.message}),{status:"installed",path:U,method:h.id,attempts:M};M.push({id:h.id,status:"missing-after-success",exitCode:$.exitCode,message:`${im(h.id)} reported success, but Chrome was not found`})}let d=M.map(hm).join("; ");throw Error(`Chrome automatic installation failed. ${d}`)}var q=N(wm(import.meta.url)),Bm=Y(q,".."),Mm=9322,Cm=400,j=60000,um=/^(about:|https?:\/\/|file:|chrome-error:\/\/)/i,fm={chrome:"Chrome",cloak:"CloakBrowser"};function O(m){let o=Number.parseInt(String(m??""),10);return Number.isInteger(o)&&o>1024&&o<65535?o:null}function Um(m){return dm("sha256").update(String(m||"default")).digest("hex")}function km(m){let o=String(m||"chrome").trim().toLowerCase();if(o==="cloakbrowser"||o==="cloak-browser")return"cloak";if(o==="chromium"||o==="google-chrome"||o==="chrome")return"chrome";return o.replace(/[^a-z0-9_.-]/g,"-")||"chrome"}function Xm(m,o){let i=m.DM_CUA_BROWSER_PATH||(o==="cloak"?m.DM_CUA_CLOAK_PATH||m.CLOAKBROWSER_BINARY_PATH:void 0)||m.CHROME_PATH;return i?Y(String(i)):void 0}function _m(m=process.env,o=process.cwd()){let i=m.DM_CUA_SCOPE||m.DM_CODING_AGENT_DIR||m.PI_CODING_AGENT_DIR||o,L=Um(Y(String(i||"."))),T=String(m.DM_CUA_INSTANCE_ID||L.slice(0,12)).replace(/[^a-zA-Z0-9_.-]/g,"-"),n=km(m.DM_CUA_BROWSER||m.DM_BROWSER_CUA_BROWSER||m.DM_CUA_BROWSER_KIND),A=m.DM_CUA_BROWSER_DISPLAY_NAME||fm[n]||n,w=Xm(m,n),M=O(m.DM_CUA_PORT)??O(m.GREEDY_SEARCH_PORT)??Mm+Number.parseInt(L.slice(0,8),16)%Cm,d=n==="chrome"?"chrome":n,h=Y(m.DM_CUA_PROFILE_DIR||f(c(),`dm-cua-${d}-profile-${T}`)),$=$m()==="win32"?c():"/tmp";return{instanceId:T,browserKind:n,browserDisplayName:A,browserExecutablePath:w,browserExecutableIsExplicit:Boolean(w),browserInstallation:void 0,port:M,profileDir:h,pidFile:f(h,"chrome.pid"),modeFile:f(h,"chrome-mode"),pagesCache:f(h,"cdp-pages.json"),socketDir:f($,`dm-cua-cdp-${T}`)}}var u=_m(),Ym=u.port,Sm=u.profileDir;function k(){return{kind:u.browserKind,displayName:u.browserDisplayName,executablePath:u.browserExecutablePath,installation:u.browserInstallation??null,profileDir:u.profileDir,port:u.port}}async function zm(m=u,{env:o=process.env,exists:i=V,ensureChrome:L=b}={}){if(m.browserKind==="cloak"&&!m.browserExecutablePath)throw Error(["CloakBrowser selected for browser CUA, but no executable path was provided.","Install/pre-download CloakBrowser yourself, then set DM_CUA_BROWSER_PATH, DM_CUA_CLOAK_PATH, CLOAKBROWSER_BINARY_PATH, or CHROME_PATH.","DM does not bundle the compiled CloakBrowser binary because its upstream license allows use but not redistribution."].join(" "));if(m.browserExecutablePath&&!i(m.browserExecutablePath))throw Error(`${m.browserDisplayName} executable not found: ${m.browserExecutablePath}`);if(m.browserExecutablePath)return m;if(m.browserKind!=="chrome")throw Error(`${m.browserDisplayName} browser CUA requires an explicit executable path via DM_CUA_BROWSER_PATH.`);let T=await L({env:o});return m.browserExecutablePath=T.path,m.browserInstallation={status:T.status,method:T.method,attempts:T.attempts.map(({id:n,status:A,exitCode:w})=>({id:n,status:A,exitCode:w}))},m}var Z;async function Jm(){if(!Z)Z=zm(u).catch((m)=>{throw Z=void 0,m});return Z}function B(){let o=[Y(q,"..","greedysearch-dm"),Y(q,"..","..","greedysearch-dm")].map((i)=>({launchScript:f(i,"bin","launch.mjs"),cdpScript:f(i,"bin","cdp.mjs")}));for(let i of o)if(V(i.launchScript)&&V(i.cdpScript))return i;return o[0]}function ym(m=u,o=process.env){let i=o.GREEDY_SEARCH_VISIBLE??o.DM_CUA_VISIBLE??o.DM_BROWSER_CUA_VISIBLE;return{...o,...m.browserExecutablePath?{CHROME_PATH:m.browserExecutablePath}:{},CDP_PROFILE_DIR:m.profileDir.replace(/\\/g,"/"),CDP_PAGES_CACHE:m.pagesCache.replace(/\\/g,"/"),CDP_SOCKET_DIR:m.socketDir.replace(/\\/g,"/"),GREEDY_SEARCH_BROWSER_LABEL:m.browserDisplayName,GREEDY_SEARCH_PORT:String(m.port),GREEDY_SEARCH_PROFILE_DIR:m.profileDir.replace(/\\/g,"/"),GREEDY_SEARCH_PID_FILE:m.pidFile.replace(/\\/g,"/"),GREEDY_SEARCH_MODE_FILE:m.modeFile.replace(/\\/g,"/"),GREEDY_SEARCH_ALLOW_PORT_CLEANUP:o.GREEDY_SEARCH_ALLOW_PORT_CLEANUP??"0",...i===void 0?{}:{GREEDY_SEARCH_VISIBLE:i}}}function s(){return ym(u,process.env)}function Dm(m){return um.test(m)}function Qm(m){return m.split(/\r?\n/).map((o)=>o.trimEnd()).filter(Boolean).map((o)=>{let i=o.trim().split(/\s{2,}/).filter(Boolean);if(i.length===0)return null;let L=i[0];if(i.length===1)return{target:L,title:"",url:""};if(i.length===2&&Dm(i[1]))return{target:L,title:"",url:i[1]};let T=i.at(-1)??"",n=i.slice(1,-1).join(" ");return{target:L,title:n,url:T}}).filter(Boolean)}function Vm(m){let o=String(m??"").trim();if(!o)throw Error("selector is required for action=click");if(/:contains\s*\(/i.test(o))throw Error(":contains(...) is not a standard CSS selector. Use browser_cua(snapshot) to inspect a standard selector, or browser_cua(evaluate) for text matching.");return o}function Zm(){let m=process.env.DM_CODING_AGENT_DIR||process.env.PI_CODING_AGENT_DIR;return m?Y(m):f(Lm(),".dm","agent")}function am(){let m=f(Zm(),"cua","screenshots");g(m,{recursive:!0});let o=new Date().toISOString().replace(/[:.]/g,"-");return f(m,`browser-${o}.png`)}function G(m,o,{env:i=process.env,timeoutMs:L=j}={}){return new Promise((T,n)=>{let A=nm("node",[m,...o],{env:i,stdio:["ignore","pipe","pipe"]}),w="",M="",d=!1,h=setTimeout(()=>{if(d)return;d=!0,A.kill("SIGTERM"),n(Error(`Timed out running ${m}`))},L);A.stdout.on("data",($)=>{w+=$.toString()}),A.stderr.on("data",($)=>{M+=$.toString()}),A.on("error",($)=>{if(d)return;d=!0,clearTimeout(h),n($)}),A.on("close",($)=>{if(d)return;if(d=!0,clearTimeout(h),$!==0){n(Error((M||w||`exit ${$}`).trim()));return}T({stdout:w,stderr:M})})})}function Hm(m,o){return new Promise((i,L)=>{let T=Tm.request({host:"127.0.0.1",port:Ym,path:o,method:m},(n)=>{let A="";n.on("data",(w)=>{A+=w.toString()}),n.on("end",()=>{if((n.statusCode??500)>=400){L(Error(`Browser CDP HTTP ${n.statusCode}: ${A.trim()}`));return}try{i(A?JSON.parse(A):{})}catch(w){L(w)}})});T.on("error",L),T.end()})}async function qm(m="about:blank"){return Hm("PUT",`/json/new?${encodeURIComponent(m)}`)}async function X(m,o={}){let{cdpScript:i}=B();return G(i,m,{env:s(),timeoutMs:o.timeoutMs??j})}async function I(){let m=await X(["list"]);return Qm(m.stdout)}function S(m,o){if(!o)return m[0]??null;let i=o.toUpperCase();return m.find((L)=>L.target.toUpperCase().startsWith(i))??null}async function l(){let{launchScript:m,cdpScript:o}=B();if(!V(m)||!V(o))throw Error("Bundled browser helpers are missing. Rebuild DM so dm-cua and greedysearch-dm are staged together.");return{launchScript:m,cdpScript:o}}async function R(){let{launchScript:m}=await l();await Jm(),await G(m,[],{env:s(),timeoutMs:j});let o=await I();if(o.length===0)await qm("about:blank"),o=await I();return o}async function y(m){let o=await R(),i=S(o,m);if(!i){if(m)throw Error(`No browser tab matching prefix "${m}". Run browser_cua(list) first.`);throw Error("No browser tab available after browser bootstrap.")}return{tab:i.target,page:i,pages:o}}async function lm({action:m,tab:o,url:i,selector:L,x:T,y:n,text:A,expression:w,outputPath:M}={}){switch(m){case"list":{let d=await R();return{status:"ok",action:m,browser:k(),pages:d}}case"navigate":{if(!i)throw Error("url is required for action=navigate");let d=await y(o),h=await X(["nav",d.tab,i],{timeoutMs:90000}),$=await I(),U=S($,d.tab)??d.page;return{status:"ok",action:m,browser:k(),tab:d.tab,url:U?.url||i,title:U?.title||"",message:h.stdout.trim(),pages:$}}case"snapshot":{let d=await y(o),h=await X(["snap",d.tab]);return{status:"ok",action:m,browser:k(),tab:d.tab,title:d.page?.title||"",url:d.page?.url||"",snapshot:h.stdout.trim()}}case"screenshot":{let d=await y(o),h=M?Y(M):am();g(N(h),{recursive:!0});let $=await X(["shot",d.tab,h],{timeoutMs:120000}),U=Am(h).toString("base64");return{status:"ok",action:m,browser:k(),tab:d.tab,title:d.page?.title||"",url:d.page?.url||"",imagePath:h,imageBase64:U,mimeType:"image/png",message:$.stdout.trim()}}case"click":{let d=Vm(L),h=await y(o),$=await X(["click",h.tab,d]);return{status:"ok",action:m,browser:k(),tab:h.tab,selector:d,message:$.stdout.trim()}}case"click_xy":{if(typeof T!=="number"||Number.isNaN(T)||typeof n!=="number"||Number.isNaN(n))throw Error("x and y are required numbers for action=click_xy");let d=await y(o),h=await X(["clickxy",d.tab,String(T),String(n)]);return{status:"ok",action:m,browser:k(),tab:d.tab,x:T,y:n,message:h.stdout.trim()}}case"type":{if(A==null||A==="")throw Error("text is required for action=type");let d=await y(o),h=await X(["type",d.tab,A]);return{status:"ok",action:m,browser:k(),tab:d.tab,textLength:A.length,message:h.stdout.trim()}}case"evaluate":{if(!w)throw Error("expression is required for action=evaluate");let d=await y(o),h=await X(["eval",d.tab,w]);return{status:"ok",action:m,browser:k(),tab:d.tab,value:h.stdout.trim()}}case"stop":{let{launchScript:d}=await l();try{await X(["stop"])}catch{}let h=await G(d,["--kill"],{env:s(),timeoutMs:30000});return{status:"ok",action:m,browser:k(),message:(h.stdout||"Stopped browser CUA lane.").trim()}}default:throw Error(`Unsupported browser action: ${m}`)}}function Rm(m){switch(m.action){case"list":{if(!Array.isArray(m.pages)||m.pages.length===0)return"Browser CUA is ready, but no pages are open.";return["Open browser tabs:",...m.pages.map((o)=>o.url?`${o.target} ${o.title||"(untitled)"} ${o.url}`:`${o.target} ${o.title||"(untitled)"}`)].join(`
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import { homedir, platform, tmpdir } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { ensureChromeAvailable } from "./browser-install.mjs";
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const ROOT_DIR = resolve(__dirname, "..");
|
|
11
|
+
const DEFAULT_CUA_PORT_BASE = 9322;
|
|
12
|
+
const DEFAULT_CUA_PORT_RANGE = 400;
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
14
|
+
const URLISH_PATTERN = /^(about:|https?:\/\/|file:|chrome-error:\/\/)/i;
|
|
15
|
+
const BROWSER_DISPLAY_NAMES = {
|
|
16
|
+
chrome: "Chrome",
|
|
17
|
+
cloak: "CloakBrowser"
|
|
18
|
+
};
|
|
19
|
+
function parsePort(value) {
|
|
20
|
+
const port = Number.parseInt(String(value ?? ""), 10);
|
|
21
|
+
return Number.isInteger(port) && port > 1024 && port < 65535 ? port : null;
|
|
22
|
+
}
|
|
23
|
+
function hashScope(value) {
|
|
24
|
+
return createHash("sha256").update(String(value || "default")).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
function normalizeBrowserKind(value) {
|
|
27
|
+
const raw = String(value || "chrome").trim().toLowerCase();
|
|
28
|
+
if (raw === "cloakbrowser" || raw === "cloak-browser")
|
|
29
|
+
return "cloak";
|
|
30
|
+
if (raw === "chromium" || raw === "google-chrome" || raw === "chrome")
|
|
31
|
+
return "chrome";
|
|
32
|
+
return raw.replace(/[^a-z0-9_.-]/g, "-") || "chrome";
|
|
33
|
+
}
|
|
34
|
+
function resolveBrowserExecutablePath(env, browserKind) {
|
|
35
|
+
const explicit = env.DM_CUA_BROWSER_PATH || (browserKind === "cloak" ? env.DM_CUA_CLOAK_PATH || env.CLOAKBROWSER_BINARY_PATH : undefined) || env.CHROME_PATH;
|
|
36
|
+
return explicit ? resolve(String(explicit)) : undefined;
|
|
37
|
+
}
|
|
38
|
+
export function resolveBrowserRuntimeConfig(env = process.env, scope = process.cwd()) {
|
|
39
|
+
const rawScope = env.DM_CUA_SCOPE || env.DM_CODING_AGENT_DIR || env.PI_CODING_AGENT_DIR || scope;
|
|
40
|
+
const scopeHash = hashScope(resolve(String(rawScope || ".")));
|
|
41
|
+
const instanceId = String(env.DM_CUA_INSTANCE_ID || scopeHash.slice(0, 12)).replace(/[^a-zA-Z0-9_.-]/g, "-");
|
|
42
|
+
const browserKind = normalizeBrowserKind(env.DM_CUA_BROWSER || env.DM_BROWSER_CUA_BROWSER || env.DM_CUA_BROWSER_KIND);
|
|
43
|
+
const browserDisplayName = env.DM_CUA_BROWSER_DISPLAY_NAME || BROWSER_DISPLAY_NAMES[browserKind] || browserKind;
|
|
44
|
+
const browserExecutablePath = resolveBrowserExecutablePath(env, browserKind);
|
|
45
|
+
const port = parsePort(env.DM_CUA_PORT) ?? parsePort(env.GREEDY_SEARCH_PORT) ?? DEFAULT_CUA_PORT_BASE + Number.parseInt(scopeHash.slice(0, 8), 16) % DEFAULT_CUA_PORT_RANGE;
|
|
46
|
+
const profileSuffix = browserKind === "chrome" ? "chrome" : browserKind;
|
|
47
|
+
const profileDir = resolve(env.DM_CUA_PROFILE_DIR || join(tmpdir(), `dm-cua-${profileSuffix}-profile-${instanceId}`));
|
|
48
|
+
const socketBaseDir = platform() === "win32" ? tmpdir() : "/tmp";
|
|
49
|
+
return {
|
|
50
|
+
instanceId,
|
|
51
|
+
browserKind,
|
|
52
|
+
browserDisplayName,
|
|
53
|
+
browserExecutablePath,
|
|
54
|
+
browserExecutableIsExplicit: Boolean(browserExecutablePath),
|
|
55
|
+
browserInstallation: undefined,
|
|
56
|
+
port,
|
|
57
|
+
profileDir,
|
|
58
|
+
pidFile: join(profileDir, "chrome.pid"),
|
|
59
|
+
modeFile: join(profileDir, "chrome-mode"),
|
|
60
|
+
pagesCache: join(profileDir, "cdp-pages.json"),
|
|
61
|
+
socketDir: join(socketBaseDir, `dm-cua-cdp-${instanceId}`)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const BROWSER_CONFIG = resolveBrowserRuntimeConfig();
|
|
65
|
+
const CHROME_PORT = BROWSER_CONFIG.port;
|
|
66
|
+
export const BROWSER_PROFILE_DIR = BROWSER_CONFIG.profileDir;
|
|
67
|
+
function browserSummary() {
|
|
68
|
+
return {
|
|
69
|
+
kind: BROWSER_CONFIG.browserKind,
|
|
70
|
+
displayName: BROWSER_CONFIG.browserDisplayName,
|
|
71
|
+
executablePath: BROWSER_CONFIG.browserExecutablePath,
|
|
72
|
+
installation: BROWSER_CONFIG.browserInstallation ?? null,
|
|
73
|
+
profileDir: BROWSER_CONFIG.profileDir,
|
|
74
|
+
port: BROWSER_CONFIG.port
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function ensureBrowserSelectionUsable(config = BROWSER_CONFIG, {
|
|
78
|
+
env = process.env,
|
|
79
|
+
exists = existsSync,
|
|
80
|
+
ensureChrome = ensureChromeAvailable
|
|
81
|
+
} = {}) {
|
|
82
|
+
if (config.browserKind === "cloak" && !config.browserExecutablePath) {
|
|
83
|
+
throw new Error([
|
|
84
|
+
"CloakBrowser selected for browser CUA, but no executable path was provided.",
|
|
85
|
+
"Install/pre-download CloakBrowser yourself, then set DM_CUA_BROWSER_PATH, DM_CUA_CLOAK_PATH, CLOAKBROWSER_BINARY_PATH, or CHROME_PATH.",
|
|
86
|
+
"DM does not bundle the compiled CloakBrowser binary because its upstream license allows use but not redistribution."
|
|
87
|
+
].join(" "));
|
|
88
|
+
}
|
|
89
|
+
if (config.browserExecutablePath && !exists(config.browserExecutablePath)) {
|
|
90
|
+
throw new Error(`${config.browserDisplayName} executable not found: ${config.browserExecutablePath}`);
|
|
91
|
+
}
|
|
92
|
+
if (config.browserExecutablePath)
|
|
93
|
+
return config;
|
|
94
|
+
if (config.browserKind !== "chrome") {
|
|
95
|
+
throw new Error(`${config.browserDisplayName} browser CUA requires an explicit executable path via DM_CUA_BROWSER_PATH.`);
|
|
96
|
+
}
|
|
97
|
+
const selection = await ensureChrome({ env });
|
|
98
|
+
config.browserExecutablePath = selection.path;
|
|
99
|
+
config.browserInstallation = {
|
|
100
|
+
status: selection.status,
|
|
101
|
+
method: selection.method,
|
|
102
|
+
attempts: selection.attempts.map(({ id, status, exitCode }) => ({
|
|
103
|
+
id,
|
|
104
|
+
status,
|
|
105
|
+
exitCode
|
|
106
|
+
}))
|
|
107
|
+
};
|
|
108
|
+
return config;
|
|
109
|
+
}
|
|
110
|
+
let browserSelectionPromise;
|
|
111
|
+
async function ensureConfiguredBrowserSelectionUsable() {
|
|
112
|
+
if (!browserSelectionPromise) {
|
|
113
|
+
browserSelectionPromise = ensureBrowserSelectionUsable(BROWSER_CONFIG).catch((error) => {
|
|
114
|
+
browserSelectionPromise = undefined;
|
|
115
|
+
throw error;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return browserSelectionPromise;
|
|
119
|
+
}
|
|
120
|
+
export function resolveBrowserHelperPaths() {
|
|
121
|
+
const helperRoots = [
|
|
122
|
+
resolve(__dirname, "..", "greedysearch-dm"),
|
|
123
|
+
resolve(__dirname, "..", "..", "greedysearch-dm")
|
|
124
|
+
];
|
|
125
|
+
const candidates = helperRoots.map((root) => ({
|
|
126
|
+
launchScript: join(root, "bin", "launch.mjs"),
|
|
127
|
+
cdpScript: join(root, "bin", "cdp.mjs")
|
|
128
|
+
}));
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
if (existsSync(candidate.launchScript) && existsSync(candidate.cdpScript)) {
|
|
131
|
+
return candidate;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return candidates[0];
|
|
135
|
+
}
|
|
136
|
+
export function resolveBrowserProcessEnv(config = BROWSER_CONFIG, env = process.env) {
|
|
137
|
+
const visible = env.GREEDY_SEARCH_VISIBLE ?? env.DM_CUA_VISIBLE ?? env.DM_BROWSER_CUA_VISIBLE;
|
|
138
|
+
return {
|
|
139
|
+
...env,
|
|
140
|
+
...config.browserExecutablePath ? { CHROME_PATH: config.browserExecutablePath } : {},
|
|
141
|
+
CDP_PROFILE_DIR: config.profileDir.replace(/\\/g, "/"),
|
|
142
|
+
CDP_PAGES_CACHE: config.pagesCache.replace(/\\/g, "/"),
|
|
143
|
+
CDP_SOCKET_DIR: config.socketDir.replace(/\\/g, "/"),
|
|
144
|
+
GREEDY_SEARCH_BROWSER_LABEL: config.browserDisplayName,
|
|
145
|
+
GREEDY_SEARCH_PORT: String(config.port),
|
|
146
|
+
GREEDY_SEARCH_PROFILE_DIR: config.profileDir.replace(/\\/g, "/"),
|
|
147
|
+
GREEDY_SEARCH_PID_FILE: config.pidFile.replace(/\\/g, "/"),
|
|
148
|
+
GREEDY_SEARCH_MODE_FILE: config.modeFile.replace(/\\/g, "/"),
|
|
149
|
+
GREEDY_SEARCH_ALLOW_PORT_CLEANUP: env.GREEDY_SEARCH_ALLOW_PORT_CLEANUP ?? "0",
|
|
150
|
+
...visible === undefined ? {} : { GREEDY_SEARCH_VISIBLE: visible }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function browserEnv() {
|
|
154
|
+
return resolveBrowserProcessEnv(BROWSER_CONFIG, process.env);
|
|
155
|
+
}
|
|
156
|
+
function looksLikeUrl(value) {
|
|
157
|
+
return URLISH_PATTERN.test(value);
|
|
158
|
+
}
|
|
159
|
+
export function parseListOutput(output) {
|
|
160
|
+
return output.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean).map((line) => {
|
|
161
|
+
const parts = line.trim().split(/\s{2,}/).filter(Boolean);
|
|
162
|
+
if (parts.length === 0) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const target = parts[0];
|
|
166
|
+
if (parts.length === 1) {
|
|
167
|
+
return { target, title: "", url: "" };
|
|
168
|
+
}
|
|
169
|
+
if (parts.length === 2 && looksLikeUrl(parts[1])) {
|
|
170
|
+
return { target, title: "", url: parts[1] };
|
|
171
|
+
}
|
|
172
|
+
const url = parts.at(-1) ?? "";
|
|
173
|
+
const title = parts.slice(1, -1).join(" ");
|
|
174
|
+
return { target, title, url };
|
|
175
|
+
}).filter(Boolean);
|
|
176
|
+
}
|
|
177
|
+
export function validateBrowserSelector(selector) {
|
|
178
|
+
const value = String(selector ?? "").trim();
|
|
179
|
+
if (!value) {
|
|
180
|
+
throw new Error("selector is required for action=click");
|
|
181
|
+
}
|
|
182
|
+
if (/:contains\s*\(/i.test(value)) {
|
|
183
|
+
throw new Error(":contains(...) is not a standard CSS selector. Use browser_cua(snapshot) to inspect a standard selector, or browser_cua(evaluate) for text matching.");
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
function resolveAgentDir() {
|
|
188
|
+
const explicit = process.env.DM_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR;
|
|
189
|
+
return explicit ? resolve(explicit) : join(homedir(), ".dm", "agent");
|
|
190
|
+
}
|
|
191
|
+
function defaultScreenshotPath() {
|
|
192
|
+
const dir = join(resolveAgentDir(), "cua", "screenshots");
|
|
193
|
+
mkdirSync(dir, { recursive: true });
|
|
194
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
195
|
+
return join(dir, `browser-${stamp}.png`);
|
|
196
|
+
}
|
|
197
|
+
function runNodeScript(scriptPath, args, { env = process.env, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
198
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
199
|
+
const child = spawn("node", [scriptPath, ...args], {
|
|
200
|
+
env,
|
|
201
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
202
|
+
});
|
|
203
|
+
let stdout = "";
|
|
204
|
+
let stderr = "";
|
|
205
|
+
let settled = false;
|
|
206
|
+
const timeout = setTimeout(() => {
|
|
207
|
+
if (settled)
|
|
208
|
+
return;
|
|
209
|
+
settled = true;
|
|
210
|
+
child.kill("SIGTERM");
|
|
211
|
+
rejectPromise(new Error(`Timed out running ${scriptPath}`));
|
|
212
|
+
}, timeoutMs);
|
|
213
|
+
child.stdout.on("data", (chunk) => {
|
|
214
|
+
stdout += chunk.toString();
|
|
215
|
+
});
|
|
216
|
+
child.stderr.on("data", (chunk) => {
|
|
217
|
+
stderr += chunk.toString();
|
|
218
|
+
});
|
|
219
|
+
child.on("error", (error) => {
|
|
220
|
+
if (settled)
|
|
221
|
+
return;
|
|
222
|
+
settled = true;
|
|
223
|
+
clearTimeout(timeout);
|
|
224
|
+
rejectPromise(error);
|
|
225
|
+
});
|
|
226
|
+
child.on("close", (code) => {
|
|
227
|
+
if (settled)
|
|
228
|
+
return;
|
|
229
|
+
settled = true;
|
|
230
|
+
clearTimeout(timeout);
|
|
231
|
+
if (code !== 0) {
|
|
232
|
+
rejectPromise(new Error((stderr || stdout || `exit ${code}`).trim()));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
resolvePromise({ stdout, stderr });
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function requestJson(method, requestPath) {
|
|
240
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
241
|
+
const req = http.request({
|
|
242
|
+
host: "127.0.0.1",
|
|
243
|
+
port: CHROME_PORT,
|
|
244
|
+
path: requestPath,
|
|
245
|
+
method
|
|
246
|
+
}, (response) => {
|
|
247
|
+
let body = "";
|
|
248
|
+
response.on("data", (chunk) => {
|
|
249
|
+
body += chunk.toString();
|
|
250
|
+
});
|
|
251
|
+
response.on("end", () => {
|
|
252
|
+
if ((response.statusCode ?? 500) >= 400) {
|
|
253
|
+
rejectPromise(new Error(`Browser CDP HTTP ${response.statusCode}: ${body.trim()}`));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
resolvePromise(body ? JSON.parse(body) : {});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
rejectPromise(error);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
req.on("error", rejectPromise);
|
|
264
|
+
req.end();
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async function createBlankTarget(url = "about:blank") {
|
|
268
|
+
return requestJson("PUT", `/json/new?${encodeURIComponent(url)}`);
|
|
269
|
+
}
|
|
270
|
+
async function runCdp(args, options = {}) {
|
|
271
|
+
const { cdpScript } = resolveBrowserHelperPaths();
|
|
272
|
+
return runNodeScript(cdpScript, args, { env: browserEnv(), timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS });
|
|
273
|
+
}
|
|
274
|
+
async function listPages() {
|
|
275
|
+
const result = await runCdp(["list"]);
|
|
276
|
+
return parseListOutput(result.stdout);
|
|
277
|
+
}
|
|
278
|
+
function findPage(pages, prefix) {
|
|
279
|
+
if (!prefix)
|
|
280
|
+
return pages[0] ?? null;
|
|
281
|
+
const upper = prefix.toUpperCase();
|
|
282
|
+
return pages.find((page) => page.target.toUpperCase().startsWith(upper)) ?? null;
|
|
283
|
+
}
|
|
284
|
+
async function ensureBrowserHelpersExist() {
|
|
285
|
+
const { launchScript, cdpScript } = resolveBrowserHelperPaths();
|
|
286
|
+
if (!existsSync(launchScript) || !existsSync(cdpScript)) {
|
|
287
|
+
throw new Error("Bundled browser helpers are missing. Rebuild DM so dm-cua and greedysearch-dm are staged together.");
|
|
288
|
+
}
|
|
289
|
+
return { launchScript, cdpScript };
|
|
290
|
+
}
|
|
291
|
+
export async function ensureBrowserReady() {
|
|
292
|
+
const { launchScript } = await ensureBrowserHelpersExist();
|
|
293
|
+
await ensureConfiguredBrowserSelectionUsable();
|
|
294
|
+
await runNodeScript(launchScript, [], { env: browserEnv(), timeoutMs: DEFAULT_TIMEOUT_MS });
|
|
295
|
+
let pages = await listPages();
|
|
296
|
+
if (pages.length === 0) {
|
|
297
|
+
await createBlankTarget("about:blank");
|
|
298
|
+
pages = await listPages();
|
|
299
|
+
}
|
|
300
|
+
return pages;
|
|
301
|
+
}
|
|
302
|
+
async function resolveTabContext(tabPrefix) {
|
|
303
|
+
const pages = await ensureBrowserReady();
|
|
304
|
+
const page = findPage(pages, tabPrefix);
|
|
305
|
+
if (!page) {
|
|
306
|
+
if (tabPrefix) {
|
|
307
|
+
throw new Error(`No browser tab matching prefix "${tabPrefix}". Run browser_cua(list) first.`);
|
|
308
|
+
}
|
|
309
|
+
throw new Error("No browser tab available after browser bootstrap.");
|
|
310
|
+
}
|
|
311
|
+
return { tab: page.target, page, pages };
|
|
312
|
+
}
|
|
313
|
+
export async function runBrowserAction({
|
|
314
|
+
action,
|
|
315
|
+
tab,
|
|
316
|
+
url,
|
|
317
|
+
selector,
|
|
318
|
+
x,
|
|
319
|
+
y,
|
|
320
|
+
text,
|
|
321
|
+
expression,
|
|
322
|
+
outputPath
|
|
323
|
+
} = {}) {
|
|
324
|
+
switch (action) {
|
|
325
|
+
case "list": {
|
|
326
|
+
const pages = await ensureBrowserReady();
|
|
327
|
+
return { status: "ok", action, browser: browserSummary(), pages };
|
|
328
|
+
}
|
|
329
|
+
case "navigate": {
|
|
330
|
+
if (!url)
|
|
331
|
+
throw new Error("url is required for action=navigate");
|
|
332
|
+
const context = await resolveTabContext(tab);
|
|
333
|
+
const result = await runCdp(["nav", context.tab, url], { timeoutMs: 90000 });
|
|
334
|
+
const pages = await listPages();
|
|
335
|
+
const page = findPage(pages, context.tab) ?? context.page;
|
|
336
|
+
return {
|
|
337
|
+
status: "ok",
|
|
338
|
+
action,
|
|
339
|
+
browser: browserSummary(),
|
|
340
|
+
tab: context.tab,
|
|
341
|
+
url: page?.url || url,
|
|
342
|
+
title: page?.title || "",
|
|
343
|
+
message: result.stdout.trim(),
|
|
344
|
+
pages
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
case "snapshot": {
|
|
348
|
+
const context = await resolveTabContext(tab);
|
|
349
|
+
const result = await runCdp(["snap", context.tab]);
|
|
350
|
+
return {
|
|
351
|
+
status: "ok",
|
|
352
|
+
action,
|
|
353
|
+
browser: browserSummary(),
|
|
354
|
+
tab: context.tab,
|
|
355
|
+
title: context.page?.title || "",
|
|
356
|
+
url: context.page?.url || "",
|
|
357
|
+
snapshot: result.stdout.trim()
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
case "screenshot": {
|
|
361
|
+
const context = await resolveTabContext(tab);
|
|
362
|
+
const imagePath = outputPath ? resolve(outputPath) : defaultScreenshotPath();
|
|
363
|
+
mkdirSync(dirname(imagePath), { recursive: true });
|
|
364
|
+
const result = await runCdp(["shot", context.tab, imagePath], { timeoutMs: 120000 });
|
|
365
|
+
const imageBase64 = readFileSync(imagePath).toString("base64");
|
|
366
|
+
return {
|
|
367
|
+
status: "ok",
|
|
368
|
+
action,
|
|
369
|
+
browser: browserSummary(),
|
|
370
|
+
tab: context.tab,
|
|
371
|
+
title: context.page?.title || "",
|
|
372
|
+
url: context.page?.url || "",
|
|
373
|
+
imagePath,
|
|
374
|
+
imageBase64,
|
|
375
|
+
mimeType: "image/png",
|
|
376
|
+
message: result.stdout.trim()
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
case "click": {
|
|
380
|
+
const safeSelector = validateBrowserSelector(selector);
|
|
381
|
+
const context = await resolveTabContext(tab);
|
|
382
|
+
const result = await runCdp(["click", context.tab, safeSelector]);
|
|
383
|
+
return {
|
|
384
|
+
status: "ok",
|
|
385
|
+
action,
|
|
386
|
+
browser: browserSummary(),
|
|
387
|
+
tab: context.tab,
|
|
388
|
+
selector: safeSelector,
|
|
389
|
+
message: result.stdout.trim()
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
case "click_xy": {
|
|
393
|
+
if (typeof x !== "number" || Number.isNaN(x) || typeof y !== "number" || Number.isNaN(y)) {
|
|
394
|
+
throw new Error("x and y are required numbers for action=click_xy");
|
|
395
|
+
}
|
|
396
|
+
const context = await resolveTabContext(tab);
|
|
397
|
+
const result = await runCdp(["clickxy", context.tab, String(x), String(y)]);
|
|
398
|
+
return {
|
|
399
|
+
status: "ok",
|
|
400
|
+
action,
|
|
401
|
+
browser: browserSummary(),
|
|
402
|
+
tab: context.tab,
|
|
403
|
+
x,
|
|
404
|
+
y,
|
|
405
|
+
message: result.stdout.trim()
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
case "type": {
|
|
409
|
+
if (text == null || text === "")
|
|
410
|
+
throw new Error("text is required for action=type");
|
|
411
|
+
const context = await resolveTabContext(tab);
|
|
412
|
+
const result = await runCdp(["type", context.tab, text]);
|
|
413
|
+
return {
|
|
414
|
+
status: "ok",
|
|
415
|
+
action,
|
|
416
|
+
browser: browserSummary(),
|
|
417
|
+
tab: context.tab,
|
|
418
|
+
textLength: text.length,
|
|
419
|
+
message: result.stdout.trim()
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
case "evaluate": {
|
|
423
|
+
if (!expression)
|
|
424
|
+
throw new Error("expression is required for action=evaluate");
|
|
425
|
+
const context = await resolveTabContext(tab);
|
|
426
|
+
const result = await runCdp(["eval", context.tab, expression]);
|
|
427
|
+
return {
|
|
428
|
+
status: "ok",
|
|
429
|
+
action,
|
|
430
|
+
browser: browserSummary(),
|
|
431
|
+
tab: context.tab,
|
|
432
|
+
value: result.stdout.trim()
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
case "stop": {
|
|
436
|
+
const { launchScript } = await ensureBrowserHelpersExist();
|
|
437
|
+
try {
|
|
438
|
+
await runCdp(["stop"]);
|
|
439
|
+
} catch {}
|
|
440
|
+
const result = await runNodeScript(launchScript, ["--kill"], { env: browserEnv(), timeoutMs: 30000 });
|
|
441
|
+
return {
|
|
442
|
+
status: "ok",
|
|
443
|
+
action,
|
|
444
|
+
browser: browserSummary(),
|
|
445
|
+
message: (result.stdout || "Stopped browser CUA lane.").trim()
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
default:
|
|
449
|
+
throw new Error(`Unsupported browser action: ${action}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
export function renderBrowserActionText(result) {
|
|
453
|
+
switch (result.action) {
|
|
454
|
+
case "list": {
|
|
455
|
+
if (!Array.isArray(result.pages) || result.pages.length === 0) {
|
|
456
|
+
return "Browser CUA is ready, but no pages are open.";
|
|
457
|
+
}
|
|
458
|
+
return [
|
|
459
|
+
"Open browser tabs:",
|
|
460
|
+
...result.pages.map((page) => page.url ? `${page.target} ${page.title || "(untitled)"} ${page.url}` : `${page.target} ${page.title || "(untitled)"}`)
|
|
461
|
+
].join(`
|
|
462
|
+
`);
|
|
463
|
+
}
|
|
464
|
+
case "navigate":
|
|
465
|
+
return [`Navigated browser tab ${result.tab} to ${result.url || "(unknown URL)"}`, result.title ? `Title: ${result.title}` : ""].filter(Boolean).join(`
|
|
466
|
+
`);
|
|
467
|
+
case "snapshot":
|
|
468
|
+
return [`Browser snapshot for ${result.tab}`, result.snapshot || "(empty snapshot)"].join(`
|
|
469
|
+
`);
|
|
470
|
+
case "screenshot":
|
|
471
|
+
return [
|
|
472
|
+
`Saved browser screenshot for ${result.tab} at ${result.imagePath}`,
|
|
473
|
+
result.title ? `Title: ${result.title}` : "",
|
|
474
|
+
result.url ? `URL: ${result.url}` : ""
|
|
475
|
+
].filter(Boolean).join(`
|
|
476
|
+
`);
|
|
477
|
+
case "click":
|
|
478
|
+
return `Clicked selector ${JSON.stringify(result.selector)} on browser tab ${result.tab}`;
|
|
479
|
+
case "click_xy":
|
|
480
|
+
return `Clicked browser tab ${result.tab} at CSS coordinates (${result.x}, ${result.y})`;
|
|
481
|
+
case "type":
|
|
482
|
+
return `Typed ${result.textLength} characters into browser tab ${result.tab}`;
|
|
483
|
+
case "evaluate":
|
|
484
|
+
return `Browser evaluate result for ${result.tab}: ${result.value}`;
|
|
485
|
+
case "stop":
|
|
486
|
+
return result.message || "Stopped browser CUA lane.";
|
|
487
|
+
default:
|
|
488
|
+
return JSON.stringify(result, null, 2);
|
|
489
|
+
}
|
|
490
|
+
}
|