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,1337 @@
1
+ // Claude driver — upstream ClaudeDriver skeleton over agentcal's
2
+ // drivers/claude.js runtime (stream-json both directions, prompt over
3
+ // stdin, completion from a real `result` event — verified against
4
+ // claude 2.1.211 by agentcal). Per-turn CLI process; the conversation
5
+ // continues across turns via --resume <sessionId> (the resumeCursor).
6
+ //
7
+ // Integrations become MCP servers on the CLI:
8
+ // - Composio Sessions (connected apps → tools) over streamable HTTP
9
+ // - the bot's cloud computer (box.ascii.dev) via server/computer-proxy.ts
10
+ // — screenshot/exec/open_url, the CUA-on-the-box bridge
11
+ import { createHash, randomBytes } from "node:crypto";
12
+ import { chmodSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
13
+ import { createServer as createNetServer } from "node:net";
14
+ import { homedir, tmpdir } from "node:os";
15
+ import { join, dirname } from "node:path";
16
+ import { DATA_DIR, stripWorkspaceCredentialEnv } from "../config.js";
17
+ import { augmentedPath } from "../env-path.js";
18
+ import { brokerSocketPath, describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
19
+ import { computerProxyEnv } from "../container-computer.js";
20
+ import { newEventId, newId } from "../contracts.js";
21
+ import { classifyError, computeBackoff, interruptibleDelay, RETRY_MAX_ATTEMPTS } from "./retry.js";
22
+ import { applyClaudeInject, decodeInjectId, mergeLocalInject, probeLocalInjects, resolveInjectId, } from "./local-inject.js";
23
+ import { appendNative } from "./native.js";
24
+ import { SPAWNED_PROXIES } from "../proxy-paths.js";
25
+ /** Whether `claude` has been signed in.
26
+ *
27
+ * Credential storage is deliberately not inspected here. Claude Code uses the
28
+ * macOS Keychain for OAuth, a JSON file on some platforms, and may gain other
29
+ * backends over time. Presence checks also accept stale credentials. The CLI's
30
+ * own machine-readable auth command is the source of truth for every backend.
31
+ */
32
+ export function claudeSignedIn(cli, env, run = execCli) {
33
+ return new Promise((resolve) => {
34
+ run(cli, ["auth", "status", "--json"], { timeout: 8000, env }, (_error, stdout) => {
35
+ try {
36
+ const status = JSON.parse(stdout);
37
+ resolve(typeof status === "object" && status !== null && "loggedIn" in status && status.loggedIn === true);
38
+ }
39
+ catch {
40
+ resolve(false);
41
+ }
42
+ });
43
+ });
44
+ }
45
+ /** The CLI environment shared by auth probes and real turns.
46
+ *
47
+ * Subscription users can be billed pay-as-you-go if an inherited API key
48
+ * leaks through, and a nested CLI must not inherit this session's identity.
49
+ * Keeping the probe and turn environments identical prevents setup from
50
+ * claiming an API-key login that the turn itself would deliberately remove.
51
+ */
52
+ function claudeEnvironment(model, source = process.env) {
53
+ const env = { ...source, PATH: augmentedPath(), NPM_CONFIG_LOGLEVEL: "error" };
54
+ delete env.CLAUDECODE;
55
+ delete env.CLAUDE_CODE_ENTRYPOINT;
56
+ // The harness process may hold workspace credentials (xai/box/voice keys,
57
+ // env-injected at boot); none of them are this CLI's to see.
58
+ stripWorkspaceCredentialEnv(env);
59
+ const applied = applyClaudeInject(env, model);
60
+ if (!applied.injected)
61
+ delete env.ANTHROPIC_API_KEY;
62
+ return env;
63
+ }
64
+ const DRIVER_KIND = "claudeAgent";
65
+ // model catalog ported from upstream packages/contracts/src/model.ts
66
+ export const STATIC_CLAUDE_MODELS = {
67
+ default: "claude-sonnet-5",
68
+ options: [
69
+ { id: "claude-fable-5-1", label: "Claude Fable 5.1" },
70
+ { id: "claude-fable-5", label: "Claude Fable 5" },
71
+ { id: "claude-opus-5", label: "Claude Opus 5" },
72
+ { id: "claude-sonnet-5", label: "Claude Sonnet 5" },
73
+ { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
74
+ ],
75
+ };
76
+ const CLAUDE_MODEL_ID = /^[a-z0-9][a-z0-9._:/-]*$/i;
77
+ /** Rewrite a leftover API slug (`orcarouter/Qwen…`) to `host::model` when a
78
+ * local host is serving it, so the turn injects instead of asking for /login.
79
+ * Official cloud ids and already-encoded inject ids skip the probe. */
80
+ async function resolveClaudeTurnModel(model, env) {
81
+ if (!model || decodeInjectId(model) || STATIC_CLAUDE_MODELS.options.some((option) => option.id === model)) {
82
+ return model;
83
+ }
84
+ return resolveInjectId(model, await probeLocalInjects(env)) ?? model;
85
+ }
86
+ function claudeConfigDir(env) {
87
+ if (env.CLAUDE_CONFIG_DIR)
88
+ return env.CLAUDE_CONFIG_DIR;
89
+ return join(env.HOME || env.USERPROFILE || homedir(), ".claude");
90
+ }
91
+ function extrasFromUnknown(value) {
92
+ if (!Array.isArray(value))
93
+ return [];
94
+ return value.flatMap((item) => {
95
+ if (typeof item === "string") {
96
+ return CLAUDE_MODEL_ID.test(item) ? [{ id: item, label: item }] : [];
97
+ }
98
+ if (!item || typeof item !== "object")
99
+ return [];
100
+ const row = item;
101
+ const id = [row.id, row.model, row.slug].find((candidate) => typeof candidate === "string");
102
+ if (!id || !CLAUDE_MODEL_ID.test(id))
103
+ return [];
104
+ const label = [row.name, row.displayName, row.label].find((candidate) => typeof candidate === "string");
105
+ return [{ id, label: label || id }];
106
+ });
107
+ }
108
+ /** Extra ids from ~/.claude/settings.json. Official cloud rows stay untagged.
109
+ * `model` is Claude Code's last-used slug, not a catalog — listing it as
110
+ * Custom put a non-inject id in the picker and the turn then had no
111
+ * ANTHROPIC_API_KEY ("Not logged in · Please run /login"). Live injects
112
+ * come from mergeLocalInject. */
113
+ export function readClaudeModelCatalog(env = process.env) {
114
+ let settings = {};
115
+ try {
116
+ settings = JSON.parse(readFileSync(join(claudeConfigDir(env), "settings.json"), "utf8"));
117
+ }
118
+ catch {
119
+ return STATIC_CLAUDE_MODELS;
120
+ }
121
+ const extras = [
122
+ ...extrasFromUnknown(settings.availableModels),
123
+ ...extrasFromUnknown(settings.customModels),
124
+ ...extrasFromUnknown(settings.extraModels),
125
+ ];
126
+ const nestedEnv = settings.env && typeof settings.env === "object" ? settings.env : {};
127
+ const envModel = nestedEnv.ANTHROPIC_MODEL ?? env.ANTHROPIC_MODEL;
128
+ if (typeof envModel === "string")
129
+ extras.push(...extrasFromUnknown([envModel]));
130
+ const options = STATIC_CLAUDE_MODELS.options.map((option) => ({ ...option }));
131
+ const seen = new Set(options.map((option) => option.id));
132
+ for (const extra of extras) {
133
+ if (seen.has(extra.id))
134
+ continue;
135
+ seen.add(extra.id);
136
+ options.push({ id: extra.id, label: extra.label, custom: true });
137
+ }
138
+ return { default: STATIC_CLAUDE_MODELS.default, options };
139
+ }
140
+ // Resolved from the server root, never relative to this file: bundling inlines
141
+ // this module into an entry one directory up, so a `".."` here would climb too
142
+ // far. See server/proxy-paths.ts.
143
+ const PROXY_PATH = SPAWNED_PROXIES.computer;
144
+ const PERM_PROXY_PATH = SPAWNED_PROXIES.permission;
145
+ const DWEB_PROXY_PATH = SPAWNED_PROXIES.dweb;
146
+ // in the packaged app process.execPath is the Electron binary — this env
147
+ // makes it behave as plain node for the spawned MCP proxies (harmless in dev)
148
+ const NODE_ENV_FLAG = { ELECTRON_RUN_AS_NODE: "1" };
149
+ function removePrivateTempDir(filePath) {
150
+ if (!filePath)
151
+ return true;
152
+ try {
153
+ rmSync(dirname(filePath), { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
154
+ return true;
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ }
160
+ const DENY_TIMEOUT_NOTE = "OpenMausBot: nobody answered this permission request in time. Skip this action and finish what you can without it.";
161
+ const QUESTION_TIMEOUT_NOTE = "OpenMausBot: nobody answered in time. Use your best judgment and continue.";
162
+ const DUPLICATE_ASK_ID_NOTE = "OpenMausBot: duplicate ask id — skipping this request.";
163
+ /** The system-source reply for an ask that outlives the turn — used both to
164
+ * drain in-flight `pending` asks on close() and to answer one that arrives
165
+ * on an already-closed broker (see the `closed` branch below). */
166
+ function systemEndedReply(kind) {
167
+ return kind === "question"
168
+ ? { behavior: "answer", message: "OpenMausBot: the turn is ending — wrap up." }
169
+ : { behavior: "deny", message: "OpenMausBot: the turn ended" };
170
+ }
171
+ /** One human-readable line for an ask — what the card subtitle shows. */
172
+ function askSummary(ask) {
173
+ const input = ask.input ?? {};
174
+ if (typeof input.question === "string")
175
+ return input.question.slice(0, 300);
176
+ if (typeof input.command === "string")
177
+ return input.command.slice(0, 200);
178
+ if (typeof input.url === "string")
179
+ return input.url.slice(0, 200);
180
+ const text = JSON.stringify(input);
181
+ return text === "{}" ? (ask.tool ?? "tool") : text.slice(0, 200);
182
+ }
183
+ export function permissionSocketPath(threadId) {
184
+ // A readable prefix alone is not unique: ids that agree on their first
185
+ // characters ("t-perm-dup-1", "t-perm-dup-2") would share a socket. POSIX
186
+ // hides that — a new broker's listen replaces the socket FILE, so the name
187
+ // always points at the fresh server — but Windows named pipes live in a
188
+ // global namespace that is never unlinked, and a reused name races the
189
+ // previous broker's async teardown. Half the tag is a digest of the FULL
190
+ // id so distinct threads get distinct sockets; the tag stays at 8 chars
191
+ // total because the POSIX path already brushes the 104-byte sun_path
192
+ // limit under deep tmp home dirs.
193
+ const prefix = threadId.replace(/[^\w-]/g, "").slice(0, 4);
194
+ const digest = createHash("sha256").update(threadId).digest("hex").slice(0, 4);
195
+ return brokerSocketPath(DATA_DIR, `${prefix}${digest}`);
196
+ }
197
+ /** Paths the broker may bind, tried in order. Windows named pipes are never
198
+ * unlinkable, and a hung CLI child from an earlier server process can hold a
199
+ * name for minutes, so fresh suffixes let the new broker bind immediately.
200
+ * POSIX gets a short temp fallback because macOS rejects Unix socket paths
201
+ * longer than its small `sun_path` limit; a deep test HOME or long username
202
+ * can otherwise make every approval silently unavailable. The proxy learns
203
+ * the actual bound path from its argv, so either fallback is transparent. */
204
+ export function brokerSocketCandidates(threadId) {
205
+ const base = permissionSocketPath(threadId);
206
+ if (process.platform !== "win32") {
207
+ const scope = createHash("sha256")
208
+ .update(`${DATA_DIR}\0${process.pid}\0${threadId}`)
209
+ .digest("hex")
210
+ .slice(0, 16);
211
+ return [base, join(tmpdir(), `omb-perm-${scope}.sock`)];
212
+ }
213
+ return [
214
+ base,
215
+ `${base}-${randomBytes(3).toString("hex")}`,
216
+ `${base}-${randomBytes(3).toString("hex")}`,
217
+ ];
218
+ }
219
+ export async function createPermissionBroker(opts) {
220
+ const timeoutMs = opts.timeoutMs ?? 15 * 60_000;
221
+ const pending = new Map();
222
+ // server.close() only stops accepting NEW connections — it does not touch
223
+ // a connection that's already open. A still-alive child's MCP proxy can
224
+ // keep sending asks on such a connection after the turn has ended, and
225
+ // this handler stays fully wired to it. Without this flag those asks would
226
+ // become new `pending` entries and `request.opened` cards for a turn the
227
+ // driver already forgot (`active.delete(threadId)` already ran), which can
228
+ // never be answered — the "zombie card" in issue #211.
229
+ let closed = false;
230
+ let boundPath = opts.socketPaths[0] ?? "";
231
+ const connectionHandler = (conn) => {
232
+ conn.on("error", () => { });
233
+ let buf = "";
234
+ conn.on("data", (chunk) => {
235
+ buf += chunk;
236
+ let nl;
237
+ while ((nl = buf.indexOf("\n")) !== -1) {
238
+ const line = buf.slice(0, nl);
239
+ buf = buf.slice(nl + 1);
240
+ let msg;
241
+ try {
242
+ msg = JSON.parse(line);
243
+ }
244
+ catch {
245
+ continue;
246
+ }
247
+ if (msg.t !== "ask")
248
+ continue;
249
+ const askId = String(msg.id ?? newId());
250
+ const kind = msg.kind === "question" ? "question" : "permission";
251
+ if (closed) {
252
+ // Closure is terminal and takes precedence over every active-turn
253
+ // rule, including duplicate-id rejection. Never register a pending
254
+ // entry or notify onAsk, but always answer an existing connection:
255
+ // permission-proxy.ts only resolves on an explicit answer (or a
256
+ // connection error/close), so a silent drop would hang the tool.
257
+ try {
258
+ conn.write(JSON.stringify({ t: "answer", id: askId, ...systemEndedReply(kind) }) + "\n");
259
+ }
260
+ catch { }
261
+ continue;
262
+ }
263
+ // A retained Claude process keeps its proxy connection between
264
+ // turns. Late/background asks must still fail closed without opening
265
+ // a card for a turn that has already settled.
266
+ if (opts.isActive && !opts.isActive()) {
267
+ try {
268
+ conn.write(JSON.stringify({ t: "answer", id: askId, ...systemEndedReply(kind) }) + "\n");
269
+ }
270
+ catch { }
271
+ continue;
272
+ }
273
+ // `pending` is server-scoped, not per-connection: two asks with the
274
+ // same id — a buggy/adversarial client, never a legitimate retry
275
+ // (permission-proxy mints a fresh randomUUID per ask) — would
276
+ // otherwise let the second `pending.set` silently overwrite the
277
+ // first, orphaning it as an unanswerable card once the first
278
+ // resolves and deletes the shared key. Reject before either ask
279
+ // becomes visible to onAsk.
280
+ if (pending.has(askId)) {
281
+ // askId is client-controlled; JSON.stringify escapes newlines and
282
+ // control characters so it can't corrupt the log line or terminal.
283
+ console.error(`permission broker on ${boundPath}: duplicate ask id ${JSON.stringify(askId)} — denying`);
284
+ try {
285
+ conn.write(JSON.stringify({ t: "answer", id: askId, behavior: "deny", message: DUPLICATE_ASK_ID_NOTE }) + "\n");
286
+ }
287
+ catch { }
288
+ continue;
289
+ }
290
+ const ask = { id: askId, kind, tool: msg.tool ?? "tool", input: msg.input ?? {}, at: Date.now() };
291
+ const finish = (behavior, message, source) => {
292
+ if (!pending.delete(askId))
293
+ return;
294
+ clearTimeout(timer);
295
+ try {
296
+ conn.write(JSON.stringify({ t: "answer", id: askId, behavior, message }) + "\n");
297
+ }
298
+ catch { }
299
+ opts.onResolve({ ...ask, behavior, source });
300
+ };
301
+ const timer = setTimeout(() => kind === "question"
302
+ ? finish("answer", QUESTION_TIMEOUT_NOTE, "timeout")
303
+ : finish("deny", DENY_TIMEOUT_NOTE, "timeout"), timeoutMs);
304
+ timer.unref?.();
305
+ pending.set(askId, { ask, finish });
306
+ opts.onAsk(ask);
307
+ }
308
+ });
309
+ };
310
+ // Bind the first candidate that will take a listener. A broker that
311
+ // never came up used to be silent — every approval then timed out into a
312
+ // deny nobody could explain. Keep the turn fail-closed on total failure,
313
+ // but leave an actionable diagnostic either way.
314
+ let server = null;
315
+ for (const [index, candidate] of opts.socketPaths.entries()) {
316
+ const attempt = createNetServer(connectionHandler);
317
+ try {
318
+ unlinkSync(candidate);
319
+ }
320
+ catch { }
321
+ let outcome = await new Promise((resolve) => {
322
+ attempt.once("listening", () => resolve("listening"));
323
+ // SAFETY: net 'error' events carry syscall errors; the optional
324
+ // `code` is only read defensively below.
325
+ attempt.once("error", (error) => resolve(error));
326
+ attempt.listen(candidate);
327
+ });
328
+ // A fallback under the shared OS temp root must not be connectable by
329
+ // another local account. DATA_DIR is private already, but applying the
330
+ // same mode to every POSIX socket keeps the rule simple and fail-closed.
331
+ if (outcome === "listening" && process.platform !== "win32") {
332
+ try {
333
+ chmodSync(candidate, 0o600);
334
+ }
335
+ catch (error) {
336
+ try {
337
+ attempt.close();
338
+ }
339
+ catch { }
340
+ try {
341
+ unlinkSync(candidate);
342
+ }
343
+ catch { }
344
+ outcome = error;
345
+ }
346
+ }
347
+ if (outcome === "listening") {
348
+ if (index > 0) {
349
+ console.error(`permission broker: ${opts.socketPaths[0]} is still held — bound fallback ${candidate}`);
350
+ }
351
+ boundPath = candidate;
352
+ server = attempt;
353
+ attempt.on("error", (error) => {
354
+ console.error(`permission broker error on ${candidate}: ${error.message}`);
355
+ });
356
+ break;
357
+ }
358
+ try {
359
+ attempt.close();
360
+ }
361
+ catch { }
362
+ if (index === opts.socketPaths.length - 1) {
363
+ console.error(`permission broker unavailable on ${candidate}: ${outcome.message}`);
364
+ break;
365
+ }
366
+ }
367
+ // Never hand the proxy an occupied candidate when every bind failed. That
368
+ // could connect it to a stale (or unrelated) listener instead of this
369
+ // broker, defeating the fail-closed boundary.
370
+ if (!server)
371
+ throw new Error("claude: permission broker could not bind a local socket");
372
+ const drain = () => {
373
+ for (const p of [...pending.values()]) {
374
+ const { behavior, message } = systemEndedReply(p.ask.kind);
375
+ p.finish(behavior, message, "system");
376
+ }
377
+ };
378
+ return {
379
+ answer(askId, behavior, message) {
380
+ const p = pending.get(askId);
381
+ if (!p)
382
+ return false;
383
+ if (p.ask.kind === "question" ? behavior !== "answer" : behavior === "answer")
384
+ return false;
385
+ p.finish(behavior, message, "user");
386
+ return true;
387
+ },
388
+ pause() {
389
+ drain();
390
+ },
391
+ close() {
392
+ closed = true;
393
+ drain();
394
+ try {
395
+ server?.close();
396
+ }
397
+ catch { }
398
+ try {
399
+ unlinkSync(boundPath);
400
+ }
401
+ catch { }
402
+ },
403
+ /** Where the broker actually listens — argv for the proxy child must
404
+ * use this, not the deterministic base, when a fallback was bound. */
405
+ socketPath: boundPath,
406
+ };
407
+ }
408
+ function decodeToolList(value, field) {
409
+ if (value === undefined)
410
+ return undefined;
411
+ if (!Array.isArray(value))
412
+ throw new Error(`claude: ${field} must be an array of non-empty strings`);
413
+ const decoded = [];
414
+ const seen = new Set();
415
+ for (const entry of value) {
416
+ if (typeof entry !== "string" || !entry.trim()) {
417
+ throw new Error(`claude: ${field} must be an array of non-empty strings`);
418
+ }
419
+ const normalized = entry.trim();
420
+ if (seen.has(normalized))
421
+ continue;
422
+ seen.add(normalized);
423
+ decoded.push(normalized);
424
+ }
425
+ return decoded;
426
+ }
427
+ function decodeConfig(raw) {
428
+ const o = (raw ?? {});
429
+ const mode = o.permissionMode;
430
+ if (mode !== undefined && mode !== "acceptEdits" && mode !== "auto" && mode !== "bypassPermissions") {
431
+ throw new Error(`claude: invalid permissionMode ${JSON.stringify(mode)}`);
432
+ }
433
+ const tools = decodeToolList(o.tools, "tools");
434
+ const disallowedTools = decodeToolList(o.disallowedTools, "disallowedTools");
435
+ return {
436
+ cli: typeof o.cli === "string" ? o.cli : "claude",
437
+ permissionMode: mode ?? "acceptEdits",
438
+ ...(tools !== undefined ? { tools } : {}),
439
+ ...(disallowedTools !== undefined ? { disallowedTools } : {}),
440
+ };
441
+ }
442
+ function firstText(content) {
443
+ if (typeof content === "string")
444
+ return content;
445
+ if (Array.isArray(content)) {
446
+ return content
447
+ .filter((b) => b?.type === "text" && b.text)
448
+ .map((b) => b.text)
449
+ .join("");
450
+ }
451
+ return "";
452
+ }
453
+ /** Claude's stream-json input accepts the same image source blocks as the
454
+ * Anthropic Messages API. Keep the old string form for text-only turns so a
455
+ * CLI update cannot disturb the overwhelmingly common path. */
456
+ function claudeUserMessage(text, images) {
457
+ if (!images?.length)
458
+ return { type: "user", message: { role: "user", content: text } };
459
+ const content = images.map((image) => ({
460
+ type: "image",
461
+ source: {
462
+ type: "base64",
463
+ media_type: image.mime,
464
+ data: readFileSync(image.path).toString("base64"),
465
+ },
466
+ }));
467
+ if (text)
468
+ content.push({ type: "text", text });
469
+ return { type: "user", message: { role: "user", content } };
470
+ }
471
+ /** Native traces are routinely attached to bug reports. Preserve the image
472
+ * block's shape and size for debugging, but never persist its base64 bytes. */
473
+ function diagnosticClaudeUserMessage(message) {
474
+ if (!Array.isArray(message.message.content))
475
+ return message;
476
+ return {
477
+ ...message,
478
+ message: {
479
+ ...message.message,
480
+ content: message.message.content.map((block) => block.type === "image"
481
+ ? {
482
+ ...block,
483
+ source: {
484
+ ...block.source,
485
+ data: `[image data: ${block.source.data.length} base64 chars]`,
486
+ },
487
+ }
488
+ : block),
489
+ },
490
+ };
491
+ }
492
+ export const ClaudeDriver = {
493
+ driverKind: DRIVER_KIND,
494
+ metadata: { displayName: "Claude", supportsMultipleInstances: true },
495
+ // npm on all three: the one recipe that is genuinely cross-platform. The
496
+ // native installers differ per OS and would need verifying separately.
497
+ install: {
498
+ command: {
499
+ darwin: "npm install -g @anthropic-ai/claude-code",
500
+ linux: "npm install -g @anthropic-ai/claude-code",
501
+ win32: "npm install -g @anthropic-ai/claude-code",
502
+ },
503
+ needsNode: true,
504
+ docsUrl: "https://claude.com/claude-code",
505
+ signInCommand: "claude",
506
+ },
507
+ models: STATIC_CLAUDE_MODELS,
508
+ decodeConfig,
509
+ defaultConfig: () => decodeConfig({}),
510
+ async create(input) {
511
+ const { instanceId, config } = input;
512
+ const catalogEnv = { ...process.env, ...input.environment };
513
+ let models = STATIC_CLAUDE_MODELS;
514
+ const refreshModels = async () => {
515
+ try {
516
+ const resolved = await mergeLocalInject(readClaudeModelCatalog(catalogEnv), catalogEnv);
517
+ if (resolved.options.length)
518
+ models = resolved;
519
+ }
520
+ catch {
521
+ // Keep the last usable catalog when settings.json is unreadable.
522
+ }
523
+ };
524
+ await refreshModels();
525
+ const listeners = new Set();
526
+ // one active turn per thread; a second send while busy is a caller bug
527
+ const active = new Map();
528
+ const sessions = new Map();
529
+ const configuredIdleMinimum = Number(process.env.OMB_CLAUDE_SESSION_IDLE_MIN_MS);
530
+ const sessionIdleMinimum = Number.isFinite(configuredIdleMinimum) && configuredIdleMinimum > 0
531
+ ? configuredIdleMinimum
532
+ : 10_000;
533
+ const SESSION_IDLE_MS = Math.max(sessionIdleMinimum, Number(process.env.OMB_CLAUDE_SESSION_IDLE_MS) || 10 * 60_000);
534
+ const closeSession = (threadId, why) => {
535
+ const s = sessions.get(threadId);
536
+ if (!s || s.closing)
537
+ return;
538
+ s.closing = true;
539
+ if (s.idleTimer)
540
+ clearTimeout(s.idleTimer);
541
+ // Broker ownership belongs to this session. Detach and close it now,
542
+ // before a replacement can bind the same per-thread socket; the old
543
+ // child's later close event must never unlink a new broker.
544
+ const broker = s.broker;
545
+ s.broker = undefined;
546
+ broker?.close();
547
+ appendNative(threadId, { dir: "out", source: "claude.session", msg: { close: why } });
548
+ // stdin EOF is the CLI's exit signal; give it a moment, then insist
549
+ try {
550
+ s.child.stdin.end();
551
+ }
552
+ catch { }
553
+ const kill = setTimeout(() => {
554
+ if (s.child.exitCode === null)
555
+ killCliTree(s.child);
556
+ }, 5_000);
557
+ kill.unref?.();
558
+ };
559
+ const armIdle = (threadId) => {
560
+ const s = sessions.get(threadId);
561
+ if (!s)
562
+ return;
563
+ if (s.idleTimer)
564
+ clearTimeout(s.idleTimer);
565
+ s.idleTimer = setTimeout(() => closeSession(threadId, "idle"), SESSION_IDLE_MS);
566
+ s.idleTimer.unref?.();
567
+ };
568
+ const writeUser = (s, threadId, promptMsg) => {
569
+ if (!s.child.stdin.writable || s.child.stdin.destroyed)
570
+ return Promise.resolve(false);
571
+ return new Promise((resolve) => {
572
+ try {
573
+ s.child.stdin.write(JSON.stringify(promptMsg) + "\n", (error) => {
574
+ if (error)
575
+ return resolve(false);
576
+ appendNative(threadId, {
577
+ dir: "out",
578
+ source: "claude.sdk.message",
579
+ msg: diagnosticClaudeUserMessage(promptMsg),
580
+ });
581
+ resolve(true);
582
+ });
583
+ }
584
+ catch {
585
+ resolve(false);
586
+ }
587
+ });
588
+ };
589
+ const emit = (event) => {
590
+ for (const l of [...listeners])
591
+ l(event);
592
+ };
593
+ const base = (threadId, turnId) => ({
594
+ eventId: newEventId(),
595
+ provider: DRIVER_KIND,
596
+ threadId,
597
+ turnId,
598
+ createdAt: new Date().toISOString(),
599
+ });
600
+ // retry bookkeeping lives PER THREAD, not per sendTurn call: a relaunch
601
+ // is a fresh sendTurn, and the attempt cap must survive across launches
602
+ const retryState = new Map();
603
+ const sendTurn = async (turn) => {
604
+ const { threadId } = turn;
605
+ if (active.has(threadId))
606
+ throw new Error("a turn is already running on this thread");
607
+ // A bot-level mode is authoritative for this turn. In particular, an
608
+ // old provider instance may still be configured with
609
+ // `bypassPermissions`; Ask/Auto must restore Claude's interactive
610
+ // broker instead of inheriting that silent bypass. Calls without a
611
+ // per-turn mode keep the legacy adapter behavior.
612
+ const permissionMode = turn.approvalMode === undefined
613
+ ? config.permissionMode
614
+ : turn.approvalMode === "full" ? "bypassPermissions"
615
+ : turn.approvalMode === "auto" ? "auto" : "default";
616
+ const controlsHost = turn.integrations?.localComputer?.scope === "local-computer";
617
+ if (controlsHost && permissionMode === "bypassPermissions" && turn.approvalMode !== "full") {
618
+ throw new Error("local computer control requires the interactive approval broker");
619
+ }
620
+ // Materialize before creating a broker or process. A missing/corrupt
621
+ // attachment must fail this call without leaving a live session behind.
622
+ const promptMsg = claudeUserMessage(turn.text, turn.images);
623
+ const turnId = newId();
624
+ const retryAbort = new AbortController();
625
+ const retry = retryState.get(threadId) ?? { attempt: 0, cancelled: false };
626
+ retry.cancelled = false;
627
+ retryState.set(threadId, retry);
628
+ // a retry relaunches the whole CLI; the backoff is scaled down in tests
629
+ // so a fake's transient failures don't stall real seconds
630
+ const retryScale = Number(process.env.FAKE_CLAUDE_RETRY_SCALE ?? "1");
631
+ const sessionId = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
632
+ const newSessionId = sessionId ? null : newId();
633
+ const args = [
634
+ "-p",
635
+ "--output-format", "stream-json",
636
+ "--input-format", "stream-json",
637
+ "--verbose", // required by stream-json output
638
+ // token-level streaming: content_block_delta events between the
639
+ // whole-message frames, so the bubble grows as the model writes
640
+ "--include-partial-messages",
641
+ "--permission-mode", permissionMode,
642
+ ];
643
+ if (config.tools !== undefined)
644
+ args.push("--tools", config.tools.join(","));
645
+ if (config.disallowedTools?.length) {
646
+ args.push("--disallowedTools", config.disallowedTools.join(","));
647
+ }
648
+ const turnEnvironment = { ...process.env, ...input.environment };
649
+ const turnModel = await resolveClaudeTurnModel(turn.model, turnEnvironment);
650
+ const injected = applyClaudeInject({ ...turnEnvironment }, turnModel);
651
+ if (injected.model)
652
+ args.push("--model", injected.model);
653
+ if (turn.effort)
654
+ args.push("--effort", turn.effort);
655
+ // A room prompt can contain section context, skills, memory, playbooks,
656
+ // and browser/agent instructions. Passing that text directly on argv
657
+ // exceeds Windows' CreateProcess command-line limit and surfaces as
658
+ // `spawn ENAMETOOLONG`. Claude accepts the same prompt from a file, so
659
+ // keep both the text and its potentially sensitive contents off argv.
660
+ let systemPromptPath = null;
661
+ // integrations → MCP servers; pre-allow their tools (a headless
662
+ // acceptEdits run silently denies anything unlisted)
663
+ const mcpServers = {};
664
+ const allowed = [];
665
+ if (turn.integrations?.composio) {
666
+ mcpServers.composio = { ...turn.integrations.composio };
667
+ allowed.push("mcp__composio");
668
+ }
669
+ if (turn.integrations?.computer) {
670
+ mcpServers.computer = {
671
+ command: process.execPath,
672
+ args: [PROXY_PATH],
673
+ env: { ...NODE_ENV_FLAG, ...computerProxyEnv(turn.integrations.computer) },
674
+ };
675
+ allowed.push("mcp__computer");
676
+ }
677
+ else if (turn.integrations?.localComputer) {
678
+ const local = turn.integrations.localComputer;
679
+ mcpServers.computer = {
680
+ command: local.command,
681
+ args: local.args,
682
+ env: local.env,
683
+ };
684
+ // The isolated Local VM preserves the established pre-allow behavior.
685
+ // Host tools always route through OpenMausBot's permission broker.
686
+ if (!controlsHost)
687
+ allowed.push("mcp__computer");
688
+ }
689
+ // peer-agent comms (list_bots/ask_bot) — the harness builds the whole
690
+ // spawn contract (command/args/env incl. the boot token) in
691
+ // agentsIntegration(); pre-allowing matters doubly here, or the CLI's
692
+ // own ListAgents look-alike shadows it and "@Bot" asks go nowhere
693
+ if (turn.integrations?.agents) {
694
+ // Coordination is foundational, not an optional deferred lookup.
695
+ // Claude waits for always-loaded tools before building the prompt.
696
+ mcpServers.agents = { ...turn.integrations.agents, alwaysLoad: true };
697
+ allowed.push("mcp__agents");
698
+ }
699
+ if (turn.integrations?.phone) {
700
+ mcpServers.phone = { ...turn.integrations.phone };
701
+ allowed.push("mcp__phone");
702
+ }
703
+ if (turn.integrations?.browser) {
704
+ mcpServers.browser = { ...turn.integrations.browser };
705
+ allowed.push("mcp__browser");
706
+ }
707
+ // dweb network daemon (status / repo / opencode model access) via
708
+ // server/drivers/dweb-proxy.ts — points at the configured dweb instance
709
+ if (turn.integrations?.dweb) {
710
+ mcpServers.dweb = {
711
+ command: process.execPath,
712
+ args: [DWEB_PROXY_PATH],
713
+ env: {
714
+ ...NODE_ENV_FLAG,
715
+ DWEB_URL: turn.integrations.dweb.url,
716
+ },
717
+ };
718
+ allowed.push("mcp__dweb");
719
+ }
720
+ // user-configured servers mount like any integration but are NOT
721
+ // pre-allowed: acceptEdits silently denies unlisted tools, which
722
+ // routes every custom tool call through the ogb permission broker
723
+ // into an Allow/Deny card. Reserved names were filtered upstream;
724
+ // skip any residual collision instead of clobbering a built-in.
725
+ for (const [name, server] of Object.entries(turn.integrations?.custom ?? {})) {
726
+ if (name in mcpServers)
727
+ continue;
728
+ mcpServers[name] = { ...server };
729
+ }
730
+ // Keep ask_user available even in Full access. Native bypass skips
731
+ // permission prompts, not questions requiring a person's answer.
732
+ let broker;
733
+ const socketPath = permissionSocketPath(threadId);
734
+ if (permissionMode !== "bypassPermissions") {
735
+ args.push("--permission-prompt-tool", "mcp__ogb__approve");
736
+ }
737
+ mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
738
+ allowed.push("mcp__ogb");
739
+ // The MCP config carries credentials — a Composio consumer key in a
740
+ // header, the box token in the computer proxy's env, the comms token in
741
+ // the agents proxy's env. On argv every one of those is world-readable
742
+ // through `ps` for the life of the turn, to any local process. The CLI
743
+ // accepts a FILE for this flag, so the secrets go in a 0600 file that
744
+ // is removed when the turn settles.
745
+ let mcpConfigPath = null;
746
+ if (Object.keys(mcpServers).length) {
747
+ mcpConfigPath = join(mkdtempSync(join(tmpdir(), "omb-mcp-")), "mcp.json");
748
+ args.push("--mcp-config", mcpConfigPath);
749
+ args.push("--allowedTools", allowed.join(","));
750
+ }
751
+ const env = claudeEnvironment(turnModel, turnEnvironment);
752
+ // Our approvals and browser credentials expire at the user-turn
753
+ // boundary. Native background workers cannot outlive that boundary;
754
+ // parallel bot work must use the harness's durable delegate_bot path.
755
+ env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS = "1";
756
+ const cwd = turn.cwd ?? homedir();
757
+ // Everything that shapes the process, minus session/turn-specific temp
758
+ // paths. Their contents are represented directly in the key instead.
759
+ const privateFileFlags = new Set(["--mcp-config"]);
760
+ const keyArgs = args.filter((a, i) => !privateFileFlags.has(a) && !privateFileFlags.has(args[i - 1] ?? ""));
761
+ const argsKey = JSON.stringify({
762
+ args: keyArgs,
763
+ system: turn.system ?? null,
764
+ mcpServers,
765
+ cwd,
766
+ model: injected.model ?? null,
767
+ base: env.ANTHROPIC_BASE_URL ?? null,
768
+ });
769
+ // Reuse the live process when it is idle, unchanged, and is the session
770
+ // the harness wants resumed. Anything else: close it and spawn fresh
771
+ // (with --resume, so the conversation continues in the new process).
772
+ const live = sessions.get(threadId);
773
+ if (live && !live.turn && !live.closing && live.child.exitCode === null && live.argsKey === argsKey && (!sessionId || sessionId === live.sessionId)) {
774
+ if (live.idleTimer)
775
+ clearTimeout(live.idleTimer);
776
+ live.turn = { turnId, settled: false, sawStreamDelta: false };
777
+ active.set(threadId, { stop: () => killCliTree(live.child), turnId, broker: live.broker });
778
+ emit({ ...base(threadId, turnId), type: "turn.started" });
779
+ const written = await writeUser(live, threadId, promptMsg);
780
+ if (!written) {
781
+ active.delete(threadId);
782
+ live.turn = null;
783
+ closeSession(threadId, "stdin write failed");
784
+ retryState.delete(threadId);
785
+ if (mcpConfigPath) {
786
+ try {
787
+ rmSync(dirname(mcpConfigPath), { recursive: true, force: true });
788
+ }
789
+ catch { }
790
+ }
791
+ throw new Error("claude session stdin is not writable");
792
+ }
793
+ // the MCP config was for the first spawn; nothing to clean here
794
+ if (mcpConfigPath) {
795
+ try {
796
+ rmSync(dirname(mcpConfigPath), { recursive: true, force: true });
797
+ }
798
+ catch { }
799
+ }
800
+ return { turnId };
801
+ }
802
+ if (live)
803
+ closeSession(threadId, "spawn contract changed");
804
+ // Until sessions.set() below, this turn owns every launch resource.
805
+ // Any bind, private-config or synchronous spawn failure must release
806
+ // them here rather than leave a live listener or credential temp file.
807
+ const cleanupUnownedLaunch = () => {
808
+ broker?.close();
809
+ broker = undefined;
810
+ if (mcpConfigPath) {
811
+ try {
812
+ rmSync(dirname(mcpConfigPath), { recursive: true, force: true });
813
+ }
814
+ catch { }
815
+ mcpConfigPath = null;
816
+ }
817
+ if (systemPromptPath) {
818
+ removePrivateTempDir(systemPromptPath);
819
+ systemPromptPath = null;
820
+ }
821
+ retryState.delete(threadId);
822
+ };
823
+ try {
824
+ // Create the prompt file only for a new process. A compatible live
825
+ // session has already consumed the same system prompt at launch.
826
+ if (turn.system) {
827
+ systemPromptPath = join(mkdtempSync(join(tmpdir(), "omb-system-")), "prompt.txt");
828
+ writeFileSync(systemPromptPath, turn.system, { mode: 0o600 });
829
+ args.push("--append-system-prompt-file", systemPromptPath);
830
+ }
831
+ // Only create a broker for a new process. A compatible retained
832
+ // process keeps its existing proxy connection and broker across turns.
833
+ if (socketPath) {
834
+ // remembers which tool each pending ask came from, so the resolved
835
+ // event can scope approvals to real desktop-control tools only
836
+ const askTools = new Map();
837
+ broker = await createPermissionBroker({
838
+ socketPaths: brokerSocketCandidates(threadId),
839
+ isActive: () => Boolean(sessions.get(threadId)?.turn),
840
+ onAsk: (ask) => {
841
+ const eventTurnId = sessions.get(threadId)?.turn?.turnId ?? turnId;
842
+ askTools.set(ask.id, typeof ask.tool === "string" ? ask.tool : undefined);
843
+ emit({
844
+ ...base(threadId, eventTurnId),
845
+ type: "request.opened",
846
+ requestId: ask.id,
847
+ requestType: ask.kind,
848
+ tool: ask.tool,
849
+ summary: askSummary(ask),
850
+ approvalScope: typeof ask.tool === "string" && controlsHost && ask.tool.startsWith("mcp__computer")
851
+ ? "local-computer"
852
+ : undefined,
853
+ choices: Array.isArray(ask.input?.choices) ? ask.input.choices.slice(0, 5) : undefined,
854
+ });
855
+ },
856
+ onResolve: (resolved) => {
857
+ const eventTurnId = sessions.get(threadId)?.turn?.turnId ?? turnId;
858
+ emit({
859
+ ...base(threadId, eventTurnId),
860
+ type: "request.resolved",
861
+ requestId: resolved.id,
862
+ behavior: resolved.behavior,
863
+ source: resolved.source,
864
+ approvalScope: controlsHost && typeof askTools.get(resolved.id) === "string" && askTools.get(resolved.id).startsWith("mcp__computer") ? "local-computer" : undefined,
865
+ });
866
+ askTools.delete(resolved.id);
867
+ },
868
+ });
869
+ // A fallback bind means the deterministic pipe is still held by an
870
+ // earlier process's child. The proxy learns its path from argv, so
871
+ // point it at the pipe we actually bound. argsKey deliberately keeps
872
+ // the base path: the nonce is not part of the spawn contract, and a
873
+ // retained session keeps its own broker object anyway.
874
+ if (broker.socketPath !== socketPath && mcpConfigPath) {
875
+ mcpServers.ogb = { command: process.execPath, args: [PERM_PROXY_PATH, broker.socketPath], env: { ...NODE_ENV_FLAG }, alwaysLoad: true };
876
+ }
877
+ }
878
+ // Write once, only after the broker has selected its real endpoint.
879
+ if (mcpConfigPath) {
880
+ writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 0o600 });
881
+ }
882
+ if (sessionId)
883
+ args.push("--resume", sessionId);
884
+ else
885
+ args.push("--session-id", newSessionId);
886
+ }
887
+ catch (error) {
888
+ cleanupUnownedLaunch();
889
+ throw error;
890
+ }
891
+ let child;
892
+ try {
893
+ child = spawnCli(config.cli, args, {
894
+ cwd,
895
+ env,
896
+ stdio: ["pipe", "pipe", "pipe"],
897
+ });
898
+ }
899
+ catch (error) {
900
+ cleanupUnownedLaunch();
901
+ throw error;
902
+ }
903
+ const session = {
904
+ child,
905
+ broker,
906
+ mcpConfigPath,
907
+ systemPromptPath,
908
+ argsKey,
909
+ sessionId: sessionId ?? newSessionId,
910
+ turn: { turnId, settled: false, sawStreamDelta: false },
911
+ idleTimer: null,
912
+ closing: false,
913
+ stderr: "",
914
+ };
915
+ sessions.set(threadId, session);
916
+ // settles the TURN, not the process: the CLI stays for the next
917
+ // message until it has been quiet for SESSION_IDLE_MS
918
+ const settle = (ok, stopReason, cost = null, usage) => {
919
+ const t = session.turn;
920
+ if (!t || t.settled)
921
+ return;
922
+ t.settled = true;
923
+ // Resolve any ask still open for this turn, but keep the broker
924
+ // listening for the next turn on the retained process. Between turns
925
+ // isActive() rejects late background asks without creating cards.
926
+ session.broker?.pause();
927
+ // the config file holds live credentials — the CLI read it at start;
928
+ // it must not sit on disk for the life of the session
929
+ if (session.mcpConfigPath) {
930
+ try {
931
+ rmSync(dirname(session.mcpConfigPath), { recursive: true, force: true });
932
+ }
933
+ catch { }
934
+ session.mcpConfigPath = null;
935
+ }
936
+ if (session.systemPromptPath) {
937
+ if (removePrivateTempDir(session.systemPromptPath))
938
+ session.systemPromptPath = null;
939
+ }
940
+ active.delete(threadId);
941
+ session.turn = null;
942
+ // A settled turn owns no retry budget. Retained CLI sessions may run
943
+ // many later turns on this thread, and each must start fresh.
944
+ retryState.delete(threadId);
945
+ emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
946
+ if (session.child.exitCode === null && !session.closing)
947
+ armIdle(threadId);
948
+ };
949
+ const currentTurnId = () => session.turn?.turnId ?? turnId;
950
+ const handleLine = (line) => {
951
+ let o;
952
+ try {
953
+ o = JSON.parse(line);
954
+ }
955
+ catch {
956
+ return;
957
+ }
958
+ appendNative(threadId, { dir: "in", source: "claude.sdk.message", msg: o });
959
+ switch (o.type) {
960
+ case "system":
961
+ if (o.subtype === "init") {
962
+ if (typeof o.session_id === "string")
963
+ session.sessionId = o.session_id;
964
+ emit({ ...base(threadId, currentTurnId()), type: "session.started", sessionId: o.session_id, model: o.model });
965
+ }
966
+ else if (o.subtype === "thinking_tokens") {
967
+ emit({ ...base(threadId, currentTurnId()), type: "item.updated", itemType: "reasoning", tokens: o.estimated_tokens });
968
+ }
969
+ break;
970
+ case "stream_event": {
971
+ // subagent narration is dropped — N parallel Tasks would
972
+ // interleave their prose into one bubble (upstream-verified bug)
973
+ if (o.parent_tool_use_id)
974
+ break;
975
+ const ev = o.event ?? {};
976
+ if (ev.type !== "content_block_delta")
977
+ break;
978
+ const d = ev.delta ?? {};
979
+ if (d.type === "text_delta" && typeof d.text === "string" && d.text) {
980
+ if (session.turn)
981
+ session.turn.sawStreamDelta = true;
982
+ emit({ ...base(threadId, currentTurnId()), type: "content.delta", streamKind: "assistant_text", delta: d.text });
983
+ }
984
+ else if (d.type === "thinking_delta" && typeof d.thinking === "string" && d.thinking) {
985
+ emit({ ...base(threadId, currentTurnId()), type: "content.delta", streamKind: "reasoning_text", delta: d.thinking });
986
+ }
987
+ break;
988
+ }
989
+ case "assistant": {
990
+ const msg = o.message ?? {};
991
+ const text = firstText(msg.content);
992
+ if (text.trim()) {
993
+ // fallback delta for CLIs/paths that never streamed the block
994
+ if (!session.turn?.sawStreamDelta) {
995
+ emit({ ...base(threadId, currentTurnId()), type: "content.delta", streamKind: "assistant_text", delta: text });
996
+ }
997
+ if (session.turn)
998
+ session.turn.sawStreamDelta = false;
999
+ emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "assistant_text", text });
1000
+ }
1001
+ for (const b of Array.isArray(msg.content) ? msg.content : []) {
1002
+ if (b.type === "tool_use") {
1003
+ emit({ ...base(threadId, currentTurnId()), type: "item.started", itemType: "tool", itemId: b.id, title: b.name });
1004
+ }
1005
+ }
1006
+ if (msg.usage) {
1007
+ emit({
1008
+ ...base(threadId, currentTurnId()),
1009
+ type: "thread.token-usage.updated",
1010
+ input: (msg.usage.input_tokens || 0) + (msg.usage.cache_read_input_tokens || 0),
1011
+ output: msg.usage.output_tokens || 0,
1012
+ ...(typeof msg.usage.cache_read_input_tokens === "number"
1013
+ ? { cachedInput: msg.usage.cache_read_input_tokens }
1014
+ : {}),
1015
+ });
1016
+ }
1017
+ break;
1018
+ }
1019
+ case "user":
1020
+ for (const b of Array.isArray(o.message?.content) ? o.message.content : []) {
1021
+ if (b.type === "tool_result") {
1022
+ emit({ ...base(threadId, currentTurnId()), type: "item.completed", itemType: "tool", itemId: b.tool_use_id, ok: !b.is_error });
1023
+ }
1024
+ }
1025
+ break;
1026
+ case "result":
1027
+ // A synthetic background completion is not the result of the
1028
+ // submitted user turn. Settling it would revoke browser access
1029
+ // and deny approvals while that user turn is still running.
1030
+ if (o.origin?.kind === "task-notification")
1031
+ break;
1032
+ // result.usage is this invocation's total — one process per turn,
1033
+ // so it is the turn's figure. cache reads count as input: they
1034
+ // are billed (at the cache rate) and they fill the window — but
1035
+ // they are reported separately too, so the UI can show how much
1036
+ // of the figure was context re-read rather than new text.
1037
+ settle(o.is_error !== true, o.stop_reason ?? o.terminal_reason ?? null, o.total_cost_usd ?? null, o.usage
1038
+ ? {
1039
+ input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
1040
+ output: o.usage.output_tokens || 0,
1041
+ ...(typeof o.usage.cache_read_input_tokens === "number"
1042
+ ? { cachedInput: o.usage.cache_read_input_tokens }
1043
+ : {}),
1044
+ }
1045
+ : undefined);
1046
+ break;
1047
+ }
1048
+ };
1049
+ let buf = "";
1050
+ // decode as UTF-8 across chunk boundaries — a raw `buf += chunk` splits
1051
+ // multibyte characters that straddle two reads and corrupts the text
1052
+ child.stdout.setEncoding("utf8");
1053
+ child.stdout.on("data", (chunk) => {
1054
+ buf += chunk;
1055
+ let nl;
1056
+ while ((nl = buf.indexOf("\n")) !== -1) {
1057
+ const line = buf.slice(0, nl);
1058
+ buf = buf.slice(nl + 1);
1059
+ if (line.trim())
1060
+ handleLine(line);
1061
+ }
1062
+ });
1063
+ child.stderr.on("data", (c) => {
1064
+ session.stderr += c;
1065
+ if (session.stderr.length > 8192)
1066
+ session.stderr = session.stderr.slice(-8192);
1067
+ });
1068
+ child.on("error", (e) => {
1069
+ emit({ ...base(threadId, currentTurnId()), type: "runtime.error", ...describeSpawnFailure(e, config.cli) });
1070
+ settle(false, "spawn_error");
1071
+ });
1072
+ child.on("close", (code) => {
1073
+ // a turn still running when the process died is a failed turn; a
1074
+ // process that exited between turns (idle close, contract change)
1075
+ // is just a session ending
1076
+ if (session.turn && !session.turn.settled) {
1077
+ const message = `claude exited ${code} before result${session.stderr ? `: ${session.stderr.trim().slice(-300)}` : ""}`;
1078
+ const verdict = classifyError({ exitCode: code, stderr: message });
1079
+ if (!retry.cancelled &&
1080
+ code !== 0 &&
1081
+ verdict.transient &&
1082
+ !session.turn.sawStreamDelta &&
1083
+ retry.attempt < RETRY_MAX_ATTEMPTS - 1) {
1084
+ // the CLI is gone but the TURN continues: keep the thread busy,
1085
+ // emit no terminal event, and relaunch after the backoff. The
1086
+ // `active` entry STAYS — it is what makes an interrupt during
1087
+ // the backoff reach this turn's stop() and cancel the retry.
1088
+ const failedBroker = session.broker;
1089
+ session.broker = undefined;
1090
+ failedBroker?.pause();
1091
+ failedBroker?.close();
1092
+ if (session.mcpConfigPath) {
1093
+ try {
1094
+ rmSync(dirname(session.mcpConfigPath), { recursive: true, force: true });
1095
+ }
1096
+ catch { }
1097
+ session.mcpConfigPath = null;
1098
+ }
1099
+ if (session.systemPromptPath) {
1100
+ removePrivateTempDir(session.systemPromptPath);
1101
+ session.systemPromptPath = null;
1102
+ }
1103
+ sessions.delete(threadId);
1104
+ session.turn = null;
1105
+ retry.attempt++;
1106
+ const delayMs = computeBackoff(retry.attempt - 1);
1107
+ emit({
1108
+ ...base(threadId, turnId),
1109
+ type: "turn.retrying",
1110
+ attempt: retry.attempt,
1111
+ delayMs,
1112
+ reason: verdict.reason,
1113
+ });
1114
+ void (async () => {
1115
+ const wait = interruptibleDelay(delayMs * retryScale, retryAbort.signal);
1116
+ await wait.promise;
1117
+ // an interrupt during the backoff landed here via stop(); the
1118
+ // turn settles as interrupted and no zombie relaunch happens
1119
+ if (retry.cancelled) {
1120
+ active.delete(threadId);
1121
+ retryState.delete(threadId);
1122
+ emit({
1123
+ ...base(threadId, turnId),
1124
+ type: "turn.completed",
1125
+ ok: false,
1126
+ stopReason: "interrupted",
1127
+ cost: null,
1128
+ });
1129
+ return;
1130
+ }
1131
+ // hand the thread back before recursing — the relaunch's own
1132
+ // guard would otherwise reject it as "already running"
1133
+ active.delete(threadId);
1134
+ try {
1135
+ const cursor = session.sessionId ?? sessionId ?? undefined;
1136
+ await sendTurn({ ...turn, resumeCursor: cursor });
1137
+ }
1138
+ catch (e) {
1139
+ retryState.delete(threadId);
1140
+ emit({
1141
+ ...base(threadId, turnId),
1142
+ type: "runtime.error",
1143
+ message: e instanceof Error ? e.message : String(e),
1144
+ });
1145
+ emit({
1146
+ ...base(threadId, turnId),
1147
+ type: "turn.completed",
1148
+ ok: false,
1149
+ stopReason: "exit_before_result",
1150
+ cost: null,
1151
+ });
1152
+ }
1153
+ })();
1154
+ return;
1155
+ }
1156
+ retryState.delete(threadId);
1157
+ emit({
1158
+ ...base(threadId, currentTurnId()),
1159
+ type: "runtime.error",
1160
+ message,
1161
+ });
1162
+ settle(false, "exit_before_result");
1163
+ }
1164
+ if (session.idleTimer)
1165
+ clearTimeout(session.idleTimer);
1166
+ session.broker?.close();
1167
+ if (session.mcpConfigPath) {
1168
+ try {
1169
+ rmSync(dirname(session.mcpConfigPath), { recursive: true, force: true });
1170
+ }
1171
+ catch { }
1172
+ }
1173
+ removePrivateTempDir(session.systemPromptPath);
1174
+ if (sessions.get(threadId) === session)
1175
+ sessions.delete(threadId);
1176
+ });
1177
+ const stop = () => {
1178
+ retry.cancelled = true;
1179
+ retryAbort.abort();
1180
+ killCliTree(child);
1181
+ };
1182
+ active.set(threadId, { stop, turnId, broker });
1183
+ emit({ ...base(threadId, turnId), type: "turn.started" });
1184
+ // prompt over stdin as a stream-json message — never argv (ARG_MAX).
1185
+ // stdin stays OPEN: that is what keeps the session alive for a
1186
+ // mid-turn steer or the next turn; closeSession() ends it.
1187
+ if (!(await writeUser(session, threadId, promptMsg))) {
1188
+ settle(false, "stdin_write_failed");
1189
+ closeSession(threadId, "stdin write failed");
1190
+ }
1191
+ return { turnId };
1192
+ };
1193
+ /** A user message into the running turn: the CLI delivers it before its
1194
+ * next model call. False when nothing is running here to steer. */
1195
+ const steer = async (threadId, text) => {
1196
+ const s = sessions.get(threadId);
1197
+ if (!s || !s.turn || s.turn.settled || s.closing || s.child.exitCode !== null)
1198
+ return false;
1199
+ return writeUser(s, threadId, claudeUserMessage(text, undefined));
1200
+ };
1201
+ const snapshot = async () => {
1202
+ const env = claudeEnvironment(undefined, { ...process.env, ...input.environment });
1203
+ const version = await new Promise((resolve) => {
1204
+ execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout.trim()));
1205
+ });
1206
+ if (!version)
1207
+ return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
1208
+ const authenticated = await claudeSignedIn(config.cli, env);
1209
+ // claudeEnvironment strips ANTHROPIC_API_KEY, so turns run on the
1210
+ // CLI's own login (Pro/Max): the cost it reports is what the call
1211
+ // WOULD bill, not a charge
1212
+ return { state: "available", version, authenticated, billing: "subscription" };
1213
+ };
1214
+ /** One-shot Claude call with the prompt on stdin, never argv. Approval
1215
+ * summaries can contain paths, commands, or secrets, so the generic
1216
+ * `claude -p "prompt"` shape is not safe for review. No tools or MCP
1217
+ * servers are mounted in this isolated process. */
1218
+ const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
1219
+ const child = spawnCli(config.cli, ["-p", "--model", "claude-haiku-4-5", "--output-format", "text"], {
1220
+ stdio: ["pipe", "pipe", "pipe"],
1221
+ env: claudeEnvironment("claude-haiku-4-5", { ...process.env, ...input.environment }),
1222
+ });
1223
+ let stdout = "";
1224
+ let stderr = "";
1225
+ let settled = false;
1226
+ const finish = (error) => {
1227
+ if (settled)
1228
+ return;
1229
+ settled = true;
1230
+ clearTimeout(timer);
1231
+ signal?.removeEventListener("abort", onAbort);
1232
+ if (error)
1233
+ reject(error);
1234
+ else
1235
+ resolve(stdout.trim());
1236
+ };
1237
+ const onAbort = () => {
1238
+ killCliTree(child);
1239
+ finish(new Error("Claude review aborted"));
1240
+ };
1241
+ const timer = setTimeout(() => {
1242
+ killCliTree(child);
1243
+ finish(new Error("Claude review timed out"));
1244
+ }, 60_000);
1245
+ timer.unref?.();
1246
+ child.stdout.setEncoding("utf8");
1247
+ child.stderr.setEncoding("utf8");
1248
+ child.stdout.on("data", (chunk) => {
1249
+ stdout += chunk;
1250
+ if (stdout.length > 1_000_000) {
1251
+ killCliTree(child);
1252
+ finish(new Error("Claude review output exceeded 1 MB"));
1253
+ }
1254
+ });
1255
+ child.stderr.on("data", (chunk) => {
1256
+ stderr = (stderr + chunk).slice(-8_192);
1257
+ });
1258
+ child.on("error", (error) => finish(error));
1259
+ child.on("close", (code) => {
1260
+ if (code === 0)
1261
+ finish();
1262
+ else
1263
+ finish(new Error(stderr.trim() || `Claude review exited ${code}`));
1264
+ });
1265
+ if (signal?.aborted)
1266
+ onAbort();
1267
+ else {
1268
+ signal?.addEventListener("abort", onAbort, { once: true });
1269
+ child.stdin.end(prompt);
1270
+ }
1271
+ });
1272
+ return {
1273
+ instanceId,
1274
+ driverKind: DRIVER_KIND,
1275
+ displayName: input.displayName,
1276
+ enabled: input.enabled,
1277
+ get models() {
1278
+ return models;
1279
+ },
1280
+ refreshModels,
1281
+ snapshot,
1282
+ adapter: {
1283
+ provider: DRIVER_KIND,
1284
+ capabilities: {
1285
+ sessionModelSwitch: "in-session",
1286
+ agentsMcp: true,
1287
+ customMcp: true,
1288
+ computerMcp: true,
1289
+ composioMcp: true,
1290
+ phoneMcp: true,
1291
+ browserMcp: true,
1292
+ images: true,
1293
+ nativeImageInput: true,
1294
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
1295
+ queueing: true,
1296
+ // Harness turns reassert a per-bot mode and restore the broker even
1297
+ // when an old instance was configured with bypassPermissions.
1298
+ localComputerMcp: true,
1299
+ },
1300
+ sendTurn,
1301
+ steer,
1302
+ interruptTurn: async (threadId) => active.get(threadId)?.stop(),
1303
+ respondToRequest: async (threadId, requestId, decision) => {
1304
+ // fail-closed by construction: no broker, or an ask that already
1305
+ // timed out / settled, is `unavailable` — the caller denies
1306
+ const broker = sessions.get(threadId)?.broker ?? active.get(threadId)?.broker;
1307
+ if (!broker)
1308
+ return "unavailable";
1309
+ const behavior = decision.behavior === "answer" ? "answer" : decision.behavior;
1310
+ if (!broker.answer(requestId, behavior, decision.message))
1311
+ return "unavailable";
1312
+ return behavior === "allow" ? "allowed-once" : behavior === "answer" ? "answered" : "rejected";
1313
+ },
1314
+ hasSession: (threadId) => active.has(threadId),
1315
+ stopAll: async () => {
1316
+ for (const { stop } of active.values())
1317
+ stop();
1318
+ for (const threadId of [...sessions.keys()])
1319
+ closeSession(threadId, "stopAll");
1320
+ },
1321
+ onEvent: (listener) => {
1322
+ listeners.add(listener);
1323
+ return () => listeners.delete(listener);
1324
+ },
1325
+ },
1326
+ generateText: (prompt) => generateReview(prompt),
1327
+ reviewPermission: generateReview,
1328
+ dispose: async () => {
1329
+ for (const { stop } of active.values())
1330
+ stop();
1331
+ for (const threadId of [...sessions.keys()])
1332
+ closeSession(threadId, "dispose");
1333
+ listeners.clear();
1334
+ },
1335
+ };
1336
+ },
1337
+ };