openmausbot 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (499) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +14 -0
  3. package/cli.js +2 -0
  4. package/dist/app-icon.svg +29 -0
  5. package/dist/assets/abap-BdImnpbu.js +1 -0
  6. package/dist/assets/actionscript-3-B3316cI-.js +1 -0
  7. package/dist/assets/ada-bCR0ucgS.js +1 -0
  8. package/dist/assets/ahk-CsyLZFj1.js +1 -0
  9. package/dist/assets/ahk2-8Zs4aa1G.js +1 -0
  10. package/dist/assets/andromeeda-C4gqWexZ.js +1 -0
  11. package/dist/assets/angular-html-DA-rfuFy.js +1 -0
  12. package/dist/assets/angular-ts-BrjP3tb8.js +1 -0
  13. package/dist/assets/apache-Pmp26Uib.js +1 -0
  14. package/dist/assets/apex-DhZFqWV2.js +1 -0
  15. package/dist/assets/apl-CORt7UWP.js +1 -0
  16. package/dist/assets/applescript-Co6uUVPk.js +1 -0
  17. package/dist/assets/ara-BRHolxvo.js +1 -0
  18. package/dist/assets/asciidoc-CSVQ5wI8.js +1 -0
  19. package/dist/assets/asm-D_Q5rh1f.js +1 -0
  20. package/dist/assets/astro-HNnZUWAn.js +1 -0
  21. package/dist/assets/aurora-x-D-2ljcwZ.js +1 -0
  22. package/dist/assets/awk-DMzUqQB5.js +1 -0
  23. package/dist/assets/ayu-dark-DYE7WIF3.js +1 -0
  24. package/dist/assets/ayu-light-BA47KaF1.js +1 -0
  25. package/dist/assets/ayu-mirage-32ctXXKs.js +1 -0
  26. package/dist/assets/ballerina-BFfxhgS-.js +1 -0
  27. package/dist/assets/bat-CickPsom.js +1 -0
  28. package/dist/assets/beancount-k_qm7-4y.js +1 -0
  29. package/dist/assets/berry-uYugtg8r.js +1 -0
  30. package/dist/assets/bibtex-CHM0blh-.js +1 -0
  31. package/dist/assets/bicep-Bmn6On1c.js +1 -0
  32. package/dist/assets/bird2-Bx8U0n9b.js +1 -0
  33. package/dist/assets/blade-2xfisSek.js +1 -0
  34. package/dist/assets/bsl-DlhNcFeZ.js +1 -0
  35. package/dist/assets/c-BIGW1oBm.js +1 -0
  36. package/dist/assets/c3-Dp5svz6Z.js +1 -0
  37. package/dist/assets/cadence-Bv_4Rxtq.js +1 -0
  38. package/dist/assets/cairo-KRGpt6FW.js +1 -0
  39. package/dist/assets/catppuccin-frappe-CZL1YF0i.js +1 -0
  40. package/dist/assets/catppuccin-latte-DH-8KZSZ.js +1 -0
  41. package/dist/assets/catppuccin-macchiato-B7yYVSCf.js +1 -0
  42. package/dist/assets/catppuccin-mocha-Ct7hS0mc.js +1 -0
  43. package/dist/assets/chapel-DTp_pixX.js +1 -0
  44. package/dist/assets/clarity-Dn5IMItf.js +1 -0
  45. package/dist/assets/clojure-P80f7IUj.js +1 -0
  46. package/dist/assets/cmake-D1j8_8rp.js +1 -0
  47. package/dist/assets/cobol-nBiQ_Alo.js +1 -0
  48. package/dist/assets/codeowners-Bp6g37R7.js +1 -0
  49. package/dist/assets/codeql-DsOJ9woJ.js +1 -0
  50. package/dist/assets/coffee-Ch7k5sss.js +1 -0
  51. package/dist/assets/common-lisp-Cg-RD9OK.js +1 -0
  52. package/dist/assets/coq-C7JzOVbR.js +1 -0
  53. package/dist/assets/cpp-BMRokrvK.js +1 -0
  54. package/dist/assets/crystal-DGywbUpC.js +1 -0
  55. package/dist/assets/csharp-DSvCPggb.js +1 -0
  56. package/dist/assets/css-CLj8gQPS.js +1 -0
  57. package/dist/assets/csv-fuZLfV_i.js +1 -0
  58. package/dist/assets/cue-D82EKSYY.js +1 -0
  59. package/dist/assets/cypher-COkxafJQ.js +1 -0
  60. package/dist/assets/d-85-TOEBH.js +1 -0
  61. package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
  62. package/dist/assets/dart-bE4Kk8sk.js +1 -0
  63. package/dist/assets/dax-CEL-wOlO.js +1 -0
  64. package/dist/assets/desktop-BmXAJ9_W.js +1 -0
  65. package/dist/assets/diff-D97Zzqfu.js +1 -0
  66. package/dist/assets/docker-BcOcwvcX.js +1 -0
  67. package/dist/assets/dotenv-Da5cRb03.js +1 -0
  68. package/dist/assets/dracula-BzJJZx-M.js +1 -0
  69. package/dist/assets/dracula-soft-BXkSAIEj.js +1 -0
  70. package/dist/assets/dream-maker-BtqSS_iP.js +1 -0
  71. package/dist/assets/edge-FbVlp4U3.js +1 -0
  72. package/dist/assets/elixir-CkH2-t6x.js +1 -0
  73. package/dist/assets/elm-DbKCFpqz.js +1 -0
  74. package/dist/assets/emacs-lisp-C_m_b--Z.js +1 -0
  75. package/dist/assets/erb-DXfck5VN.js +1 -0
  76. package/dist/assets/erlang-DsQrWhSR.js +1 -0
  77. package/dist/assets/everforest-dark-BgDCqdQA.js +1 -0
  78. package/dist/assets/everforest-light-C8M2exoo.js +1 -0
  79. package/dist/assets/fennel-BYunw83y.js +1 -0
  80. package/dist/assets/fish-BvzEVeQv.js +1 -0
  81. package/dist/assets/fluent-C4IJs8-o.js +1 -0
  82. package/dist/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  83. package/dist/assets/fortran-free-form-BxgE0vQu.js +1 -0
  84. package/dist/assets/fsharp-CXgrBDvD.js +1 -0
  85. package/dist/assets/gdresource-TyuKm33G.js +1 -0
  86. package/dist/assets/gdscript-DqcFQ5yU.js +1 -0
  87. package/dist/assets/gdshader-DkwncUOv.js +1 -0
  88. package/dist/assets/genie-D0YGMca9.js +1 -0
  89. package/dist/assets/gherkin-DyxjwDmM.js +1 -0
  90. package/dist/assets/git-commit-F4YmCXRG.js +1 -0
  91. package/dist/assets/git-rebase-r7XF79zn.js +1 -0
  92. package/dist/assets/github-dark-DHJKELXO.js +1 -0
  93. package/dist/assets/github-dark-default-Cuk6v7N8.js +1 -0
  94. package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  95. package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  96. package/dist/assets/github-light-DAi9KRSo.js +1 -0
  97. package/dist/assets/github-light-default-D7oLnXFd.js +1 -0
  98. package/dist/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  99. package/dist/assets/gleam-BspZqrRM.js +1 -0
  100. package/dist/assets/glimmer-js-ByusRIyA.js +1 -0
  101. package/dist/assets/glimmer-ts-BfAWNZQY.js +1 -0
  102. package/dist/assets/glsl-DplSGwfg.js +1 -0
  103. package/dist/assets/gn-n2N0HUVH.js +1 -0
  104. package/dist/assets/gnuplot-DdkO51Og.js +1 -0
  105. package/dist/assets/go-C27-OAKa.js +1 -0
  106. package/dist/assets/graphql-ChdNCCLP.js +1 -0
  107. package/dist/assets/groovy-gcz8RCvz.js +1 -0
  108. package/dist/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  109. package/dist/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  110. package/dist/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  111. package/dist/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  112. package/dist/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  113. package/dist/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  114. package/dist/assets/hack-BWmVpMyf.js +1 -0
  115. package/dist/assets/haml-D5jkg6IW.js +1 -0
  116. package/dist/assets/handlebars-BpdQsYii.js +1 -0
  117. package/dist/assets/haskell-Df6bDoY_.js +1 -0
  118. package/dist/assets/haxe-CfZj7gIn.js +1 -0
  119. package/dist/assets/hcl-BWvSN4gD.js +1 -0
  120. package/dist/assets/hjson-D5-asLiD.js +1 -0
  121. package/dist/assets/hlsl-D3lLCCz7.js +1 -0
  122. package/dist/assets/horizon-BUw7H-hv.js +1 -0
  123. package/dist/assets/horizon-bright-CUuTKBJd.js +1 -0
  124. package/dist/assets/houston-DnULxvSX.js +1 -0
  125. package/dist/assets/html-derivative-DlHx6ybY.js +1 -0
  126. package/dist/assets/html-pp8916En.js +1 -0
  127. package/dist/assets/http-jrhK8wxY.js +1 -0
  128. package/dist/assets/hurl-irOxFIW8.js +1 -0
  129. package/dist/assets/hxml-2-FPmUDs.js +1 -0
  130. package/dist/assets/hy-DFXneXwc.js +1 -0
  131. package/dist/assets/imba-DGztddWO.js +1 -0
  132. package/dist/assets/index-B9rLIY81.js +263 -0
  133. package/dist/assets/index-CJBrtDbC.css +1 -0
  134. package/dist/assets/index-D-u0LYsV.js +153 -0
  135. package/dist/assets/ini-BEwlwnbL.js +1 -0
  136. package/dist/assets/java-CylS5w8V.js +1 -0
  137. package/dist/assets/javascript-wDzz0qaB.js +1 -0
  138. package/dist/assets/jinja-f2NsQr07.js +1 -0
  139. package/dist/assets/jison-wvAkD_A8.js +1 -0
  140. package/dist/assets/json-Cp-IABpG.js +1 -0
  141. package/dist/assets/json5-C9tS-k6U.js +1 -0
  142. package/dist/assets/jsonc-Des-eS-w.js +1 -0
  143. package/dist/assets/jsonl-DcaNXYhu.js +1 -0
  144. package/dist/assets/jsonnet-DFQXde-d.js +1 -0
  145. package/dist/assets/jssm-C2t-YnRu.js +1 -0
  146. package/dist/assets/jsx-g9-lgVsj.js +1 -0
  147. package/dist/assets/julia-5Bft2YPA.js +1 -0
  148. package/dist/assets/just-Cwhn7H3k.js +1 -0
  149. package/dist/assets/kanagawa-dragon-BuwD2xS4.js +1 -0
  150. package/dist/assets/kanagawa-lotus-C3DzagqV.js +1 -0
  151. package/dist/assets/kanagawa-wave-DFGoQZhC.js +1 -0
  152. package/dist/assets/kdl-DV7GczEv.js +1 -0
  153. package/dist/assets/kotlin-BdnUsdx6.js +1 -0
  154. package/dist/assets/kusto-wEQ09or8.js +1 -0
  155. package/dist/assets/laserwave-DUszq2jm.js +1 -0
  156. package/dist/assets/latex-D5pSuvFb.js +1 -0
  157. package/dist/assets/lean-BZvkOJ9d.js +1 -0
  158. package/dist/assets/less-B1dDrJ26.js +1 -0
  159. package/dist/assets/light-plus-B7mTdjB0.js +1 -0
  160. package/dist/assets/liquid-C0sCDyMI.js +1 -0
  161. package/dist/assets/llvm-BZoOZj88.js +1 -0
  162. package/dist/assets/log-2UxHyX5q.js +1 -0
  163. package/dist/assets/logo-BtOb2qkB.js +1 -0
  164. package/dist/assets/lua-BaeVxFsk.js +1 -0
  165. package/dist/assets/luau-BnpPk5vE.js +1 -0
  166. package/dist/assets/make-CHLpvVh8.js +1 -0
  167. package/dist/assets/markdown-Cvjx9yec.js +1 -0
  168. package/dist/assets/marko-DjSrsDqO.js +1 -0
  169. package/dist/assets/material-theme-D5KoaKCx.js +1 -0
  170. package/dist/assets/material-theme-darker-BfHTSMKl.js +1 -0
  171. package/dist/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  172. package/dist/assets/material-theme-ocean-CyktbL80.js +1 -0
  173. package/dist/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  174. package/dist/assets/matlab-D7o27uSR.js +1 -0
  175. package/dist/assets/mdc-D1_yUvq7.js +1 -0
  176. package/dist/assets/mdx-Cmh6b_Ma.js +1 -0
  177. package/dist/assets/mermaid-CQcHuHx7.js +1 -0
  178. package/dist/assets/min-dark-CafNBF8u.js +1 -0
  179. package/dist/assets/min-light-CTRr51gU.js +1 -0
  180. package/dist/assets/mipsasm-CKIfxQSi.js +1 -0
  181. package/dist/assets/mojo-DJz3ZmWd.js +1 -0
  182. package/dist/assets/monokai-BRVnQi9A.js +1 -0
  183. package/dist/assets/moonbit-CHtswR0a.js +1 -0
  184. package/dist/assets/move-el3G9tDJ.js +1 -0
  185. package/dist/assets/narrat-DRg8JJMk.js +1 -0
  186. package/dist/assets/nextflow-C-mBbutL.js +1 -0
  187. package/dist/assets/nextflow-groovy-vE_lwT2v.js +1 -0
  188. package/dist/assets/nginx-BpAMiNFr.js +1 -0
  189. package/dist/assets/night-owl-C39BiMTA.js +1 -0
  190. package/dist/assets/night-owl-light-CMTm3GFP.js +1 -0
  191. package/dist/assets/nim-BIad80T-.js +1 -0
  192. package/dist/assets/nix-CwoSXNpI.js +1 -0
  193. package/dist/assets/nord-Ddv68eIx.js +1 -0
  194. package/dist/assets/nsis-BlV79W_Q.js +1 -0
  195. package/dist/assets/nushell-D3jzshHO.js +1 -0
  196. package/dist/assets/objective-c-DXmwc3jG.js +1 -0
  197. package/dist/assets/objective-cpp-CLxacb5B.js +1 -0
  198. package/dist/assets/ocaml-C0hk2d4L.js +1 -0
  199. package/dist/assets/odin-BBf5iR-q.js +1 -0
  200. package/dist/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  201. package/dist/assets/one-light-C3Wv6jpd.js +1 -0
  202. package/dist/assets/openscad-C4EeE6gA.js +1 -0
  203. package/dist/assets/org-DM6o9KBp.js +1 -0
  204. package/dist/assets/pascal-D93ZcfNL.js +1 -0
  205. package/dist/assets/perl-B9cMNwum.js +1 -0
  206. package/dist/assets/php-Csjmro_R.js +1 -0
  207. package/dist/assets/pkl-u5AG7uiY.js +1 -0
  208. package/dist/assets/plastic-3e1v2bzS.js +1 -0
  209. package/dist/assets/plsql-ChMvpjG-.js +1 -0
  210. package/dist/assets/po-BTJTHyun.js +1 -0
  211. package/dist/assets/poimandres-CS3Unz2-.js +1 -0
  212. package/dist/assets/polar-C0HS_06l.js +1 -0
  213. package/dist/assets/postcss-CXtECtnM.js +1 -0
  214. package/dist/assets/powerquery-CEu0bR-o.js +1 -0
  215. package/dist/assets/powershell-BmBUJMz7.js +1 -0
  216. package/dist/assets/prisma-Vru482bI.js +1 -0
  217. package/dist/assets/prolog-CbFg5uaA.js +1 -0
  218. package/dist/assets/proto-C7zT0LnQ.js +1 -0
  219. package/dist/assets/pug-DKIMFp6K.js +1 -0
  220. package/dist/assets/puppet-BMWR74SV.js +1 -0
  221. package/dist/assets/purescript-CklMAg4u.js +1 -0
  222. package/dist/assets/python-B6aJPvgy.js +1 -0
  223. package/dist/assets/qml-3beO22l8.js +1 -0
  224. package/dist/assets/qmldir-C8lEn-DE.js +1 -0
  225. package/dist/assets/qss-IeuSbFQv.js +1 -0
  226. package/dist/assets/r-Cf5RLm7j.js +1 -0
  227. package/dist/assets/racket-BqYA7rlc.js +1 -0
  228. package/dist/assets/raku-DXvB9xmW.js +1 -0
  229. package/dist/assets/razor-BjBPvh-w.js +1 -0
  230. package/dist/assets/rbs-CpoqiR4B.js +1 -0
  231. package/dist/assets/red-hvxz__6c.js +1 -0
  232. package/dist/assets/reg-C-SQnVFl.js +1 -0
  233. package/dist/assets/regexp-CDVJQ6XC.js +1 -0
  234. package/dist/assets/rel-C3B-1QV4.js +1 -0
  235. package/dist/assets/riscv-BM1_JUlF.js +1 -0
  236. package/dist/assets/ron-D8l8udqQ.js +1 -0
  237. package/dist/assets/rose-pine-5qJOZa0Y.js +1 -0
  238. package/dist/assets/rose-pine-dawn-zx0QlTCp.js +1 -0
  239. package/dist/assets/rose-pine-moon-C9pEdX9L.js +1 -0
  240. package/dist/assets/rosmsg-BJDFO7_C.js +1 -0
  241. package/dist/assets/rst-bs7f0vWN.js +1 -0
  242. package/dist/assets/ruby-C0TQ7zu5.js +1 -0
  243. package/dist/assets/rust-B1yitclQ.js +1 -0
  244. package/dist/assets/sas-DEy46yEz.js +1 -0
  245. package/dist/assets/sass-Cj5Yp3dK.js +1 -0
  246. package/dist/assets/scala-CqE71os6.js +1 -0
  247. package/dist/assets/scheme-C98Dy4si.js +1 -0
  248. package/dist/assets/scss-D5BDwBP9.js +1 -0
  249. package/dist/assets/sdbl-DVxCFoDh.js +1 -0
  250. package/dist/assets/shaderlab-Dg9Lc6iA.js +1 -0
  251. package/dist/assets/shellscript-Yzrsuije.js +1 -0
  252. package/dist/assets/shellsession-BADoaaVG.js +1 -0
  253. package/dist/assets/slack-dark-BthQWCQV.js +1 -0
  254. package/dist/assets/slack-ochin-DqwNpetd.js +1 -0
  255. package/dist/assets/smalltalk-BOQMe2GC.js +1 -0
  256. package/dist/assets/smithy-cds9vsN8.js +1 -0
  257. package/dist/assets/snazzy-light-Bw305WKR.js +1 -0
  258. package/dist/assets/solarized-dark-CpvCGNkr.js +1 -0
  259. package/dist/assets/solarized-light-Dlz6yCKv.js +1 -0
  260. package/dist/assets/solidity-DijEV5ha.js +1 -0
  261. package/dist/assets/soy-8wufbnw4.js +1 -0
  262. package/dist/assets/sparql-rVzFXLq3.js +1 -0
  263. package/dist/assets/splunk-BtCnVYZw.js +1 -0
  264. package/dist/assets/sql-CRqJ_cUM.js +1 -0
  265. package/dist/assets/ssh-config-_ykCGR6B.js +1 -0
  266. package/dist/assets/stata-DI20mbqo.js +1 -0
  267. package/dist/assets/stylus-BEDo0Tqx.js +1 -0
  268. package/dist/assets/surrealql-Cjom0U5J.js +1 -0
  269. package/dist/assets/svelte-Cy7k_4gC.js +1 -0
  270. package/dist/assets/swift-C2oV4EkX.js +1 -0
  271. package/dist/assets/synthwave-84-CbfX1IO0.js +1 -0
  272. package/dist/assets/system-verilog-0hqHdDBg.js +1 -0
  273. package/dist/assets/systemd-4A_iFExJ.js +1 -0
  274. package/dist/assets/talonscript-CkByrt1z.js +1 -0
  275. package/dist/assets/tasl-QIJgUcNo.js +1 -0
  276. package/dist/assets/tcl-dwOrl1Do.js +1 -0
  277. package/dist/assets/templ-DhtptRzy.js +1 -0
  278. package/dist/assets/terraform-BETggiCN.js +1 -0
  279. package/dist/assets/tex-D96PA37w.js +1 -0
  280. package/dist/assets/tokyo-night-hegEt444.js +1 -0
  281. package/dist/assets/toml-vGWfd6FD.js +1 -0
  282. package/dist/assets/ts-tags-D351s5mN.js +1 -0
  283. package/dist/assets/tsv-B_m7g4N7.js +1 -0
  284. package/dist/assets/tsx-COt5Ahok.js +1 -0
  285. package/dist/assets/turtle-BsS91CYL.js +1 -0
  286. package/dist/assets/twig-27uCiNez.js +1 -0
  287. package/dist/assets/typescript-BPQ3VLAy.js +1 -0
  288. package/dist/assets/typespec-CAFt9gP4.js +1 -0
  289. package/dist/assets/typst-BUadGCkm.js +1 -0
  290. package/dist/assets/v-BGw2Nkan.js +1 -0
  291. package/dist/assets/vala-CsfeWuGM.js +1 -0
  292. package/dist/assets/vb-Cu-pLBUe.js +1 -0
  293. package/dist/assets/verilog-nZwndyjY.js +1 -0
  294. package/dist/assets/vesper-DRje8inN.js +1 -0
  295. package/dist/assets/vhdl-CeAyd5Ju.js +1 -0
  296. package/dist/assets/viml-CJc9bBzg.js +1 -0
  297. package/dist/assets/vitesse-black-Bkuqu6BP.js +1 -0
  298. package/dist/assets/vitesse-dark-D0r3Knsf.js +1 -0
  299. package/dist/assets/vitesse-light-CVO1_9PV.js +1 -0
  300. package/dist/assets/vue-BqiEGhQt.js +1 -0
  301. package/dist/assets/vue-html-AaS7Mt5G.js +1 -0
  302. package/dist/assets/vue-vine-BoDAl6tE.js +1 -0
  303. package/dist/assets/vyper-CDx5xZoG.js +1 -0
  304. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  305. package/dist/assets/wasm-MzD3tlZU.js +1 -0
  306. package/dist/assets/wenyan-BV7otONQ.js +1 -0
  307. package/dist/assets/wgsl-Dx-B1_4e.js +1 -0
  308. package/dist/assets/wikitext-BhOHFoWU.js +1 -0
  309. package/dist/assets/wit-5i3qLPDT.js +1 -0
  310. package/dist/assets/wolfram-lXgVvXCa.js +1 -0
  311. package/dist/assets/xml-sdJ4AIDG.js +1 -0
  312. package/dist/assets/xsl-CtQFsRM5.js +1 -0
  313. package/dist/assets/yaml-Buea-lGh.js +1 -0
  314. package/dist/assets/zenscript-DVFEvuxE.js +1 -0
  315. package/dist/assets/zig-VOosw3JB.js +1 -0
  316. package/dist/index.html +14 -0
  317. package/dist-server/companion/src/listener.js +187 -0
  318. package/dist-server/computer-proxy.js +1185 -0
  319. package/dist-server/connector-proxy.js +186 -0
  320. package/dist-server/container-mcp.js +15186 -0
  321. package/dist-server/drivers/agents-proxy.js +887 -0
  322. package/dist-server/drivers/browser-proxy.js +15037 -0
  323. package/dist-server/drivers/dweb-proxy.js +175 -0
  324. package/dist-server/drivers/phone-proxy.js +339 -0
  325. package/dist-server/drivers/pi-mcp-extension.ts +638 -0
  326. package/dist-server/index.js +64869 -0
  327. package/dist-server/local-computer.js +198 -0
  328. package/dist-server/mcp-server.js +1233 -0
  329. package/dist-server/openmausbot.js +3671 -0
  330. package/dist-server/pair-cli.js +3671 -0
  331. package/dist-server/permission-proxy.js +126 -0
  332. package/dist-server/prepare-cloudflared.js +253 -0
  333. package/dist-server/proxy-paths.js +29 -0
  334. package/dist-server/server/atomic.js +40 -0
  335. package/dist-server/server/attachments.js +471 -0
  336. package/dist-server/server/auto-approve.js +162 -0
  337. package/dist-server/server/auto-review.js +79 -0
  338. package/dist-server/server/avatar-image.js +144 -0
  339. package/dist-server/server/bot-cwd.js +29 -0
  340. package/dist-server/server/bot-directory.js +127 -0
  341. package/dist-server/server/bot-package.js +268 -0
  342. package/dist-server/server/bot-profile.js +76 -0
  343. package/dist-server/server/box-create-idempotency.js +463 -0
  344. package/dist-server/server/box.js +944 -0
  345. package/dist-server/server/brand.js +65 -0
  346. package/dist-server/server/browser-connection.js +187 -0
  347. package/dist-server/server/browser-lifecycle-cleanup.js +359 -0
  348. package/dist-server/server/calendar-calls.js +314 -0
  349. package/dist-server/server/channel-queue.js +70 -0
  350. package/dist-server/server/checkpoints.js +405 -0
  351. package/dist-server/server/chief-of-staff.js +49 -0
  352. package/dist-server/server/claude-update.js +70 -0
  353. package/dist-server/server/cli.js +524 -0
  354. package/dist-server/server/cloud-backend.js +30 -0
  355. package/dist-server/server/comms-visibility.js +109 -0
  356. package/dist-server/server/composio.js +945 -0
  357. package/dist-server/server/computer-control.js +149 -0
  358. package/dist-server/server/computer-observation.js +133 -0
  359. package/dist-server/server/computer-proxy.js +1061 -0
  360. package/dist-server/server/config.js +760 -0
  361. package/dist-server/server/connector-proxy.js +228 -0
  362. package/dist-server/server/container-computer.js +840 -0
  363. package/dist-server/server/container-mcp.js +27 -0
  364. package/dist-server/server/contracts.js +24 -0
  365. package/dist-server/server/control-client.js +100 -0
  366. package/dist-server/server/data-dir-lease.js +3 -0
  367. package/dist-server/server/decision-log.js +106 -0
  368. package/dist-server/server/delegations.js +628 -0
  369. package/dist-server/server/drivers/acp/core.js +904 -0
  370. package/dist-server/server/drivers/acp/cursor.js +365 -0
  371. package/dist-server/server/drivers/acp/custom.js +51 -0
  372. package/dist-server/server/drivers/acp/droid.js +225 -0
  373. package/dist-server/server/drivers/acp/gemini.js +95 -0
  374. package/dist-server/server/drivers/acp/grok.js +249 -0
  375. package/dist-server/server/drivers/acp/hermes.js +429 -0
  376. package/dist-server/server/drivers/acp/kimi.js +559 -0
  377. package/dist-server/server/drivers/acp/opencode-go.js +370 -0
  378. package/dist-server/server/drivers/acp/qwen.js +98 -0
  379. package/dist-server/server/drivers/agents-proxy.js +941 -0
  380. package/dist-server/server/drivers/antigravity-acp.js +507 -0
  381. package/dist-server/server/drivers/antigravity-release.js +65 -0
  382. package/dist-server/server/drivers/antigravity-runtime.js +395 -0
  383. package/dist-server/server/drivers/antigravity.js +144 -0
  384. package/dist-server/server/drivers/boxagent.js +245 -0
  385. package/dist-server/server/drivers/browser-proxy.js +520 -0
  386. package/dist-server/server/drivers/builtIn.js +36 -0
  387. package/dist-server/server/drivers/claude.js +1337 -0
  388. package/dist-server/server/drivers/codex-catalog.js +409 -0
  389. package/dist-server/server/drivers/codex.js +1116 -0
  390. package/dist-server/server/drivers/dweb-proxy.js +193 -0
  391. package/dist-server/server/drivers/grok.js +48 -0
  392. package/dist-server/server/drivers/grokagent.js +4 -0
  393. package/dist-server/server/drivers/local-inject.js +388 -0
  394. package/dist-server/server/drivers/minimax.js +108 -0
  395. package/dist-server/server/drivers/native.js +21 -0
  396. package/dist-server/server/drivers/openai-chat.js +240 -0
  397. package/dist-server/server/drivers/openai-compat.js +143 -0
  398. package/dist-server/server/drivers/phone-proxy.js +372 -0
  399. package/dist-server/server/drivers/pi.js +855 -0
  400. package/dist-server/server/drivers/retry.js +96 -0
  401. package/dist-server/server/enterprise.js +93 -0
  402. package/dist-server/server/env-path.js +329 -0
  403. package/dist-server/server/environment.js +105 -0
  404. package/dist-server/server/generated-image.js +39 -0
  405. package/dist-server/server/graceful-shutdown.js +24 -0
  406. package/dist-server/server/group-goal-run.js +124 -0
  407. package/dist-server/server/harness/bus.js +86 -0
  408. package/dist-server/server/harness/registry.js +217 -0
  409. package/dist-server/server/index.js +11634 -0
  410. package/dist-server/server/installed-playbooks.js +34 -0
  411. package/dist-server/server/local-computer.js +278 -0
  412. package/dist-server/server/local-routing.js +9 -0
  413. package/dist-server/server/local-vm-idle.js +44 -0
  414. package/dist-server/server/local-vm-inventory.js +34 -0
  415. package/dist-server/server/local-vm-lease.js +58 -0
  416. package/dist-server/server/mcp-bridge.js +251 -0
  417. package/dist-server/server/mcp-probe.js +153 -0
  418. package/dist-server/server/mcp-registry.js +130 -0
  419. package/dist-server/server/member-turn.js +6 -0
  420. package/dist-server/server/message-db.js +358 -0
  421. package/dist-server/server/message-file.js +439 -0
  422. package/dist-server/server/names.js +26 -0
  423. package/dist-server/server/notify.js +69 -0
  424. package/dist-server/server/openmaus-status-capsule.js +329 -0
  425. package/dist-server/server/openmausbot.js +6 -0
  426. package/dist-server/server/package-export.js +157 -0
  427. package/dist-server/server/pair-cli.js +7 -0
  428. package/dist-server/server/peer-approval-key.js +5 -0
  429. package/dist-server/server/peer-approval.js +182 -0
  430. package/dist-server/server/peer-provenance.js +44 -0
  431. package/dist-server/server/peer-roster.js +139 -0
  432. package/dist-server/server/permission-proxy.js +157 -0
  433. package/dist-server/server/phone-secret.js +358 -0
  434. package/dist-server/server/private-screen-capture.js +17 -0
  435. package/dist-server/server/procs.js +123 -0
  436. package/dist-server/server/project-scout.js +304 -0
  437. package/dist-server/server/proxy-paths.js +47 -0
  438. package/dist-server/server/redact.js +102 -0
  439. package/dist-server/server/remote-computer.js +170 -0
  440. package/dist-server/server/repeat-detector.js +48 -0
  441. package/dist-server/server/replies.js +38 -0
  442. package/dist-server/server/request-auth.js +327 -0
  443. package/dist-server/server/room-cwd.js +15 -0
  444. package/dist-server/server/room-post-budget.js +112 -0
  445. package/dist-server/server/room-turn-timeout.js +115 -0
  446. package/dist-server/server/routine-requests.js +871 -0
  447. package/dist-server/server/routines.js +1111 -0
  448. package/dist-server/server/schema.js +11 -0
  449. package/dist-server/server/screen-frame-gate.js +80 -0
  450. package/dist-server/server/section-context.js +85 -0
  451. package/dist-server/server/send-idempotency.js +53 -0
  452. package/dist-server/server/sessions.js +319 -0
  453. package/dist-server/server/skill-fetch.js +148 -0
  454. package/dist-server/server/skill-learn.js +61 -0
  455. package/dist-server/server/skill-library.js +116 -0
  456. package/dist-server/server/skills.js +1223 -0
  457. package/dist-server/server/steer-queue.js +114 -0
  458. package/dist-server/server/store.js +1217 -0
  459. package/dist-server/server/surface.js +107 -0
  460. package/dist-server/server/tailscale.js +103 -0
  461. package/dist-server/server/team-backup.js +183 -0
  462. package/dist-server/server/team-library.js +205 -0
  463. package/dist-server/server/team-manifest.js +224 -0
  464. package/dist-server/server/thread-events.js +226 -0
  465. package/dist-server/server/tts/elevenlabs.js +92 -0
  466. package/dist-server/server/tts/index.js +79 -0
  467. package/dist-server/server/tts/speech-text.js +195 -0
  468. package/dist-server/server/tts/system-voices.js +74 -0
  469. package/dist-server/server/tunnel.js +202 -0
  470. package/dist-server/server/turn-context.js +40 -0
  471. package/dist-server/server/turn-dispatch-guard.js +152 -0
  472. package/dist-server/server/turn-images.js +138 -0
  473. package/dist-server/server/turn-watchdog.js +65 -0
  474. package/dist-server/server/vps-computer.js +1099 -0
  475. package/dist-server/server/vps-container-mcp.js +31 -0
  476. package/dist-server/server/webhook-ingress.js +173 -0
  477. package/dist-server/server/webhooks.js +491 -0
  478. package/dist-server/server/workspace.js +178 -0
  479. package/dist-server/shared/approval-mode.js +39 -0
  480. package/dist-server/shared/bot-avatar.js +33 -0
  481. package/dist-server/shared/bot-profile.js +7 -0
  482. package/dist-server/shared/connector-availability.js +12 -0
  483. package/dist-server/shared/credential-request.js +82 -0
  484. package/dist-server/shared/group-goal-run.js +1 -0
  485. package/dist-server/shared/import-name.js +11 -0
  486. package/dist-server/shared/mascot-bodies.js +107 -0
  487. package/dist-server/shared/routine-request.js +10 -0
  488. package/dist-server/shared/routine-run.js +1 -0
  489. package/dist-server/shared/skill-request.js +16 -0
  490. package/dist-server/shared/team-backup.js +148 -0
  491. package/dist-server/src/lib/composer-attachments.js +676 -0
  492. package/dist-server/tunnel-guardian.js +390 -0
  493. package/dist-server/vps-container-mcp.js +15209 -0
  494. package/package.json +32 -0
  495. package/skills/create-verification-skill/SKILL.md +65 -0
  496. package/skills/create-verification-skill/manifest.json +13 -0
  497. package/skills/phone-harness/SKILL.md +26 -0
  498. package/skills/phone-harness/agents/openai.yaml +13 -0
  499. package/skills/phone-harness/manifest.json +17 -0
@@ -0,0 +1,944 @@
1
+ // Box (box.ascii.dev) provider — the bot's cloud computer. Ported from
2
+ // agentcal-api src/providers/box.js, reshaped per-bot instead of
3
+ // per-customer: every bot gets one persistent box (deterministic name),
4
+ // stop pauses billing while the disk survives, and Join always mints a
5
+ // FRESH desktop URL (stream tokens rotate on every state change — never
6
+ // persist one).
7
+ //
8
+ // Substrate facts (probed by agentcal 2026-07-24 on a live box):
9
+ // - REST only: POST /boxes/{id}/commands runs shell synchronously.
10
+ // - stop→archived ~5s, resume→idle ~8s; disk persists, tmux does not.
11
+ // - X11 desktop with Chrome + Ghostty; passwordless sudo; node 24.
12
+ // - the dedicated IP rotates across archive/resume — never persist it.
13
+ import { createHash } from "node:crypto";
14
+ import { DATA_DIR } from "./config.js";
15
+ import { loadEnvironmentId } from "./environment.js";
16
+ import { adoptResolvedBox, beginBoxCreate, discardBoxCreate, rememberCreatedBox, resolveBoxCreate, retireDeletedBoxCreate, } from "./box-create-idempotency.js";
17
+ import { ensureRemoteCuaCommand, isolatedRemoteCommand, MAX_REMOTE_COMMAND_LENGTH, remoteComputerBootstrapCommand, } from "./remote-computer.js";
18
+ // overridable so tests can point at a stub instead of the live provider
19
+ const BOX_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
20
+ const READY = new Set(["idle", "ready", "running"]);
21
+ const SLEEPING = new Set(["archived", "archiving", "stopped", "stopping"]);
22
+ const DEFAULT_BOX_TTL_SECONDS = 8 * 60 * 60;
23
+ const TRIAL_BOX_TTL_SECONDS = 2 * 60 * 60;
24
+ const BOX_CREATE_IN_PROGRESS_RETRY_DELAYS_MS = [250, 750, 1_500];
25
+ const BOX_DELETE_OPERATION_POLL_DELAYS_MS = [0, 100, 250, 500, 1_000, 2_000];
26
+ const BOX_INVENTORY_PAGE_SIZE = 200;
27
+ // Current self-serve accounts top out below 2,000 boxes. Keep the walk
28
+ // bounded anyway: a broken or adversarial cursor must not hold Settings open.
29
+ const MAX_BOX_INVENTORY_PAGES = 10;
30
+ const LEGACY_MANAGED_BOX_NAME = /^ogb-[a-z0-9]{1,8}-[a-f0-9]{6}$/;
31
+ const SCOPED_MANAGED_BOX_NAME = /^ogb-[a-f0-9]{12}-[a-z0-9]{1,8}-[a-f0-9]{6}$/;
32
+ const BOX_ID = /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/;
33
+ const BOX_DELETE_OPERATION_ID = /^bdop_[a-f0-9]{32}$/;
34
+ const BOX_DELETE_OPERATION_STATES = new Set(["pending", "processing", "blocked", "completed"]);
35
+ const BOX_STATES = new Set([
36
+ "init",
37
+ "idle",
38
+ "ready",
39
+ "running",
40
+ "archived",
41
+ "archiving",
42
+ "stopped",
43
+ "stopping",
44
+ "provisioning",
45
+ "provisioned",
46
+ "cloning",
47
+ "starting",
48
+ "error",
49
+ ]);
50
+ // Provider listings are account-wide. Hash the durable local environment id
51
+ // into every new name so another OpenMausBot installation using the same Box
52
+ // account cannot mistake this installation's computers for abandoned ones.
53
+ // The environment UUID itself never leaves the local data directory.
54
+ let scopedBoxPrefixCache = null;
55
+ /** Resolve only after server startup has migrated the legacy data directory
56
+ * and acquired its writer lease. A static-import side effect here used to
57
+ * create the new directory too early and suppress that migration. */
58
+ function scopedBoxPrefix() {
59
+ if (scopedBoxPrefixCache)
60
+ return scopedBoxPrefixCache;
61
+ const scope = createHash("sha256")
62
+ .update(loadEnvironmentId(DATA_DIR))
63
+ .digest("hex")
64
+ .slice(0, 12);
65
+ scopedBoxPrefixCache = `ogb-${scope}-`;
66
+ return scopedBoxPrefixCache;
67
+ }
68
+ /** Decide lifecycle work before a turn mounts Box. Auto may observe and
69
+ * attach an already-ready Box, but only explicit Cloud may create or wake. */
70
+ export function boxTurnLifecycleAction({ explicitCloud, canMount, state, }) {
71
+ if (!canMount)
72
+ return "none";
73
+ if (state && READY.has(state))
74
+ return "attach";
75
+ if (!explicitCloud)
76
+ return "none";
77
+ return state ? "wake" : "provision";
78
+ }
79
+ /** Keep one provider account for the whole logical operation. Settings may
80
+ * replace the shared config object after an async request has started; every
81
+ * follow-up (rename, readiness, cleanup, etc.) must keep using the credential
82
+ * that selected or created the Box in the first place. */
83
+ function snapshotBoxConfig(cfg) {
84
+ return { box: cfg.box ? { token: cfg.box.token } : undefined };
85
+ }
86
+ function boxFetch(cfg, path, opts = {}) {
87
+ return fetch(`${BOX_API}${path}`, {
88
+ ...opts,
89
+ headers: {
90
+ authorization: `Bearer ${cfg.box?.token}`,
91
+ "content-type": "application/json",
92
+ ...opts.headers,
93
+ },
94
+ });
95
+ }
96
+ async function boxJson(cfg, path, opts = {}) {
97
+ const res = await boxFetch(cfg, path, opts);
98
+ const body = await res.json().catch(() => null);
99
+ return { ok: res.ok && body?.ok !== false, status: res.status, body };
100
+ }
101
+ /** Accept only the immutable identity fields needed to follow a delete. Any
102
+ * malformed success envelope falls back to a direct Box read instead of
103
+ * authorizing journal retirement. */
104
+ function boxDeletionOperation(body, boxId, expectedOperationId) {
105
+ const operation = body?.operation;
106
+ const id = typeof operation?.id === "string" ? operation.id : "";
107
+ const status = typeof operation?.status === "string" ? operation.status : "";
108
+ if (!BOX_DELETE_OPERATION_ID.test(id)
109
+ || (expectedOperationId !== undefined && id !== expectedOperationId)
110
+ || operation?.kind !== "box"
111
+ || operation?.targetId !== boxId
112
+ || !BOX_DELETE_OPERATION_STATES.has(status))
113
+ return null;
114
+ return { id, status: status };
115
+ }
116
+ function deletionBlockedError(boxId) {
117
+ return Object.assign(new Error(`ascii.dev accepted deletion of ${boxId}, but the deletion operation is blocked — check ascii.dev and retry`), { status: 409 });
118
+ }
119
+ function boxDeleteProvedAbsent(result) {
120
+ return result.status === 404 || result.status === 410;
121
+ }
122
+ /** DELETE /boxes only accepts background deletion. Keep the durable create
123
+ * receipt until its operation completes. If operation polling is unavailable,
124
+ * a direct 404/410 is the only alternate proof that this Box identity is gone. */
125
+ async function confirmAcceptedBoxDeletion(cfg, boxId, acceptedBody) {
126
+ let operation = boxDeletionOperation(acceptedBody, boxId);
127
+ if (operation?.status === "completed")
128
+ return;
129
+ if (operation?.status === "blocked")
130
+ throw deletionBlockedError(boxId);
131
+ if (operation) {
132
+ for (const delayMs of BOX_DELETE_OPERATION_POLL_DELAYS_MS) {
133
+ if (delayMs > 0)
134
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
135
+ let polled;
136
+ try {
137
+ polled = await boxJson(cfg, `/deletion-operations/${operation.id}`, {
138
+ signal: AbortSignal.timeout(20_000),
139
+ });
140
+ }
141
+ catch {
142
+ break;
143
+ }
144
+ if (!polled.ok)
145
+ break;
146
+ const current = boxDeletionOperation(polled.body, boxId, operation.id);
147
+ if (!current)
148
+ break;
149
+ operation = current;
150
+ if (operation.status === "completed")
151
+ return;
152
+ if (operation.status === "blocked")
153
+ throw deletionBlockedError(boxId);
154
+ }
155
+ }
156
+ const inspected = await inspectBoxIdentity(cfg, boxId);
157
+ if (!inspected.available) {
158
+ throw Object.assign(new Error(`${inspected.problem ?? "the cloud computer deletion could not be verified"}; its recovery record was kept`), { status: 503 });
159
+ }
160
+ if (inspected.identity) {
161
+ throw Object.assign(new Error(`ascii.dev accepted deletion of ${boxId}, but it is not confirmed yet — retry after it finishes`), { status: 503 });
162
+ }
163
+ }
164
+ function boxBotNameParts(botId) {
165
+ const prefix = botId.slice(0, 8).toLowerCase().replace(/[^a-z0-9]/g, "") || "bot";
166
+ const hash = createHash("sha256").update(botId).digest("hex").slice(0, 6);
167
+ return { prefix, hash };
168
+ }
169
+ function legacyBoxNameFor(botId) {
170
+ const { prefix, hash } = boxBotNameParts(botId);
171
+ return `ogb-${prefix}-${hash}`;
172
+ }
173
+ // Deterministic per installation and bot. The bot hash kills truncated-id
174
+ // collisions; the environment scope prevents cross-install ownership claims.
175
+ export async function boxNameFor(botId) {
176
+ const { prefix, hash } = boxBotNameParts(botId);
177
+ return `${scopedBoxPrefix()}${prefix}-${hash}`;
178
+ }
179
+ /** Credential restoration must accept both current installation-scoped names
180
+ * and durable pre-scope names that the ownership journal may have adopted. */
181
+ export async function boxNameMatchesBot(botId, name) {
182
+ return name === await boxNameFor(botId) || name === legacyBoxNameFor(botId);
183
+ }
184
+ export async function runCommand(cfg, boxId, command, { timeoutMs = 120_000 } = {}) {
185
+ const res = await boxFetch(cfg, `/boxes/${boxId}/commands`, {
186
+ method: "POST",
187
+ body: JSON.stringify({ command }),
188
+ signal: AbortSignal.timeout(timeoutMs),
189
+ });
190
+ const body = await res.json().catch(() => null);
191
+ return {
192
+ ok: res.ok && body?.exitCode === 0,
193
+ exitCode: body?.exitCode ?? null,
194
+ stdout: body?.stdout ?? "",
195
+ stderr: body?.stderr ?? "",
196
+ };
197
+ }
198
+ // Desktop access, in the order that actually works (agentcal probing):
199
+ // 1) VNC (POST /desktop?vnc=1) — plain WebSocket, survives P2P-blocking
200
+ // networks; answers {provisioning:true} first, so poll for the URL.
201
+ // 2) WebRTC stream (POST /desktop) as fallback — STUN-only, can hang.
202
+ // The desktopUrl stored on the box object is NOT usable on its own.
203
+ async function mintDesktopUrl(cfg, boxId, { vncBudgetMs = 60_000 } = {}) {
204
+ const t0 = Date.now();
205
+ while (Date.now() - t0 < vncBudgetMs) {
206
+ const { body } = await boxJson(cfg, `/boxes/${boxId}/desktop?vnc=1`, { method: "POST" });
207
+ const url = body?.desktopUrl ?? body?.url;
208
+ if (url)
209
+ return url;
210
+ if (!body?.provisioning)
211
+ break;
212
+ await new Promise((r) => setTimeout(r, 3000));
213
+ }
214
+ const { body } = await boxJson(cfg, `/boxes/${boxId}/desktop`, { method: "POST" });
215
+ return body?.desktopUrl ?? body?.url ?? null;
216
+ }
217
+ async function waitReady(cfg, boxId, budgetMs = 90_000) {
218
+ const t0 = Date.now();
219
+ while (Date.now() - t0 < budgetMs) {
220
+ const { body } = await boxJson(cfg, `/boxes/${boxId}`);
221
+ const state = body?.box?.state;
222
+ if (READY.has(state))
223
+ return body.box;
224
+ if (state === "error")
225
+ return null;
226
+ // an archiving box can't resume until the snapshot lands — nudge after
227
+ if (state === "archived")
228
+ await boxJson(cfg, `/boxes/${boxId}/resume`, { method: "POST" });
229
+ await new Promise((r) => setTimeout(r, 2500));
230
+ }
231
+ return null;
232
+ }
233
+ // Resolving a bot's box means LISTing every box in the account, so it is
234
+ // the most expensive thing on any hot path. The name is deterministic, so
235
+ // once we know the id we can go straight at it — the cache is refreshed
236
+ // whenever the direct read fails (deleted/renamed box) and always carries
237
+ // the live state so callers can still see "archived".
238
+ const boxIdCache = new Map();
239
+ function boxInventoryProblem(status, body) {
240
+ if (status === 401 || status === 403) {
241
+ return "ascii.dev rejected the Box API key — update it in Settings → Connections";
242
+ }
243
+ if (status === 429)
244
+ return "ascii.dev is rate-limiting this account — wait a minute and refresh";
245
+ const message = typeof body?.message === "string" ? body.message.trim() : "";
246
+ return message ? `ascii.dev could not list cloud computers: ${message}` : `ascii.dev could not list cloud computers (${status})`;
247
+ }
248
+ function safeBoxState(value) {
249
+ if (typeof value !== "string")
250
+ return "unknown";
251
+ const state = value.toLowerCase();
252
+ return BOX_STATES.has(state) ? state : "unknown";
253
+ }
254
+ async function listBoxPages(cfg) {
255
+ const boxes = [];
256
+ const seenCursors = new Set();
257
+ let cursor = null;
258
+ for (let page = 0; page < MAX_BOX_INVENTORY_PAGES; page += 1) {
259
+ const path = `/boxes?limit=${BOX_INVENTORY_PAGE_SIZE}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
260
+ let listed;
261
+ try {
262
+ listed = await boxJson(cfg, path, { signal: AbortSignal.timeout(20_000) });
263
+ }
264
+ catch {
265
+ return { ok: false, problem: "Could not reach ascii.dev to list cloud computers — check your connection and refresh" };
266
+ }
267
+ if (!listed.ok || !Array.isArray(listed.body?.boxes)) {
268
+ return { ok: false, problem: boxInventoryProblem(listed.status, listed.body) };
269
+ }
270
+ boxes.push(...listed.body.boxes);
271
+ const next = listed.body?.pageInfo?.nextCursor;
272
+ if (next === undefined || next === null || next === "")
273
+ return { ok: true, boxes };
274
+ if (typeof next !== "string" || next.length > 4_096) {
275
+ return { ok: false, problem: "ascii.dev returned an invalid cloud computer page cursor" };
276
+ }
277
+ if (seenCursors.has(next)) {
278
+ return { ok: false, problem: "ascii.dev repeated a cloud computer page cursor — refresh and try again" };
279
+ }
280
+ seenCursors.add(next);
281
+ cursor = next;
282
+ }
283
+ return { ok: false, problem: "ascii.dev returned too many cloud computer pages — narrow the account inventory and refresh" };
284
+ }
285
+ /**
286
+ * One account listing for Settings and deletion guards. Only boxes
287
+ * carrying OpenMausBot's exact deterministic name shape leave this boundary;
288
+ * provider desktop links, IPs, environment details and other raw fields never
289
+ * reach the renderer. Only names scoped to this installation may become
290
+ * ownerless rows. Legacy names are accepted solely when a current bot proves
291
+ * ownership; foreign-install and ownerless legacy rows remain invisible and
292
+ * therefore cannot become deletion targets.
293
+ */
294
+ export async function listManagedBoxes(cfg, owners, options = {}) {
295
+ cfg = snapshotBoxConfig(cfg);
296
+ if (!boxConfigured(cfg)) {
297
+ return { configured: false, available: false, problem: null, instances: [] };
298
+ }
299
+ const listed = await listBoxPages(cfg);
300
+ if (!listed.ok) {
301
+ return {
302
+ configured: true,
303
+ available: false,
304
+ problem: listed.problem,
305
+ instances: [],
306
+ };
307
+ }
308
+ const namedOwners = await Promise.all(owners.map(async (owner) => ({
309
+ currentName: await boxNameFor(owner.botId),
310
+ legacyName: legacyBoxNameFor(owner.botId),
311
+ owner,
312
+ })));
313
+ const ownerByCurrentName = new Map(namedOwners.map(({ currentName, owner }) => [currentName, owner]));
314
+ const ownerByLegacyName = new Map(namedOwners.map(({ legacyName, owner }) => [legacyName, owner]));
315
+ const boxIdCounts = new Map();
316
+ for (const candidate of listed.boxes) {
317
+ if (!candidate || typeof candidate !== "object")
318
+ continue;
319
+ const boxId = typeof candidate.id === "string" ? candidate.id : "";
320
+ if (BOX_ID.test(boxId))
321
+ boxIdCounts.set(boxId, (boxIdCounts.get(boxId) ?? 0) + 1);
322
+ }
323
+ const invalidInventory = (problem) => ({
324
+ configured: true,
325
+ available: false,
326
+ problem,
327
+ instances: [],
328
+ });
329
+ const ownedBoxByBot = new Map();
330
+ for (const candidate of listed.boxes) {
331
+ if (!candidate || typeof candidate !== "object")
332
+ continue;
333
+ const name = typeof candidate.name === "string" ? candidate.name : "";
334
+ const owner = ownerByCurrentName.get(name) ?? ownerByLegacyName.get(name) ?? null;
335
+ if (!owner)
336
+ continue;
337
+ const boxId = typeof candidate.id === "string" ? candidate.id : "";
338
+ if (!BOX_ID.test(boxId)) {
339
+ return invalidInventory("ascii.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in ascii.dev");
340
+ }
341
+ const existing = ownedBoxByBot.get(owner.botId);
342
+ if (existing && existing !== boxId) {
343
+ return invalidInventory("ascii.dev returned conflicting cloud computers for one OpenMaus bot — repair them in ascii.dev before continuing");
344
+ }
345
+ ownedBoxByBot.set(owner.botId, boxId);
346
+ }
347
+ const instances = [];
348
+ const seenBoxIds = new Set();
349
+ const scopedPrefix = scopedBoxPrefix();
350
+ for (const candidate of listed.boxes) {
351
+ if (!candidate || typeof candidate !== "object")
352
+ continue;
353
+ const boxId = typeof candidate.id === "string" ? candidate.id : "";
354
+ const name = typeof candidate.name === "string" ? candidate.name : "";
355
+ let owner = null;
356
+ let legacyOwner = false;
357
+ if (SCOPED_MANAGED_BOX_NAME.test(name)) {
358
+ // A valid OMB name for another environment is account-visible but not
359
+ // ours to display or mutate.
360
+ if (!name.startsWith(scopedPrefix))
361
+ continue;
362
+ owner = ownerByCurrentName.get(name) ?? null;
363
+ }
364
+ else if (LEGACY_MANAGED_BOX_NAME.test(name)) {
365
+ // Pre-scope names have no installation provenance. A live local bot is
366
+ // the only safe ownership proof; unmatched legacy rows stay provider-
367
+ // managed until the person handles them in ascii.dev directly.
368
+ owner = ownerByLegacyName.get(name) ?? null;
369
+ if (!owner)
370
+ continue;
371
+ legacyOwner = true;
372
+ }
373
+ else {
374
+ continue;
375
+ }
376
+ // Once a row names this installation (or a live bot through its legacy
377
+ // deterministic name), silently skipping a malformed/duplicated identity
378
+ // could let bot deletion mistake provider corruption for absence.
379
+ if (!BOX_ID.test(boxId)) {
380
+ return invalidInventory("ascii.dev returned an invalid id for an OpenMaus-managed cloud computer — refresh or repair it in ascii.dev");
381
+ }
382
+ if ((boxIdCounts.get(boxId) ?? 0) !== 1 || seenBoxIds.has(boxId)) {
383
+ return invalidInventory("ascii.dev returned a conflicting id for an OpenMaus-managed cloud computer — refresh or repair it in ascii.dev");
384
+ }
385
+ if (legacyOwner && owner && options.adoptLegacy !== false) {
386
+ try {
387
+ adoptResolvedBox(owner.botId, boxId);
388
+ }
389
+ catch {
390
+ return invalidInventory("OpenMausBot could not safely remember this legacy cloud computer's owner — repair it in ascii.dev before continuing");
391
+ }
392
+ }
393
+ seenBoxIds.add(boxId);
394
+ instances.push({
395
+ boxId,
396
+ name,
397
+ state: safeBoxState(candidate.state),
398
+ ownerBotId: owner?.botId ?? null,
399
+ ownerName: owner?.name ?? null,
400
+ orphaned: owner === null,
401
+ inUse: owner?.inUse ?? false,
402
+ });
403
+ }
404
+ instances.sort((a, b) => {
405
+ if (a.orphaned !== b.orphaned)
406
+ return a.orphaned ? 1 : -1;
407
+ return (a.ownerName ?? a.name).localeCompare(b.ownerName ?? b.name);
408
+ });
409
+ return { configured: true, available: true, problem: null, instances };
410
+ }
411
+ /** Direct identity proof for a Box remembered in the local create journal.
412
+ * Unlike account LIST, this endpoint is not eventually consistent. Only the
413
+ * immutable id and provider name cross this boundary. */
414
+ export async function inspectBoxIdentity(cfg, boxId) {
415
+ cfg = snapshotBoxConfig(cfg);
416
+ if (!BOX_ID.test(boxId)) {
417
+ return { available: false, identity: null, problem: "the remembered cloud computer id is invalid" };
418
+ }
419
+ let inspected;
420
+ try {
421
+ inspected = await boxJson(cfg, `/boxes/${boxId}`, { signal: AbortSignal.timeout(20_000) });
422
+ }
423
+ catch {
424
+ return {
425
+ available: false,
426
+ identity: null,
427
+ problem: "Could not reach ascii.dev to verify a remembered cloud computer",
428
+ };
429
+ }
430
+ if (inspected.status === 404 || inspected.status === 410) {
431
+ return { available: true, identity: null, problem: null };
432
+ }
433
+ if (!inspected.ok) {
434
+ return { available: false, identity: null, problem: boxInventoryProblem(inspected.status, inspected.body) };
435
+ }
436
+ const candidate = inspected.body?.box;
437
+ const returnedId = typeof candidate?.id === "string" ? candidate.id : "";
438
+ const name = typeof candidate?.name === "string" ? candidate.name : "";
439
+ if (returnedId !== boxId || name.length === 0 || name.length > 100 || /[\r\n]/.test(name)) {
440
+ return { available: false, identity: null, problem: "ascii.dev returned an invalid cloud computer identity" };
441
+ }
442
+ return { available: true, identity: { boxId, name }, problem: null };
443
+ }
444
+ function inventoryFailure(inventory) {
445
+ const error = new Error(inventory.configured
446
+ ? (inventory.problem ?? "Cloud computer inventory is unavailable")
447
+ : "Box is not configured — add its API key in Settings → Connections");
448
+ error.status = inventory.configured ? 503 : 409;
449
+ return error;
450
+ }
451
+ async function revalidateManagedBox(cfg, owners, boxId) {
452
+ if (!BOX_ID.test(boxId))
453
+ throw Object.assign(new Error("invalid cloud computer id"), { status: 400 });
454
+ const inventory = await listManagedBoxes(cfg, owners);
455
+ if (!inventory.available)
456
+ throw inventoryFailure(inventory);
457
+ const instance = inventory.instances.find((candidate) => candidate.boxId === boxId);
458
+ if (!instance) {
459
+ throw Object.assign(new Error("that OpenMaus-managed cloud computer no longer exists"), { status: 404 });
460
+ }
461
+ return instance;
462
+ }
463
+ const QUIESCE_BROWSER = [
464
+ 'for name in chrome google-chrome chromium chromium-browser; do pid=$(pgrep -o -x "$name" 2>/dev/null || true); [ -z "$pid" ] || kill -TERM "$pid" 2>/dev/null || true; done',
465
+ 'for i in 1 2 3 4 5 6 7 8; do if ! pgrep -x chrome >/dev/null 2>&1 && ! pgrep -x google-chrome >/dev/null 2>&1 && ! pgrep -x chromium >/dev/null 2>&1 && ! pgrep -x chromium-browser >/dev/null 2>&1; then break; fi; sleep 0.25; done',
466
+ ].join("; ");
467
+ async function stopBox(cfg, boxId) {
468
+ // Browser shutdown is best-effort, but the provider stop is not: Settings
469
+ // must never say a computer is sleeping when ascii.dev rejected the action.
470
+ await runCommand(cfg, boxId, QUIESCE_BROWSER, { timeoutMs: 5_000 }).catch(() => null);
471
+ const stopped = await boxJson(cfg, `/boxes/${boxId}/stop`, { method: "POST" });
472
+ if (!stopped.ok)
473
+ throw Object.assign(new Error(boxErrorMessage(stopped.status, "box sleep", stopped.body)), { status: stopped.status });
474
+ }
475
+ function forgetBoxId(boxId) {
476
+ for (const [botId, cachedId] of boxIdCache) {
477
+ if (cachedId === boxId)
478
+ boxIdCache.delete(botId);
479
+ }
480
+ }
481
+ /** Explicit Settings action. Re-listing prevents a stale row from targeting a
482
+ * renamed or foreign provider resource. This never wakes or joins a Box. */
483
+ export async function sleepManagedBox(cfg, owners, boxId, claim) {
484
+ cfg = snapshotBoxConfig(cfg);
485
+ const instance = await revalidateManagedBox(cfg, owners, boxId);
486
+ if (instance.inUse) {
487
+ throw Object.assign(new Error("this cloud computer is in use — stop its bot's work first"), { status: 409 });
488
+ }
489
+ if (!SLEEPING.has(instance.state) && !READY.has(instance.state)) {
490
+ throw Object.assign(new Error(`this cloud computer cannot sleep while it is ${instance.state}`), { status: 409 });
491
+ }
492
+ const release = claim?.(instance);
493
+ try {
494
+ if (!SLEEPING.has(instance.state))
495
+ await stopBox(cfg, instance.boxId);
496
+ forgetBoxId(instance.boxId);
497
+ return { ok: true };
498
+ }
499
+ finally {
500
+ release?.();
501
+ }
502
+ }
503
+ /** Permanent Settings action. The caller must echo the exact freshly-listed
504
+ * machine name as well as its id; ascii.dev independently requires the id in
505
+ * its confirmation header. */
506
+ export async function deleteManagedBox(cfg, owners, boxId, confirmName, claim) {
507
+ cfg = snapshotBoxConfig(cfg);
508
+ const instance = await revalidateManagedBox(cfg, owners, boxId);
509
+ if (instance.inUse) {
510
+ throw Object.assign(new Error("this cloud computer is in use — stop its bot's work first"), { status: 409 });
511
+ }
512
+ if (confirmName !== instance.name) {
513
+ throw Object.assign(new Error("cloud computer confirmation no longer matches — refresh and try again"), { status: 409 });
514
+ }
515
+ const release = claim?.(instance);
516
+ try {
517
+ const removed = await boxJson(cfg, `/boxes/${instance.boxId}`, {
518
+ method: "DELETE",
519
+ headers: { "X-Ascii-Confirm-Delete": instance.boxId },
520
+ });
521
+ if (!removed.ok && !boxDeleteProvedAbsent(removed)) {
522
+ throw Object.assign(new Error(boxErrorMessage(removed.status, "box delete", removed.body)), { status: removed.status });
523
+ }
524
+ if (removed.ok)
525
+ await confirmAcceptedBoxDeletion(cfg, instance.boxId, removed.body);
526
+ retireDeletedBoxCreate(instance.boxId);
527
+ forgetBoxId(instance.boxId);
528
+ return { ok: true };
529
+ }
530
+ finally {
531
+ release?.();
532
+ }
533
+ }
534
+ export async function findBox(cfg, botId) {
535
+ cfg = snapshotBoxConfig(cfg);
536
+ const cachedId = boxIdCache.get(botId);
537
+ if (cachedId) {
538
+ let direct = null;
539
+ try {
540
+ direct = await boxJson(cfg, `/boxes/${cachedId}`);
541
+ }
542
+ catch {
543
+ // A direct read can fail while the account listing still succeeds.
544
+ // Fall through to the authoritative paginated lookup before deciding.
545
+ }
546
+ const directBox = direct?.body?.box;
547
+ if (direct?.ok && directBox?.id === cachedId && directBox.state !== "error")
548
+ return directBox;
549
+ if (direct?.ok && directBox?.id !== cachedId) {
550
+ throw Object.assign(new Error("ascii.dev returned an invalid cloud computer identity"), { status: 503 });
551
+ }
552
+ boxIdCache.delete(botId); // gone or broken — fall back to the listing
553
+ }
554
+ const name = await boxNameFor(botId);
555
+ const legacyName = legacyBoxNameFor(botId);
556
+ const listed = await listBoxPages(cfg);
557
+ if (!listed.ok) {
558
+ throw Object.assign(new Error(listed.problem), { status: 503 });
559
+ }
560
+ // Prefer the installation-scoped identity. A legacy name remains
561
+ // discoverable only for this exact local bot id.
562
+ const expected = listed.boxes.filter((candidate) => candidate?.name === name || candidate?.name === legacyName);
563
+ if (expected.some((candidate) => !BOX_ID.test(candidate?.id))) {
564
+ throw Object.assign(new Error("ascii.dev returned an invalid cloud computer identity"), { status: 503 });
565
+ }
566
+ const found = expected.find((candidate) => candidate.name === name && candidate.state !== "error")
567
+ ?? expected.find((candidate) => candidate.name === legacyName && candidate.state !== "error")
568
+ ?? null;
569
+ if (found) {
570
+ const duplicateId = listed.boxes.filter((candidate) => candidate?.id === found.id).length !== 1;
571
+ if (duplicateId) {
572
+ throw Object.assign(new Error("ascii.dev returned a conflicting cloud computer identity"), { status: 503 });
573
+ }
574
+ if (found.name === legacyName)
575
+ adoptResolvedBox(botId, found.id);
576
+ boxIdCache.set(botId, found.id);
577
+ }
578
+ return found;
579
+ }
580
+ /** Ready-or-null without the LIST when we already know the box. */
581
+ export async function readyBox(cfg, botId, budgetMs = 60_000) {
582
+ cfg = snapshotBoxConfig(cfg);
583
+ const box = await findBox(cfg, botId);
584
+ if (!box)
585
+ return null;
586
+ if (READY.has(box.state))
587
+ return box;
588
+ return waitReady(cfg, box.id, budgetMs);
589
+ }
590
+ export function boxConfigured(cfg) {
591
+ return Boolean(cfg.box?.token);
592
+ }
593
+ /** Ask the provider whether a token is real, before we let someone save
594
+ * it. Without this the paste "succeeds", and the first sign of trouble is
595
+ * a 401 in a different panel minutes later, with nothing to act on. */
596
+ export async function verifyToken(token) {
597
+ try {
598
+ const res = await fetch(`${BOX_API}/boxes`, {
599
+ headers: { authorization: `Bearer ${token}` },
600
+ signal: AbortSignal.timeout(20_000),
601
+ });
602
+ if (res.ok)
603
+ return { ok: true };
604
+ if (res.status === 401 || res.status === 403) {
605
+ // the common mistake is pasting some other credential entirely —
606
+ // box API keys are prefixed, so say which thing is wrong
607
+ return {
608
+ ok: false,
609
+ message: token.startsWith("box_")
610
+ ? "ascii.dev rejected that token — it may have been revoked or expired. Copy a fresh one from your ascii.dev account."
611
+ : "That doesn't look like a box API key: they start with box_. Copy the API key from your ascii.dev account (an account or session token won't work here).",
612
+ };
613
+ }
614
+ return { ok: false, message: `ascii.dev returned ${res.status} for that token — try again in a moment.` };
615
+ }
616
+ catch {
617
+ return { ok: false, message: "Couldn't reach ascii.dev to check that token — check your connection and retry." };
618
+ }
619
+ }
620
+ /** Turn a provider refusal into something a person can act on. The
621
+ * provider's own message is better than anything we can invent — it knows
622
+ * the plan, the limit and the link — so prefer it and only fall back to
623
+ * our own wording when it says nothing useful. */
624
+ export function boxErrorMessage(status, what, body) {
625
+ const theirs = typeof body?.message === "string" ? body.message.trim() : "";
626
+ const link = typeof body?.error?.details?.billingUrl === "string" ? body.error.details.billingUrl : "";
627
+ if (status === 402) {
628
+ // e.g. "Start the $20/month Box plan to create sandboxes."
629
+ return [theirs || "ascii.dev needs a paid Box plan before it will create a computer.", link].filter(Boolean).join(" ");
630
+ }
631
+ if (status === 401 || status === 403) {
632
+ return "your box token was rejected by ascii.dev — open App Settings and paste a current token (it starts with box_)";
633
+ }
634
+ if (status === 429) {
635
+ return theirs || "ascii.dev is rate-limiting this account — wait a minute and try again";
636
+ }
637
+ return theirs ? `${what} failed: ${theirs}` : `${what} failed (${status})`;
638
+ }
639
+ /** ascii.dev trial accounts reject the normal eight-hour auto-stop with a
640
+ * structured `trial_auto_stop_required` refusal. Retry that one condition
641
+ * once at the provider's advertised maximum (or the documented two-hour
642
+ * trial ceiling). Other create failures must retain their original error. */
643
+ function trialBoxTtlSeconds(body) {
644
+ const code = body?.error?.code ?? body?.code;
645
+ if (code !== "trial_auto_stop_required")
646
+ return null;
647
+ const details = body?.error?.details ?? body?.details ?? {};
648
+ for (const value of [details.maxTtlSeconds, details.maximumTtlSeconds, details.maxAutoStopSeconds]) {
649
+ if (Number.isInteger(value) && value > 0 && value <= DEFAULT_BOX_TTL_SECONDS)
650
+ return value;
651
+ }
652
+ return TRIAL_BOX_TTL_SECONDS;
653
+ }
654
+ function idempotentCreateInProgress(result) {
655
+ const code = result.body?.error?.code ?? result.body?.code;
656
+ return result.status === 409 && code === "idempotency_in_progress";
657
+ }
658
+ async function requestBoxCreate(cfg, botId, ttlSeconds) {
659
+ // The computer needs the user's desktop session, not the account owner's
660
+ // host credentials. Keep provider-side env injection off so API keys cannot
661
+ // silently appear inside the guest. The exact serialized body is also the
662
+ // idempotency identity: a trial-TTL retry must receive a different key.
663
+ const body = JSON.stringify({ ttlSeconds, noEnv: true });
664
+ let attempt = beginBoxCreate(botId, body);
665
+ let request = attempt.request;
666
+ let createdThisAttempt = attempt.startedNow;
667
+ // A previous process received the Box but died before naming it. Resolve
668
+ // the durable identity directly; never issue a second create first.
669
+ if (request.boxId) {
670
+ const recovered = await boxJson(cfg, `/boxes/${request.boxId}`, {
671
+ signal: AbortSignal.timeout(20_000),
672
+ });
673
+ if (recovered.ok && recovered.body?.box?.id === request.boxId) {
674
+ return { ...recovered, request, createdThisAttempt: false };
675
+ }
676
+ if (recovered.status !== 404 && recovered.status !== 410) {
677
+ return { ...recovered, request, createdThisAttempt: false };
678
+ }
679
+ discardBoxCreate(request);
680
+ attempt = beginBoxCreate(botId, body);
681
+ request = attempt.request;
682
+ createdThisAttempt = attempt.startedNow;
683
+ }
684
+ let last = null;
685
+ let ambiguousRetries = 0;
686
+ let inProgressRetries = 0;
687
+ for (;;) {
688
+ try {
689
+ last = await boxJson(cfg, "/boxes", {
690
+ method: "POST",
691
+ headers: { "Idempotency-Key": request.idempotencyKey },
692
+ signal: AbortSignal.timeout(45_000),
693
+ body,
694
+ });
695
+ }
696
+ catch (error) {
697
+ // A dropped response is ambiguous: ascii.dev may already have created
698
+ // the Box. One retry with the same key recovers it safely.
699
+ if (ambiguousRetries++ === 0)
700
+ continue;
701
+ throw error;
702
+ }
703
+ const boxId = last.body?.box?.id;
704
+ if (last.ok && typeof boxId === "string" && boxId) {
705
+ request = rememberCreatedBox(request, boxId);
706
+ return { ...last, request, createdThisAttempt };
707
+ }
708
+ if (idempotentCreateInProgress(last)) {
709
+ const delay = BOX_CREATE_IN_PROGRESS_RETRY_DELAYS_MS[inProgressRetries++];
710
+ if (delay !== undefined) {
711
+ await new Promise((resolve) => setTimeout(resolve, delay));
712
+ continue;
713
+ }
714
+ return { ...last, request, createdThisAttempt };
715
+ }
716
+ if ((last.status >= 500 || last.ok) && ambiguousRetries++ === 0)
717
+ continue;
718
+ // A 5xx or any idempotency conflict can follow a provider-side create;
719
+ // keep its key for recovery. Only a definitive client rejection proves
720
+ // this request did not create a Box and may be replaced safely.
721
+ if (last.status < 500 && last.status !== 409 && !last.ok)
722
+ discardBoxCreate(request);
723
+ return { ...last, request, createdThisAttempt };
724
+ }
725
+ }
726
+ async function createBox(cfg, botId) {
727
+ const first = await requestBoxCreate(cfg, botId, DEFAULT_BOX_TTL_SECONDS);
728
+ if (first.ok)
729
+ return first;
730
+ const trialTtl = trialBoxTtlSeconds(first.body);
731
+ return trialTtl === null ? first : requestBoxCreate(cfg, botId, trialTtl);
732
+ }
733
+ /** Box state for the Computer panel. */
734
+ export async function boxStatus(cfg, botId) {
735
+ cfg = snapshotBoxConfig(cfg);
736
+ if (!boxConfigured(cfg))
737
+ return { configured: false, box: null };
738
+ const box = await findBox(cfg, botId);
739
+ return {
740
+ configured: true,
741
+ box: box ? { boxId: box.id, state: box.state, desktopAvailable: box.desktopAvailable ?? null } : null,
742
+ };
743
+ }
744
+ /**
745
+ * Find-or-create the bot's persistent box, wait for ready, run the
746
+ * idempotent bootstrap (screenshot tooling for the computer-use bridge +
747
+ * a tmux welcome), and mint a fresh desktop URL.
748
+ */
749
+ export async function provisionBox(cfg, botId, botName) {
750
+ cfg = snapshotBoxConfig(cfg);
751
+ if (!boxConfigured(cfg)) {
752
+ throw new Error('box provider not enabled — add {"box":{"token":"…"}} to ~/.openmausbot/config.json');
753
+ }
754
+ const vmName = await boxNameFor(botId);
755
+ let box = await findBox(cfg, botId);
756
+ let created = false;
757
+ let createRequest = null;
758
+ try {
759
+ if (!box) {
760
+ // Provider-side backstop: archives itself (billing pauses, disk
761
+ // survives) if every stop path dies. Trial accounts get one narrower
762
+ // retry when ascii.dev reports their shorter TTL ceiling.
763
+ const createRes = await createBox(cfg, botId);
764
+ if (!createRes.ok || !createRes.body?.box?.id) {
765
+ throw new Error(boxErrorMessage(createRes.status, "box create", createRes.body));
766
+ }
767
+ box = createRes.body.box;
768
+ createRequest = createRes.request;
769
+ created = createRes.createdThisAttempt;
770
+ const rename = await boxJson(cfg, `/boxes/${box.id}`, {
771
+ method: "PATCH",
772
+ body: JSON.stringify({ name: vmName }),
773
+ });
774
+ if (!rename.ok)
775
+ throw new Error(boxErrorMessage(rename.status, "box naming", rename.body));
776
+ if (createRequest)
777
+ createRequest = resolveBoxCreate(createRequest);
778
+ }
779
+ const ready = await waitReady(cfg, box.id);
780
+ if (!ready)
781
+ throw new Error("box did not become ready within 90s — retry in a minute");
782
+ // Install the exact Cua Driver executable in the background, keep its
783
+ // daemon private to the VM, and retain X11 tooling as a degraded fallback.
784
+ const bootstrap = remoteComputerBootstrapCommand(botName);
785
+ let boot;
786
+ for (let attempt = 0; attempt < 5; attempt++) {
787
+ boot = await runCommand(cfg, box.id, bootstrap);
788
+ if (boot.ok || boot.exitCode !== null)
789
+ break;
790
+ await new Promise((r) => setTimeout(r, 3000));
791
+ }
792
+ if (!boot?.ok) {
793
+ const detail = boot?.stderr?.slice(0, 200) || (boot?.exitCode != null ? `exit ${boot.exitCode}` : "no response");
794
+ throw new Error(`box setup failed: ${detail}`);
795
+ }
796
+ const joinUrl = await mintDesktopUrl(cfg, box.id);
797
+ if (!joinUrl)
798
+ throw new Error("box desktop link could not be created");
799
+ return { boxId: box.id, machineName: vmName, reused: !created, state: ready.state, joinUrl };
800
+ }
801
+ catch (error) {
802
+ if (!created || !box?.id)
803
+ throw error;
804
+ const cleanup = await boxJson(cfg, `/boxes/${box.id}`, {
805
+ method: "DELETE",
806
+ headers: { "X-Ascii-Confirm-Delete": box.id },
807
+ }).catch(() => null);
808
+ if (cleanup && (cleanup.ok || boxDeleteProvedAbsent(cleanup))) {
809
+ if (cleanup.ok) {
810
+ try {
811
+ await confirmAcceptedBoxDeletion(cfg, box.id, cleanup.body);
812
+ }
813
+ catch (cleanupError) {
814
+ const message = error instanceof Error ? error.message : String(error);
815
+ const cleanupMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
816
+ throw new Error(`${message}. The new computer's deletion was not confirmed: ${cleanupMessage}. Check box ${box.id} in ascii.dev.`);
817
+ }
818
+ }
819
+ boxIdCache.delete(botId);
820
+ if (createRequest) {
821
+ // A stale record is safe (the next retry verifies its Box ID), so a
822
+ // cleanup-journal write failure must not hide the original error.
823
+ try {
824
+ discardBoxCreate(createRequest);
825
+ }
826
+ catch {
827
+ /* verified absent before any future create */
828
+ }
829
+ }
830
+ throw error;
831
+ }
832
+ boxIdCache.delete(botId);
833
+ const message = error instanceof Error ? error.message : String(error);
834
+ throw new Error(`${message}. The new computer could not be removed automatically; delete box ${box.id} in ascii.dev.`);
835
+ }
836
+ }
837
+ /** Wake the bot's box and return a FRESH desktop URL. */
838
+ export async function joinBox(cfg, botId) {
839
+ cfg = snapshotBoxConfig(cfg);
840
+ const box = await findBox(cfg, botId);
841
+ if (!box)
842
+ throw new Error("no computer yet — provision it first");
843
+ const ready = await waitReady(cfg, box.id);
844
+ if (!ready)
845
+ throw new Error("the box did not wake in time — try again");
846
+ // Provider archive/resume preserves disk but not processes. Reattach the
847
+ // driver daemon before handing the desktop back to the user.
848
+ await runCommand(cfg, box.id, ensureRemoteCuaCommand(), { timeoutMs: 15_000 }).catch(() => null);
849
+ return { joinUrl: await mintDesktopUrl(cfg, box.id), state: ready.state ?? null };
850
+ }
851
+ /** Mint a human-control URL without changing provider lifecycle or guest
852
+ * processes. This is the only join path allowed while a bot turn is active. */
853
+ export async function joinReadyBox(cfg, botId) {
854
+ cfg = snapshotBoxConfig(cfg);
855
+ const box = await findBox(cfg, botId);
856
+ if (!box)
857
+ throw Object.assign(new Error("no computer yet — provision it first"), { status: 409 });
858
+ if (!READY.has(box.state)) {
859
+ throw Object.assign(new Error("the cloud computer is sleeping or starting — interrupt the bot before waking it"), { status: 409 });
860
+ }
861
+ return { joinUrl: await mintDesktopUrl(cfg, box.id), state: box.state ?? null };
862
+ }
863
+ /** Archive the bot's box now (billing pauses, disk survives). */
864
+ export async function sleepBox(cfg, botId) {
865
+ cfg = snapshotBoxConfig(cfg);
866
+ const box = await findBox(cfg, botId);
867
+ if (!box)
868
+ throw new Error("no computer for this bot");
869
+ await stopBox(cfg, box.id);
870
+ forgetBoxId(box.id);
871
+ return { ok: true };
872
+ }
873
+ /** Owner-scoped shell for the Computer panel's console. */
874
+ export async function execOnBox(cfg, botId, command) {
875
+ cfg = snapshotBoxConfig(cfg);
876
+ if (command.length > MAX_REMOTE_COMMAND_LENGTH) {
877
+ throw new RangeError(`command is too long (maximum ${MAX_REMOTE_COMMAND_LENGTH} characters)`);
878
+ }
879
+ const box = await findBox(cfg, botId);
880
+ if (!box)
881
+ throw new Error("no computer for this bot yet");
882
+ const ready = await waitReady(cfg, box.id, 60_000);
883
+ if (!ready)
884
+ throw new Error("box did not wake");
885
+ const out = await runCommand(cfg, box.id, isolatedRemoteCommand(command));
886
+ return { exitCode: out.exitCode, stdout: out.stdout.slice(-4000), stderr: out.stderr.slice(-2000) };
887
+ }
888
+ // Screenshot for the Computer panel + screen-in-chat. Two hops: capture
889
+ // to a file on the box (scrot straight to JPEG — no ImageMagick startup
890
+ // unless a downscale is actually needed), then read the bytes back.
891
+ // Base64 over command stdout is NOT reliable for the panel's full-size
892
+ // frames (probed 2026-08-12: an otherwise-complete payload came back with
893
+ // a corrupted length), so the frame is always fetched over HTTP here.
894
+ const PANEL_PATH = "/tmp/ogb-panel.jpg";
895
+ const PANEL_WIDTH = 1024;
896
+ const SHOT_CMD = [
897
+ "export DISPLAY=${DISPLAY:-:0}",
898
+ `f=${PANEL_PATH}`,
899
+ 'w=$(xdotool getdisplaygeometry 2>/dev/null | cut -d" " -f1)',
900
+ 'case "$w" in ""|*[!0-9]*) w=0;; esac',
901
+ 'scrot -o -q 70 "$f" 2>/dev/null || import -window root -quality 70 "$f" 2>/dev/null || ffmpeg -y -f x11grab -i "$DISPLAY" -frames:v 1 -q:v 7 "$f" >/dev/null 2>&1',
902
+ `if [ "$w" -gt ${PANEL_WIDTH} ] 2>/dev/null && command -v convert >/dev/null 2>&1; then convert "$f" -thumbnail ${PANEL_WIDTH}x -quality 70 "$f" 2>/dev/null || true; fi`,
903
+ 'test -s "$f" && echo captured',
904
+ ].join("; ");
905
+ /** Read a file off the box as base64 — raw artifact bytes when the API
906
+ * supports it (33% less transfer, no JSON envelope), else the files API. */
907
+ async function readFileBase64(cfg, boxId, path) {
908
+ try {
909
+ const res = await boxFetch(cfg, `/boxes/${boxId}/artifacts?path=${encodeURIComponent(path)}`);
910
+ if (res.ok) {
911
+ const bytes = Buffer.from(await res.arrayBuffer());
912
+ if (bytes.length)
913
+ return bytes.toString("base64");
914
+ }
915
+ }
916
+ catch {
917
+ /* fall through */
918
+ }
919
+ const { ok, body } = await boxJson(cfg, `/boxes/${boxId}/files?path=${encodeURIComponent(path)}&encoding=base64`);
920
+ const content = body?.content;
921
+ return ok && typeof content === "string" && content ? content : null;
922
+ }
923
+ /** `knownBoxId` skips box resolution entirely — the screen poller holds
924
+ * the id for the whole turn and must not re-resolve it every frame. */
925
+ export async function screenshotBox(cfg, botId, knownBoxId) {
926
+ cfg = snapshotBoxConfig(cfg);
927
+ let boxId = knownBoxId;
928
+ if (!boxId) {
929
+ const box = await findBox(cfg, botId);
930
+ if (!box)
931
+ throw new Error("no computer for this bot yet");
932
+ if (!READY.has(box.state))
933
+ throw new Error(`box is ${box.state}`);
934
+ boxId = box.id;
935
+ }
936
+ const out = await runCommand(cfg, boxId, SHOT_CMD, { timeoutMs: 60_000 });
937
+ if (!/captured/.test(out.stdout)) {
938
+ throw new Error(out.stderr.slice(0, 200) || "screen capture failed on the box");
939
+ }
940
+ const data = await readFileBase64(cfg, boxId, PANEL_PATH);
941
+ if (!data)
942
+ throw new Error("could not read the frame back from the box");
943
+ return { png: data, format: "jpeg" };
944
+ }