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.
- package/LICENSE +202 -0
- package/README.md +14 -0
- package/cli.js +2 -0
- package/dist/app-icon.svg +29 -0
- package/dist/assets/abap-BdImnpbu.js +1 -0
- package/dist/assets/actionscript-3-B3316cI-.js +1 -0
- package/dist/assets/ada-bCR0ucgS.js +1 -0
- package/dist/assets/ahk-CsyLZFj1.js +1 -0
- package/dist/assets/ahk2-8Zs4aa1G.js +1 -0
- package/dist/assets/andromeeda-C4gqWexZ.js +1 -0
- package/dist/assets/angular-html-DA-rfuFy.js +1 -0
- package/dist/assets/angular-ts-BrjP3tb8.js +1 -0
- package/dist/assets/apache-Pmp26Uib.js +1 -0
- package/dist/assets/apex-DhZFqWV2.js +1 -0
- package/dist/assets/apl-CORt7UWP.js +1 -0
- package/dist/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/assets/ara-BRHolxvo.js +1 -0
- package/dist/assets/asciidoc-CSVQ5wI8.js +1 -0
- package/dist/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/assets/astro-HNnZUWAn.js +1 -0
- package/dist/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/assets/awk-DMzUqQB5.js +1 -0
- package/dist/assets/ayu-dark-DYE7WIF3.js +1 -0
- package/dist/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/assets/ayu-mirage-32ctXXKs.js +1 -0
- package/dist/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/assets/bat-CickPsom.js +1 -0
- package/dist/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/assets/berry-uYugtg8r.js +1 -0
- package/dist/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/assets/bird2-Bx8U0n9b.js +1 -0
- package/dist/assets/blade-2xfisSek.js +1 -0
- package/dist/assets/bsl-DlhNcFeZ.js +1 -0
- package/dist/assets/c-BIGW1oBm.js +1 -0
- package/dist/assets/c3-Dp5svz6Z.js +1 -0
- package/dist/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/assets/catppuccin-frappe-CZL1YF0i.js +1 -0
- package/dist/assets/catppuccin-latte-DH-8KZSZ.js +1 -0
- package/dist/assets/catppuccin-macchiato-B7yYVSCf.js +1 -0
- package/dist/assets/catppuccin-mocha-Ct7hS0mc.js +1 -0
- package/dist/assets/chapel-DTp_pixX.js +1 -0
- package/dist/assets/clarity-Dn5IMItf.js +1 -0
- package/dist/assets/clojure-P80f7IUj.js +1 -0
- package/dist/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/assets/cobol-nBiQ_Alo.js +1 -0
- package/dist/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/assets/coq-C7JzOVbR.js +1 -0
- package/dist/assets/cpp-BMRokrvK.js +1 -0
- package/dist/assets/crystal-DGywbUpC.js +1 -0
- package/dist/assets/csharp-DSvCPggb.js +1 -0
- package/dist/assets/css-CLj8gQPS.js +1 -0
- package/dist/assets/csv-fuZLfV_i.js +1 -0
- package/dist/assets/cue-D82EKSYY.js +1 -0
- package/dist/assets/cypher-COkxafJQ.js +1 -0
- package/dist/assets/d-85-TOEBH.js +1 -0
- package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/assets/dart-bE4Kk8sk.js +1 -0
- package/dist/assets/dax-CEL-wOlO.js +1 -0
- package/dist/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/assets/diff-D97Zzqfu.js +1 -0
- package/dist/assets/docker-BcOcwvcX.js +1 -0
- package/dist/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/assets/edge-FbVlp4U3.js +1 -0
- package/dist/assets/elixir-CkH2-t6x.js +1 -0
- package/dist/assets/elm-DbKCFpqz.js +1 -0
- package/dist/assets/emacs-lisp-C_m_b--Z.js +1 -0
- package/dist/assets/erb-DXfck5VN.js +1 -0
- package/dist/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/assets/fennel-BYunw83y.js +1 -0
- package/dist/assets/fish-BvzEVeQv.js +1 -0
- package/dist/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/assets/gdresource-TyuKm33G.js +1 -0
- package/dist/assets/gdscript-DqcFQ5yU.js +1 -0
- package/dist/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/assets/genie-D0YGMca9.js +1 -0
- package/dist/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/assets/gleam-BspZqrRM.js +1 -0
- package/dist/assets/glimmer-js-ByusRIyA.js +1 -0
- package/dist/assets/glimmer-ts-BfAWNZQY.js +1 -0
- package/dist/assets/glsl-DplSGwfg.js +1 -0
- package/dist/assets/gn-n2N0HUVH.js +1 -0
- package/dist/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/assets/go-C27-OAKa.js +1 -0
- package/dist/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/assets/hack-BWmVpMyf.js +1 -0
- package/dist/assets/haml-D5jkg6IW.js +1 -0
- package/dist/assets/handlebars-BpdQsYii.js +1 -0
- package/dist/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/assets/haxe-CfZj7gIn.js +1 -0
- package/dist/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/assets/hjson-D5-asLiD.js +1 -0
- package/dist/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/assets/horizon-BUw7H-hv.js +1 -0
- package/dist/assets/horizon-bright-CUuTKBJd.js +1 -0
- package/dist/assets/houston-DnULxvSX.js +1 -0
- package/dist/assets/html-derivative-DlHx6ybY.js +1 -0
- package/dist/assets/html-pp8916En.js +1 -0
- package/dist/assets/http-jrhK8wxY.js +1 -0
- package/dist/assets/hurl-irOxFIW8.js +1 -0
- package/dist/assets/hxml-2-FPmUDs.js +1 -0
- package/dist/assets/hy-DFXneXwc.js +1 -0
- package/dist/assets/imba-DGztddWO.js +1 -0
- package/dist/assets/index-B9rLIY81.js +263 -0
- package/dist/assets/index-CJBrtDbC.css +1 -0
- package/dist/assets/index-D-u0LYsV.js +153 -0
- package/dist/assets/ini-BEwlwnbL.js +1 -0
- package/dist/assets/java-CylS5w8V.js +1 -0
- package/dist/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/assets/jinja-f2NsQr07.js +1 -0
- package/dist/assets/jison-wvAkD_A8.js +1 -0
- package/dist/assets/json-Cp-IABpG.js +1 -0
- package/dist/assets/json5-C9tS-k6U.js +1 -0
- package/dist/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/assets/julia-5Bft2YPA.js +1 -0
- package/dist/assets/just-Cwhn7H3k.js +1 -0
- package/dist/assets/kanagawa-dragon-BuwD2xS4.js +1 -0
- package/dist/assets/kanagawa-lotus-C3DzagqV.js +1 -0
- package/dist/assets/kanagawa-wave-DFGoQZhC.js +1 -0
- package/dist/assets/kdl-DV7GczEv.js +1 -0
- package/dist/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/assets/kusto-wEQ09or8.js +1 -0
- package/dist/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/assets/latex-D5pSuvFb.js +1 -0
- package/dist/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/assets/less-B1dDrJ26.js +1 -0
- package/dist/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/assets/liquid-C0sCDyMI.js +1 -0
- package/dist/assets/llvm-BZoOZj88.js +1 -0
- package/dist/assets/log-2UxHyX5q.js +1 -0
- package/dist/assets/logo-BtOb2qkB.js +1 -0
- package/dist/assets/lua-BaeVxFsk.js +1 -0
- package/dist/assets/luau-BnpPk5vE.js +1 -0
- package/dist/assets/make-CHLpvVh8.js +1 -0
- package/dist/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/assets/marko-DjSrsDqO.js +1 -0
- package/dist/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/assets/matlab-D7o27uSR.js +1 -0
- package/dist/assets/mdc-D1_yUvq7.js +1 -0
- package/dist/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/assets/mermaid-CQcHuHx7.js +1 -0
- package/dist/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/assets/min-light-CTRr51gU.js +1 -0
- package/dist/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/assets/mojo-DJz3ZmWd.js +1 -0
- package/dist/assets/monokai-BRVnQi9A.js +1 -0
- package/dist/assets/moonbit-CHtswR0a.js +1 -0
- package/dist/assets/move-el3G9tDJ.js +1 -0
- package/dist/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/assets/nextflow-C-mBbutL.js +1 -0
- package/dist/assets/nextflow-groovy-vE_lwT2v.js +1 -0
- package/dist/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/assets/night-owl-light-CMTm3GFP.js +1 -0
- package/dist/assets/nim-BIad80T-.js +1 -0
- package/dist/assets/nix-CwoSXNpI.js +1 -0
- package/dist/assets/nord-Ddv68eIx.js +1 -0
- package/dist/assets/nsis-BlV79W_Q.js +1 -0
- package/dist/assets/nushell-D3jzshHO.js +1 -0
- package/dist/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/assets/odin-BBf5iR-q.js +1 -0
- package/dist/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/assets/one-light-C3Wv6jpd.js +1 -0
- package/dist/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/assets/org-DM6o9KBp.js +1 -0
- package/dist/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/assets/perl-B9cMNwum.js +1 -0
- package/dist/assets/php-Csjmro_R.js +1 -0
- package/dist/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/assets/po-BTJTHyun.js +1 -0
- package/dist/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/assets/polar-C0HS_06l.js +1 -0
- package/dist/assets/postcss-CXtECtnM.js +1 -0
- package/dist/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/assets/powershell-BmBUJMz7.js +1 -0
- package/dist/assets/prisma-Vru482bI.js +1 -0
- package/dist/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/assets/pug-DKIMFp6K.js +1 -0
- package/dist/assets/puppet-BMWR74SV.js +1 -0
- package/dist/assets/purescript-CklMAg4u.js +1 -0
- package/dist/assets/python-B6aJPvgy.js +1 -0
- package/dist/assets/qml-3beO22l8.js +1 -0
- package/dist/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/assets/qss-IeuSbFQv.js +1 -0
- package/dist/assets/r-Cf5RLm7j.js +1 -0
- package/dist/assets/racket-BqYA7rlc.js +1 -0
- package/dist/assets/raku-DXvB9xmW.js +1 -0
- package/dist/assets/razor-BjBPvh-w.js +1 -0
- package/dist/assets/rbs-CpoqiR4B.js +1 -0
- package/dist/assets/red-hvxz__6c.js +1 -0
- package/dist/assets/reg-C-SQnVFl.js +1 -0
- package/dist/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/assets/rel-C3B-1QV4.js +1 -0
- package/dist/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/assets/ron-D8l8udqQ.js +1 -0
- package/dist/assets/rose-pine-5qJOZa0Y.js +1 -0
- package/dist/assets/rose-pine-dawn-zx0QlTCp.js +1 -0
- package/dist/assets/rose-pine-moon-C9pEdX9L.js +1 -0
- package/dist/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/assets/rst-bs7f0vWN.js +1 -0
- package/dist/assets/ruby-C0TQ7zu5.js +1 -0
- package/dist/assets/rust-B1yitclQ.js +1 -0
- package/dist/assets/sas-DEy46yEz.js +1 -0
- package/dist/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/assets/scala-CqE71os6.js +1 -0
- package/dist/assets/scheme-C98Dy4si.js +1 -0
- package/dist/assets/scss-D5BDwBP9.js +1 -0
- package/dist/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/assets/smalltalk-BOQMe2GC.js +1 -0
- package/dist/assets/smithy-cds9vsN8.js +1 -0
- package/dist/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/assets/solarized-dark-CpvCGNkr.js +1 -0
- package/dist/assets/solarized-light-Dlz6yCKv.js +1 -0
- package/dist/assets/solidity-DijEV5ha.js +1 -0
- package/dist/assets/soy-8wufbnw4.js +1 -0
- package/dist/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/assets/sql-CRqJ_cUM.js +1 -0
- package/dist/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/assets/stata-DI20mbqo.js +1 -0
- package/dist/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/assets/surrealql-Cjom0U5J.js +1 -0
- package/dist/assets/svelte-Cy7k_4gC.js +1 -0
- package/dist/assets/swift-C2oV4EkX.js +1 -0
- package/dist/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/assets/system-verilog-0hqHdDBg.js +1 -0
- package/dist/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/assets/templ-DhtptRzy.js +1 -0
- package/dist/assets/terraform-BETggiCN.js +1 -0
- package/dist/assets/tex-D96PA37w.js +1 -0
- package/dist/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/assets/toml-vGWfd6FD.js +1 -0
- package/dist/assets/ts-tags-D351s5mN.js +1 -0
- package/dist/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/assets/tsx-COt5Ahok.js +1 -0
- package/dist/assets/turtle-BsS91CYL.js +1 -0
- package/dist/assets/twig-27uCiNez.js +1 -0
- package/dist/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/assets/typespec-CAFt9gP4.js +1 -0
- package/dist/assets/typst-BUadGCkm.js +1 -0
- package/dist/assets/v-BGw2Nkan.js +1 -0
- package/dist/assets/vala-CsfeWuGM.js +1 -0
- package/dist/assets/vb-Cu-pLBUe.js +1 -0
- package/dist/assets/verilog-nZwndyjY.js +1 -0
- package/dist/assets/vesper-DRje8inN.js +1 -0
- package/dist/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/assets/viml-CJc9bBzg.js +1 -0
- package/dist/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/assets/vue-BqiEGhQt.js +1 -0
- package/dist/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/assets/vue-vine-BoDAl6tE.js +1 -0
- package/dist/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/assets/wit-5i3qLPDT.js +1 -0
- package/dist/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/assets/yaml-Buea-lGh.js +1 -0
- package/dist/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/assets/zig-VOosw3JB.js +1 -0
- package/dist/index.html +14 -0
- package/dist-server/companion/src/listener.js +187 -0
- package/dist-server/computer-proxy.js +1185 -0
- package/dist-server/connector-proxy.js +186 -0
- package/dist-server/container-mcp.js +15186 -0
- package/dist-server/drivers/agents-proxy.js +887 -0
- package/dist-server/drivers/browser-proxy.js +15037 -0
- package/dist-server/drivers/dweb-proxy.js +175 -0
- package/dist-server/drivers/phone-proxy.js +339 -0
- package/dist-server/drivers/pi-mcp-extension.ts +638 -0
- package/dist-server/index.js +64869 -0
- package/dist-server/local-computer.js +198 -0
- package/dist-server/mcp-server.js +1233 -0
- package/dist-server/openmausbot.js +3671 -0
- package/dist-server/pair-cli.js +3671 -0
- package/dist-server/permission-proxy.js +126 -0
- package/dist-server/prepare-cloudflared.js +253 -0
- package/dist-server/proxy-paths.js +29 -0
- package/dist-server/server/atomic.js +40 -0
- package/dist-server/server/attachments.js +471 -0
- package/dist-server/server/auto-approve.js +162 -0
- package/dist-server/server/auto-review.js +79 -0
- package/dist-server/server/avatar-image.js +144 -0
- package/dist-server/server/bot-cwd.js +29 -0
- package/dist-server/server/bot-directory.js +127 -0
- package/dist-server/server/bot-package.js +268 -0
- package/dist-server/server/bot-profile.js +76 -0
- package/dist-server/server/box-create-idempotency.js +463 -0
- package/dist-server/server/box.js +944 -0
- package/dist-server/server/brand.js +65 -0
- package/dist-server/server/browser-connection.js +187 -0
- package/dist-server/server/browser-lifecycle-cleanup.js +359 -0
- package/dist-server/server/calendar-calls.js +314 -0
- package/dist-server/server/channel-queue.js +70 -0
- package/dist-server/server/checkpoints.js +405 -0
- package/dist-server/server/chief-of-staff.js +49 -0
- package/dist-server/server/claude-update.js +70 -0
- package/dist-server/server/cli.js +524 -0
- package/dist-server/server/cloud-backend.js +30 -0
- package/dist-server/server/comms-visibility.js +109 -0
- package/dist-server/server/composio.js +945 -0
- package/dist-server/server/computer-control.js +149 -0
- package/dist-server/server/computer-observation.js +133 -0
- package/dist-server/server/computer-proxy.js +1061 -0
- package/dist-server/server/config.js +760 -0
- package/dist-server/server/connector-proxy.js +228 -0
- package/dist-server/server/container-computer.js +840 -0
- package/dist-server/server/container-mcp.js +27 -0
- package/dist-server/server/contracts.js +24 -0
- package/dist-server/server/control-client.js +100 -0
- package/dist-server/server/data-dir-lease.js +3 -0
- package/dist-server/server/decision-log.js +106 -0
- package/dist-server/server/delegations.js +628 -0
- package/dist-server/server/drivers/acp/core.js +904 -0
- package/dist-server/server/drivers/acp/cursor.js +365 -0
- package/dist-server/server/drivers/acp/custom.js +51 -0
- package/dist-server/server/drivers/acp/droid.js +225 -0
- package/dist-server/server/drivers/acp/gemini.js +95 -0
- package/dist-server/server/drivers/acp/grok.js +249 -0
- package/dist-server/server/drivers/acp/hermes.js +429 -0
- package/dist-server/server/drivers/acp/kimi.js +559 -0
- package/dist-server/server/drivers/acp/opencode-go.js +370 -0
- package/dist-server/server/drivers/acp/qwen.js +98 -0
- package/dist-server/server/drivers/agents-proxy.js +941 -0
- package/dist-server/server/drivers/antigravity-acp.js +507 -0
- package/dist-server/server/drivers/antigravity-release.js +65 -0
- package/dist-server/server/drivers/antigravity-runtime.js +395 -0
- package/dist-server/server/drivers/antigravity.js +144 -0
- package/dist-server/server/drivers/boxagent.js +245 -0
- package/dist-server/server/drivers/browser-proxy.js +520 -0
- package/dist-server/server/drivers/builtIn.js +36 -0
- package/dist-server/server/drivers/claude.js +1337 -0
- package/dist-server/server/drivers/codex-catalog.js +409 -0
- package/dist-server/server/drivers/codex.js +1116 -0
- package/dist-server/server/drivers/dweb-proxy.js +193 -0
- package/dist-server/server/drivers/grok.js +48 -0
- package/dist-server/server/drivers/grokagent.js +4 -0
- package/dist-server/server/drivers/local-inject.js +388 -0
- package/dist-server/server/drivers/minimax.js +108 -0
- package/dist-server/server/drivers/native.js +21 -0
- package/dist-server/server/drivers/openai-chat.js +240 -0
- package/dist-server/server/drivers/openai-compat.js +143 -0
- package/dist-server/server/drivers/phone-proxy.js +372 -0
- package/dist-server/server/drivers/pi.js +855 -0
- package/dist-server/server/drivers/retry.js +96 -0
- package/dist-server/server/enterprise.js +93 -0
- package/dist-server/server/env-path.js +329 -0
- package/dist-server/server/environment.js +105 -0
- package/dist-server/server/generated-image.js +39 -0
- package/dist-server/server/graceful-shutdown.js +24 -0
- package/dist-server/server/group-goal-run.js +124 -0
- package/dist-server/server/harness/bus.js +86 -0
- package/dist-server/server/harness/registry.js +217 -0
- package/dist-server/server/index.js +11634 -0
- package/dist-server/server/installed-playbooks.js +34 -0
- package/dist-server/server/local-computer.js +278 -0
- package/dist-server/server/local-routing.js +9 -0
- package/dist-server/server/local-vm-idle.js +44 -0
- package/dist-server/server/local-vm-inventory.js +34 -0
- package/dist-server/server/local-vm-lease.js +58 -0
- package/dist-server/server/mcp-bridge.js +251 -0
- package/dist-server/server/mcp-probe.js +153 -0
- package/dist-server/server/mcp-registry.js +130 -0
- package/dist-server/server/member-turn.js +6 -0
- package/dist-server/server/message-db.js +358 -0
- package/dist-server/server/message-file.js +439 -0
- package/dist-server/server/names.js +26 -0
- package/dist-server/server/notify.js +69 -0
- package/dist-server/server/openmaus-status-capsule.js +329 -0
- package/dist-server/server/openmausbot.js +6 -0
- package/dist-server/server/package-export.js +157 -0
- package/dist-server/server/pair-cli.js +7 -0
- package/dist-server/server/peer-approval-key.js +5 -0
- package/dist-server/server/peer-approval.js +182 -0
- package/dist-server/server/peer-provenance.js +44 -0
- package/dist-server/server/peer-roster.js +139 -0
- package/dist-server/server/permission-proxy.js +157 -0
- package/dist-server/server/phone-secret.js +358 -0
- package/dist-server/server/private-screen-capture.js +17 -0
- package/dist-server/server/procs.js +123 -0
- package/dist-server/server/project-scout.js +304 -0
- package/dist-server/server/proxy-paths.js +47 -0
- package/dist-server/server/redact.js +102 -0
- package/dist-server/server/remote-computer.js +170 -0
- package/dist-server/server/repeat-detector.js +48 -0
- package/dist-server/server/replies.js +38 -0
- package/dist-server/server/request-auth.js +327 -0
- package/dist-server/server/room-cwd.js +15 -0
- package/dist-server/server/room-post-budget.js +112 -0
- package/dist-server/server/room-turn-timeout.js +115 -0
- package/dist-server/server/routine-requests.js +871 -0
- package/dist-server/server/routines.js +1111 -0
- package/dist-server/server/schema.js +11 -0
- package/dist-server/server/screen-frame-gate.js +80 -0
- package/dist-server/server/section-context.js +85 -0
- package/dist-server/server/send-idempotency.js +53 -0
- package/dist-server/server/sessions.js +319 -0
- package/dist-server/server/skill-fetch.js +148 -0
- package/dist-server/server/skill-learn.js +61 -0
- package/dist-server/server/skill-library.js +116 -0
- package/dist-server/server/skills.js +1223 -0
- package/dist-server/server/steer-queue.js +114 -0
- package/dist-server/server/store.js +1217 -0
- package/dist-server/server/surface.js +107 -0
- package/dist-server/server/tailscale.js +103 -0
- package/dist-server/server/team-backup.js +183 -0
- package/dist-server/server/team-library.js +205 -0
- package/dist-server/server/team-manifest.js +224 -0
- package/dist-server/server/thread-events.js +226 -0
- package/dist-server/server/tts/elevenlabs.js +92 -0
- package/dist-server/server/tts/index.js +79 -0
- package/dist-server/server/tts/speech-text.js +195 -0
- package/dist-server/server/tts/system-voices.js +74 -0
- package/dist-server/server/tunnel.js +202 -0
- package/dist-server/server/turn-context.js +40 -0
- package/dist-server/server/turn-dispatch-guard.js +152 -0
- package/dist-server/server/turn-images.js +138 -0
- package/dist-server/server/turn-watchdog.js +65 -0
- package/dist-server/server/vps-computer.js +1099 -0
- package/dist-server/server/vps-container-mcp.js +31 -0
- package/dist-server/server/webhook-ingress.js +173 -0
- package/dist-server/server/webhooks.js +491 -0
- package/dist-server/server/workspace.js +178 -0
- package/dist-server/shared/approval-mode.js +39 -0
- package/dist-server/shared/bot-avatar.js +33 -0
- package/dist-server/shared/bot-profile.js +7 -0
- package/dist-server/shared/connector-availability.js +12 -0
- package/dist-server/shared/credential-request.js +82 -0
- package/dist-server/shared/group-goal-run.js +1 -0
- package/dist-server/shared/import-name.js +11 -0
- package/dist-server/shared/mascot-bodies.js +107 -0
- package/dist-server/shared/routine-request.js +10 -0
- package/dist-server/shared/routine-run.js +1 -0
- package/dist-server/shared/skill-request.js +16 -0
- package/dist-server/shared/team-backup.js +148 -0
- package/dist-server/src/lib/composer-attachments.js +676 -0
- package/dist-server/tunnel-guardian.js +390 -0
- package/dist-server/vps-container-mcp.js +15209 -0
- package/package.json +32 -0
- package/skills/create-verification-skill/SKILL.md +65 -0
- package/skills/create-verification-skill/manifest.json +13 -0
- package/skills/phone-harness/SKILL.md +26 -0
- package/skills/phone-harness/agents/openai.yaml +13 -0
- package/skills/phone-harness/manifest.json +17 -0
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
// Bot + thread persistence. bots.json holds bot records (including the
|
|
2
|
+
// thread→instance binding and per-instance resume cursors — upstream's
|
|
3
|
+
// ProviderSessionDirectory, recipe step 6: persist the binding from day
|
|
4
|
+
// one). messages-<threadId>.json holds the folded transcript.
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync, rmSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
9
|
+
import { peerAllowKey } from "./peer-approval-key.js";
|
|
10
|
+
import { DATA_DIR, loadBrowserProfileIdAliases } from "./config.js";
|
|
11
|
+
import * as mdb from "./message-db.js";
|
|
12
|
+
import { workspaceDir } from "./workspace.js";
|
|
13
|
+
import { newId } from "./contracts.js";
|
|
14
|
+
import { pickBotName } from "./names.js";
|
|
15
|
+
import { redactSecretsInText } from "./redact.js";
|
|
16
|
+
import { botAvatarProfile } from "../shared/bot-avatar.js";
|
|
17
|
+
import { isApprovalMode } from "../shared/approval-mode.js";
|
|
18
|
+
/** Everything the BOT authored is scrubbed of content-shaped secrets before
|
|
19
|
+
* it is stored: its reply text, a tool title (an ACP engine's title can be
|
|
20
|
+
* the whole command line), a permission card's summary. What the user typed
|
|
21
|
+
* is theirs and stays as typed. Stored, not just displayed: the transcript
|
|
22
|
+
* is replayed into every rebuild, and a leaked key would otherwise be
|
|
23
|
+
* permanent. */
|
|
24
|
+
function redactBotAuthored(message) {
|
|
25
|
+
if (message.role !== "bot")
|
|
26
|
+
return message;
|
|
27
|
+
const out = { ...message };
|
|
28
|
+
if (typeof out.text === "string")
|
|
29
|
+
out.text = redactSecretsInText(out.text);
|
|
30
|
+
if (out.tool?.name)
|
|
31
|
+
out.tool = { ...out.tool, name: redactSecretsInText(out.tool.name) };
|
|
32
|
+
if (out.routineRun) {
|
|
33
|
+
const routineRun = { ...out.routineRun };
|
|
34
|
+
routineRun.routineName = redactSecretsInText(routineRun.routineName);
|
|
35
|
+
if (routineRun.summary)
|
|
36
|
+
routineRun.summary = redactSecretsInText(routineRun.summary);
|
|
37
|
+
if (routineRun.error)
|
|
38
|
+
routineRun.error = redactSecretsInText(routineRun.error);
|
|
39
|
+
out.routineRun = routineRun;
|
|
40
|
+
}
|
|
41
|
+
if (out.goalRun) {
|
|
42
|
+
out.goalRun = {
|
|
43
|
+
...out.goalRun,
|
|
44
|
+
goal: redactSecretsInText(out.goalRun.goal),
|
|
45
|
+
coordinatorName: redactSecretsInText(out.goalRun.coordinatorName),
|
|
46
|
+
detail: out.goalRun.detail ? redactSecretsInText(out.goalRun.detail) : undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (out.card) {
|
|
50
|
+
const card = { ...out.card };
|
|
51
|
+
card.title = redactSecretsInText(card.title);
|
|
52
|
+
if (typeof card.subtitle === "string")
|
|
53
|
+
card.subtitle = redactSecretsInText(card.subtitle);
|
|
54
|
+
if (typeof card.summary === "string")
|
|
55
|
+
card.summary = redactSecretsInText(card.summary);
|
|
56
|
+
if (typeof card.held === "string")
|
|
57
|
+
card.held = redactSecretsInText(card.held);
|
|
58
|
+
// Routine definitions are executable bot-authored text stored behind the
|
|
59
|
+
// visible summary. Scrub the durable payload too so nesting it on a card
|
|
60
|
+
// cannot bypass the transcript's secret-redaction boundary.
|
|
61
|
+
if (card.routineRequest) {
|
|
62
|
+
const operation = card.routineRequest.operation;
|
|
63
|
+
card.routineRequest = {
|
|
64
|
+
...card.routineRequest,
|
|
65
|
+
operation: operation.action === "create"
|
|
66
|
+
? {
|
|
67
|
+
...operation,
|
|
68
|
+
routine: {
|
|
69
|
+
...operation.routine,
|
|
70
|
+
name: redactSecretsInText(operation.routine.name),
|
|
71
|
+
instructions: redactSecretsInText(operation.routine.instructions),
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
: operation.action === "update"
|
|
75
|
+
? {
|
|
76
|
+
...operation,
|
|
77
|
+
changes: {
|
|
78
|
+
...operation.changes,
|
|
79
|
+
...(typeof operation.changes.name === "string"
|
|
80
|
+
? { name: redactSecretsInText(operation.changes.name) }
|
|
81
|
+
: {}),
|
|
82
|
+
...(typeof operation.changes.instructions === "string"
|
|
83
|
+
? { instructions: redactSecretsInText(operation.changes.instructions) }
|
|
84
|
+
: {}),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
: { ...operation },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (card.skillRequest) {
|
|
91
|
+
const originalPreview = card.skillRequest.preview;
|
|
92
|
+
const preview = originalPreview === undefined
|
|
93
|
+
? undefined
|
|
94
|
+
: redactSecretsInText(originalPreview);
|
|
95
|
+
// Current skill proposals are scrubbed before staging and their digest
|
|
96
|
+
// binds the card to the exact SKILL.md bytes that apply will install.
|
|
97
|
+
// Keep that binding only when this store-wide safety pass is a no-op and
|
|
98
|
+
// the supplied digest already matches the persisted preview. A caller
|
|
99
|
+
// that bypassed staging (or an older malformed card) is therefore
|
|
100
|
+
// safely deny-only instead of showing one document and approving
|
|
101
|
+
// another.
|
|
102
|
+
const previewSha256 = preview !== undefined && preview === originalPreview
|
|
103
|
+
? createHash("sha256").update(preview).digest("hex")
|
|
104
|
+
: undefined;
|
|
105
|
+
const sha256 = card.skillRequest.sha256 !== undefined
|
|
106
|
+
&& card.skillRequest.sha256 === previewSha256
|
|
107
|
+
? card.skillRequest.sha256
|
|
108
|
+
: undefined;
|
|
109
|
+
card.skillRequest = {
|
|
110
|
+
...card.skillRequest,
|
|
111
|
+
gist: redactSecretsInText(card.skillRequest.gist),
|
|
112
|
+
source: card.skillRequest.source === undefined
|
|
113
|
+
? undefined
|
|
114
|
+
: redactSecretsInText(card.skillRequest.source),
|
|
115
|
+
preview,
|
|
116
|
+
sha256,
|
|
117
|
+
warnings: card.skillRequest.warnings.map((warning) => redactSecretsInText(warning)),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
out.card = card;
|
|
121
|
+
}
|
|
122
|
+
if (out.connector) {
|
|
123
|
+
out.connector = {
|
|
124
|
+
...out.connector,
|
|
125
|
+
label: redactSecretsInText(out.connector.label),
|
|
126
|
+
description: redactSecretsInText(out.connector.description),
|
|
127
|
+
error: out.connector.error ? redactSecretsInText(out.connector.error) : undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (out.secret) {
|
|
131
|
+
out.secret = {
|
|
132
|
+
...out.secret,
|
|
133
|
+
label: redactSecretsInText(out.secret.label),
|
|
134
|
+
description: redactSecretsInText(out.secret.description),
|
|
135
|
+
error: out.secret.error ? redactSecretsInText(out.secret.error) : undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/** The states in which the bot cannot take a new message. */
|
|
141
|
+
export const ACTIVITY_BUSY = new Set(["working", "waiting-on-you", "no-signal"]);
|
|
142
|
+
/** What a task is called before its first message names it. */
|
|
143
|
+
export const UNTITLED_TASK = "New task";
|
|
144
|
+
/** A task's name, taken from the first thing you asked it to do. */
|
|
145
|
+
export function titleFromMessage(text) {
|
|
146
|
+
const line = text.trim().split("\n")[0].trim();
|
|
147
|
+
return line.length > 48 ? `${line.slice(0, 47)}…` : line || UNTITLED_TASK;
|
|
148
|
+
}
|
|
149
|
+
const BOTS_FILE = join(DATA_DIR, "bots.json");
|
|
150
|
+
const GROUPS_FILE = join(DATA_DIR, "groups.json");
|
|
151
|
+
const messagesFile = (threadId) => join(DATA_DIR, `messages-${threadId}.json`);
|
|
152
|
+
const COLORS = [
|
|
153
|
+
"green",
|
|
154
|
+
"blue",
|
|
155
|
+
"red",
|
|
156
|
+
"orange",
|
|
157
|
+
"purple",
|
|
158
|
+
"cyan",
|
|
159
|
+
"pink",
|
|
160
|
+
"yellow",
|
|
161
|
+
"teal",
|
|
162
|
+
"coral",
|
|
163
|
+
];
|
|
164
|
+
/** Sections are persisted as display labels, so exact trimmed labels are
|
|
165
|
+
* their identity. Missing/blank means the unsectioned (General) team. */
|
|
166
|
+
export const sectionKey = (section) => section?.trim() || "";
|
|
167
|
+
/** Resolve @mentions in a message against a bot roster: `@` must start a
|
|
168
|
+
* word, the name must end on a word boundary (so "@New Bottle" never matches
|
|
169
|
+
* "New Bot"), names match case-insensitively, longest name wins (so
|
|
170
|
+
* "@New Bot 2" never half-matches "New Bot"), hidden bots skipped, results
|
|
171
|
+
* deduped. Callers pre-filter the sender out of `peers`. */
|
|
172
|
+
export function mentionedBots(text, peers) {
|
|
173
|
+
const candidates = peers
|
|
174
|
+
.filter((p) => !p.hidden && p.name.trim())
|
|
175
|
+
.sort((a, b) => b.name.length - a.name.length);
|
|
176
|
+
const lower = text.toLowerCase();
|
|
177
|
+
const found = [];
|
|
178
|
+
let at = -1;
|
|
179
|
+
while ((at = lower.indexOf("@", at + 1)) !== -1) {
|
|
180
|
+
if (at > 0 && !/\s/.test(text[at - 1]))
|
|
181
|
+
continue; // user@host, not a tag
|
|
182
|
+
const rest = lower.slice(at + 1);
|
|
183
|
+
const hit = candidates.find((p) => {
|
|
184
|
+
const name = p.name.toLowerCase();
|
|
185
|
+
if (!rest.startsWith(name))
|
|
186
|
+
return false;
|
|
187
|
+
const after = rest[name.length]; // must not run into a longer word
|
|
188
|
+
return after === undefined || !/[a-z0-9]/i.test(after);
|
|
189
|
+
});
|
|
190
|
+
if (hit && !found.includes(hit))
|
|
191
|
+
found.push(hit);
|
|
192
|
+
}
|
|
193
|
+
return found;
|
|
194
|
+
}
|
|
195
|
+
/** Normalize persisted or API-provided routing. Old rooms did not have this
|
|
196
|
+
* field; giving them their first member as lead fixes the old silent-send
|
|
197
|
+
* behavior without making every prompt fan out to every model. */
|
|
198
|
+
export function normalizeGroupDefaultResponder(value, memberIds, dm = false) {
|
|
199
|
+
if (dm)
|
|
200
|
+
return { kind: "mentions" };
|
|
201
|
+
if (value && typeof value === "object") {
|
|
202
|
+
const candidate = value;
|
|
203
|
+
if (candidate.kind === "everyone")
|
|
204
|
+
return { kind: "everyone" };
|
|
205
|
+
if (candidate.kind === "mentions")
|
|
206
|
+
return { kind: "mentions" };
|
|
207
|
+
if (candidate.kind === "member" &&
|
|
208
|
+
typeof candidate.botId === "string" &&
|
|
209
|
+
memberIds.includes(candidate.botId)) {
|
|
210
|
+
return { kind: "member", botId: candidate.botId };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (memberIds.length === 0)
|
|
214
|
+
return { kind: "mentions" };
|
|
215
|
+
return { kind: "member", botId: memberIds[0] };
|
|
216
|
+
}
|
|
217
|
+
/** Resolve the bots invoked by a human room message. Explicit targets win;
|
|
218
|
+
* otherwise the room policy chooses one member, everyone, or nobody. */
|
|
219
|
+
export function roomResponders(text, members, defaultResponder) {
|
|
220
|
+
const available = members.filter((member) => !member.hidden);
|
|
221
|
+
if (/(?:^|\s)@everyone\b/i.test(text))
|
|
222
|
+
return available;
|
|
223
|
+
const mentioned = mentionedBots(text, available);
|
|
224
|
+
if (mentioned.length)
|
|
225
|
+
return mentioned;
|
|
226
|
+
if (defaultResponder.kind === "everyone")
|
|
227
|
+
return available;
|
|
228
|
+
if (defaultResponder.kind === "member") {
|
|
229
|
+
const lead = available.find((member) => member.id === defaultResponder.botId);
|
|
230
|
+
return lead ? [lead] : [];
|
|
231
|
+
}
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
const onboardingCard = () => ({
|
|
235
|
+
title: "What do you mostly want help with?",
|
|
236
|
+
subtitle: "Pick whatever's closest; we can always expand from there.",
|
|
237
|
+
options: ["Work & projects", "Writing & research", "Life admin", "A bit of everything"],
|
|
238
|
+
});
|
|
239
|
+
export class Store {
|
|
240
|
+
bots = [];
|
|
241
|
+
groups = [];
|
|
242
|
+
threads = new Map();
|
|
243
|
+
defaultSelection;
|
|
244
|
+
listeners = new Set();
|
|
245
|
+
constructor(defaultSelection) {
|
|
246
|
+
this.defaultSelection = defaultSelection;
|
|
247
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
248
|
+
try {
|
|
249
|
+
this.bots = JSON.parse(readFileSync(BOTS_FILE, "utf8"));
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
this.bots = [];
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
this.groups = JSON.parse(readFileSync(GROUPS_FILE, "utf8"));
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
this.groups = [];
|
|
259
|
+
}
|
|
260
|
+
// busy never survives a restart — no turn does either. Rooms saved
|
|
261
|
+
// before default responders existed adopt their first member as lead.
|
|
262
|
+
let botsMigrated = false;
|
|
263
|
+
const browserProfileAliases = loadBrowserProfileIdAliases();
|
|
264
|
+
const chiefSectionsSeen = new Set();
|
|
265
|
+
let groupsMigrated = false;
|
|
266
|
+
for (const b of this.bots) {
|
|
267
|
+
// transient state never survives a restart — and if a previous
|
|
268
|
+
// process died mid-turn, bots.json still says busy/working; persist
|
|
269
|
+
// the reset so the next load does not read it again
|
|
270
|
+
if (b.busy || (b.activity !== undefined && b.activity !== "idle"))
|
|
271
|
+
botsMigrated = true;
|
|
272
|
+
b.busy = false;
|
|
273
|
+
b.activity = "idle";
|
|
274
|
+
if (b.browserProfile) {
|
|
275
|
+
const browserProfile = browserProfileAliases.get(b.browserProfile);
|
|
276
|
+
if (browserProfile && browserProfile !== b.browserProfile) {
|
|
277
|
+
b.browserProfile = browserProfile;
|
|
278
|
+
botsMigrated = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (b.cloudBackend !== undefined && b.cloudBackend !== "box" && b.cloudBackend !== "vps") {
|
|
282
|
+
delete b.cloudBackend;
|
|
283
|
+
botsMigrated = true;
|
|
284
|
+
}
|
|
285
|
+
if (b.autoStartVps !== undefined && b.autoStartVps !== true && b.autoStartVps !== false) {
|
|
286
|
+
delete b.autoStartVps;
|
|
287
|
+
botsMigrated = true;
|
|
288
|
+
}
|
|
289
|
+
if (b.approvalMode !== undefined && !isApprovalMode(b.approvalMode)) {
|
|
290
|
+
delete b.approvalMode;
|
|
291
|
+
botsMigrated = true;
|
|
292
|
+
}
|
|
293
|
+
// A trusted elevation is a prepare/confirm/activate commit. If the
|
|
294
|
+
// desktop process or its private reply path died before activation,
|
|
295
|
+
// the durable marker survives beside the mode in the same atomic
|
|
296
|
+
// bots.json write. Revoke it before schedulers, listeners, or HTTP can
|
|
297
|
+
// start any new work.
|
|
298
|
+
if (b.approvalGrant !== undefined) {
|
|
299
|
+
b.approvalMode = "ask";
|
|
300
|
+
b.autoApprove = false;
|
|
301
|
+
delete b.approvalGrant;
|
|
302
|
+
botsMigrated = true;
|
|
303
|
+
}
|
|
304
|
+
const avatar = botAvatarProfile(b);
|
|
305
|
+
if (b.avatarUrl !== undefined && avatar.avatarUrl !== b.avatarUrl) {
|
|
306
|
+
delete b.avatarUrl;
|
|
307
|
+
botsMigrated = true;
|
|
308
|
+
}
|
|
309
|
+
if (b.avatarCrop !== undefined && avatar.avatarCrop !== b.avatarCrop) {
|
|
310
|
+
delete b.avatarCrop;
|
|
311
|
+
botsMigrated = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const b of this.bots) {
|
|
315
|
+
if (!b.chiefOfStaff)
|
|
316
|
+
continue;
|
|
317
|
+
const key = sectionKey(b.section);
|
|
318
|
+
if (!chiefSectionsSeen.has(key)) {
|
|
319
|
+
chiefSectionsSeen.add(key);
|
|
320
|
+
if (b.hidden) {
|
|
321
|
+
b.hidden = false;
|
|
322
|
+
botsMigrated = true;
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
b.chiefOfStaff = false;
|
|
327
|
+
botsMigrated = true;
|
|
328
|
+
}
|
|
329
|
+
// Peer grants originally used mutable display names (ask_bot:@Helper).
|
|
330
|
+
// Convert only when exactly one bot has that name; ambiguous legacy
|
|
331
|
+
// entries remain inert rather than granting access to the wrong bot.
|
|
332
|
+
for (const b of this.bots) {
|
|
333
|
+
if (!b.alwaysAllow?.length)
|
|
334
|
+
continue;
|
|
335
|
+
let changed = false;
|
|
336
|
+
const migrated = b.alwaysAllow.map((key) => {
|
|
337
|
+
const match = key.match(/^(ask_bot|delegate_bot):@(.+)$/);
|
|
338
|
+
if (!match)
|
|
339
|
+
return key;
|
|
340
|
+
const candidates = this.bots.filter((candidate) => candidate.name === match[2]);
|
|
341
|
+
if (candidates.length !== 1)
|
|
342
|
+
return key;
|
|
343
|
+
changed = true;
|
|
344
|
+
return peerAllowKey(match[1], candidates[0].id);
|
|
345
|
+
});
|
|
346
|
+
if (changed) {
|
|
347
|
+
b.alwaysAllow = [...new Set(migrated)];
|
|
348
|
+
botsMigrated = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (const g of this.groups) {
|
|
352
|
+
g.busyBotId = null;
|
|
353
|
+
const normalized = normalizeGroupDefaultResponder(g.defaultResponder, g.memberIds, Boolean(g.dm));
|
|
354
|
+
if (JSON.stringify(normalized) !== JSON.stringify(g.defaultResponder))
|
|
355
|
+
groupsMigrated = true;
|
|
356
|
+
g.defaultResponder = normalized;
|
|
357
|
+
// Bot-to-bot channels intentionally remain one canonical thread.
|
|
358
|
+
if (g.dm) {
|
|
359
|
+
if (g.tasks !== undefined) {
|
|
360
|
+
delete g.tasks;
|
|
361
|
+
groupsMigrated = true;
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (!g.tasks?.length) {
|
|
366
|
+
const initialTask = {
|
|
367
|
+
threadId: g.threadId,
|
|
368
|
+
title: this.firstUserLine(g.threadId) ?? UNTITLED_TASK,
|
|
369
|
+
createdAt: g.createdAt,
|
|
370
|
+
};
|
|
371
|
+
if (g.pinnedCwd !== undefined)
|
|
372
|
+
initialTask.pinnedCwd = g.pinnedCwd;
|
|
373
|
+
if (g.pinnedMessageId)
|
|
374
|
+
initialTask.pinnedMessageId = g.pinnedMessageId;
|
|
375
|
+
g.tasks = [initialTask];
|
|
376
|
+
groupsMigrated = true;
|
|
377
|
+
}
|
|
378
|
+
// Repair a malformed/stale active pointer conservatively. Every task
|
|
379
|
+
// transcript is retained; the newest known task becomes active.
|
|
380
|
+
let active = g.tasks.find((task) => task.threadId === g.threadId);
|
|
381
|
+
if (!active) {
|
|
382
|
+
active = g.tasks[0];
|
|
383
|
+
g.threadId = active.threadId;
|
|
384
|
+
groupsMigrated = true;
|
|
385
|
+
}
|
|
386
|
+
g.pinnedCwd = active.pinnedCwd;
|
|
387
|
+
g.pinnedMessageId = active.pinnedMessageId;
|
|
388
|
+
}
|
|
389
|
+
if (botsMigrated)
|
|
390
|
+
this.saveBots();
|
|
391
|
+
if (groupsMigrated)
|
|
392
|
+
this.saveGroups();
|
|
393
|
+
// bots saved before tasks existed have one endless thread; adopt it as
|
|
394
|
+
// their first task so nothing is lost and nothing special-cases it
|
|
395
|
+
for (const b of this.bots) {
|
|
396
|
+
if (b.tasks?.length)
|
|
397
|
+
continue;
|
|
398
|
+
b.tasks = [
|
|
399
|
+
{
|
|
400
|
+
threadId: b.threadId,
|
|
401
|
+
title: this.firstUserLine(b.threadId) ?? UNTITLED_TASK,
|
|
402
|
+
createdAt: b.createdAt,
|
|
403
|
+
resumeCursors: b.resumeCursors ?? {},
|
|
404
|
+
},
|
|
405
|
+
];
|
|
406
|
+
}
|
|
407
|
+
// Search reads SQLite directly, so migrate every known legacy transcript
|
|
408
|
+
// at startup rather than waiting until the user happens to open it. Only
|
|
409
|
+
// pending JSON files are touched; already-migrated threads stay lazy.
|
|
410
|
+
const knownThreads = new Set([
|
|
411
|
+
...this.bots.flatMap((b) => [b.threadId, ...(b.tasks ?? []).map((task) => task.threadId)]),
|
|
412
|
+
...this.groups.flatMap((group) => [group.threadId, ...(group.tasks ?? []).map((task) => task.threadId)]),
|
|
413
|
+
]);
|
|
414
|
+
for (const threadId of knownThreads) {
|
|
415
|
+
const legacyFile = messagesFile(threadId);
|
|
416
|
+
if (existsSync(legacyFile))
|
|
417
|
+
mdb.readThread(threadId, legacyFile);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
saveBots(bots = this.bots) {
|
|
421
|
+
writeFileAtomic(BOTS_FILE, JSON.stringify(bots, null, 2));
|
|
422
|
+
}
|
|
423
|
+
saveGroups() {
|
|
424
|
+
writeFileAtomic(GROUPS_FILE, JSON.stringify(this.groups.map(({ busyBotId: _busyBotId, ...g }) => g), null, 2));
|
|
425
|
+
}
|
|
426
|
+
// ── groups ────────────────────────────────────────────────────────────
|
|
427
|
+
/** Subscribe to every write. Listeners run after the write and after
|
|
428
|
+
* save; a throwing listener never breaks the write. */
|
|
429
|
+
onChange(listener) {
|
|
430
|
+
this.listeners.add(listener);
|
|
431
|
+
return () => this.listeners.delete(listener);
|
|
432
|
+
}
|
|
433
|
+
emit(change) {
|
|
434
|
+
for (const listener of [...this.listeners]) {
|
|
435
|
+
try {
|
|
436
|
+
listener(change);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
console.error("store: change listener threw", error);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
group(id) {
|
|
444
|
+
return this.groups.find((g) => g.id === id);
|
|
445
|
+
}
|
|
446
|
+
groupByThread(threadId) {
|
|
447
|
+
return this.groups.find((group) => group.threadId === threadId || group.tasks?.some((task) => task.threadId === threadId));
|
|
448
|
+
}
|
|
449
|
+
createGroup(name, memberIds, dm = false, section, setup) {
|
|
450
|
+
const threadId = newId();
|
|
451
|
+
const createdAt = Date.now();
|
|
452
|
+
const group = {
|
|
453
|
+
id: newId(),
|
|
454
|
+
threadId,
|
|
455
|
+
name,
|
|
456
|
+
memberIds,
|
|
457
|
+
defaultResponder: dm
|
|
458
|
+
? { kind: "mentions" }
|
|
459
|
+
: normalizeGroupDefaultResponder(setup?.defaultResponder, memberIds, false),
|
|
460
|
+
bulletin: setup?.bulletin ?? "",
|
|
461
|
+
unread: false,
|
|
462
|
+
createdAt,
|
|
463
|
+
dm: dm || undefined,
|
|
464
|
+
busyBotId: null,
|
|
465
|
+
section,
|
|
466
|
+
};
|
|
467
|
+
if (!dm) {
|
|
468
|
+
group.tasks = [{ threadId, title: UNTITLED_TASK, createdAt }];
|
|
469
|
+
group.setupCompletedAt = setup?.completed ? createdAt : null;
|
|
470
|
+
group.setupSkippedAt = null;
|
|
471
|
+
}
|
|
472
|
+
this.groups.unshift(group);
|
|
473
|
+
this.saveGroups();
|
|
474
|
+
this.emit({ type: "group", groupId: group.id });
|
|
475
|
+
return group;
|
|
476
|
+
}
|
|
477
|
+
/** The bot⇄bot channel for a pair, if it exists (order-insensitive). */
|
|
478
|
+
dmGroup(a, b) {
|
|
479
|
+
return this.groups.find((g) => g.dm && g.memberIds.length === 2 && g.memberIds.includes(a) && g.memberIds.includes(b));
|
|
480
|
+
}
|
|
481
|
+
patchGroup(id, patch) {
|
|
482
|
+
const group = this.group(id);
|
|
483
|
+
if (!group)
|
|
484
|
+
return null;
|
|
485
|
+
Object.assign(group, patch);
|
|
486
|
+
if (!group.dm && Object.prototype.hasOwnProperty.call(patch, "pinnedMessageId")) {
|
|
487
|
+
const active = this.activeGroupTask(group.id);
|
|
488
|
+
if (active)
|
|
489
|
+
active.pinnedMessageId = patch.pinnedMessageId;
|
|
490
|
+
}
|
|
491
|
+
group.defaultResponder = normalizeGroupDefaultResponder(group.defaultResponder, group.memberIds, Boolean(group.dm));
|
|
492
|
+
this.saveGroups();
|
|
493
|
+
this.emit({ type: "group", groupId: group.id });
|
|
494
|
+
return group;
|
|
495
|
+
}
|
|
496
|
+
/** A thread's durable record: DB rows plus any legacy JSON leftovers. */
|
|
497
|
+
deleteThreadRecord(threadId) {
|
|
498
|
+
this.threads.delete(threadId);
|
|
499
|
+
mdb.deleteThread(threadId);
|
|
500
|
+
for (const file of [messagesFile(threadId), `${messagesFile(threadId)}.imported`]) {
|
|
501
|
+
try {
|
|
502
|
+
unlinkSync(file);
|
|
503
|
+
}
|
|
504
|
+
catch { }
|
|
505
|
+
}
|
|
506
|
+
this.emit({ type: "thread.deleted", threadId });
|
|
507
|
+
}
|
|
508
|
+
deleteGroup(id) {
|
|
509
|
+
const group = this.group(id);
|
|
510
|
+
if (!group)
|
|
511
|
+
return false;
|
|
512
|
+
this.groups = this.groups.filter((g) => g.id !== id);
|
|
513
|
+
this.saveGroups();
|
|
514
|
+
for (const threadId of new Set([group.threadId, ...(group.tasks ?? []).map((task) => task.threadId)])) {
|
|
515
|
+
this.deleteThreadRecord(threadId);
|
|
516
|
+
}
|
|
517
|
+
this.emit({ type: "group.deleted", groupId: id });
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
/** A process restart cannot preserve an in-flight room orchestrator. Close
|
|
521
|
+
* every durable working receipt before clients load it, including manual
|
|
522
|
+
* goals that do not have a RoutineRun record to reconcile separately. */
|
|
523
|
+
reconcileInterruptedGroupGoals(resolve, fallbackDetail = "OpenMausBot restarted before this goal finished.", fallbackFinishedAt = Date.now()) {
|
|
524
|
+
const ownedThreadIds = new Set();
|
|
525
|
+
for (const group of this.groups) {
|
|
526
|
+
ownedThreadIds.add(group.threadId);
|
|
527
|
+
for (const task of group.tasks ?? [])
|
|
528
|
+
ownedThreadIds.add(task.threadId);
|
|
529
|
+
}
|
|
530
|
+
// load() already migrated every legacy transcript file into SQLite, so
|
|
531
|
+
// this recovery query is proportional to unfinished goals, not history.
|
|
532
|
+
let recovered = 0;
|
|
533
|
+
for (const hit of mdb.workingGoalRunMessages()) {
|
|
534
|
+
if (!ownedThreadIds.has(hit.threadId) || !hit.message.goalRun)
|
|
535
|
+
continue;
|
|
536
|
+
const resolution = resolve?.(hit.message.goalRun.runId, hit.threadId) ?? {
|
|
537
|
+
status: "failed",
|
|
538
|
+
detail: fallbackDetail,
|
|
539
|
+
finishedAt: fallbackFinishedAt,
|
|
540
|
+
};
|
|
541
|
+
const state = resolution.status === "needs-input"
|
|
542
|
+
? "needs your input"
|
|
543
|
+
: resolution.status === "limit-reached"
|
|
544
|
+
? "reached its turn limit"
|
|
545
|
+
: resolution.status;
|
|
546
|
+
this.patchMessage(hit.threadId, hit.message.id, {
|
|
547
|
+
text: `Goal ${state}: ${resolution.detail}`,
|
|
548
|
+
goalRun: {
|
|
549
|
+
...hit.message.goalRun,
|
|
550
|
+
status: resolution.status,
|
|
551
|
+
detail: resolution.detail,
|
|
552
|
+
finishedAt: resolution.finishedAt,
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
recovered += 1;
|
|
556
|
+
}
|
|
557
|
+
return recovered;
|
|
558
|
+
}
|
|
559
|
+
// ── channel tasks ────────────────────────────────────────────────────
|
|
560
|
+
groupTasks(groupId) {
|
|
561
|
+
const group = this.group(groupId);
|
|
562
|
+
return group?.dm ? [] : (group?.tasks ?? []);
|
|
563
|
+
}
|
|
564
|
+
activeGroupTask(groupId) {
|
|
565
|
+
const group = this.group(groupId);
|
|
566
|
+
return group?.tasks?.find((task) => task.threadId === group.threadId);
|
|
567
|
+
}
|
|
568
|
+
groupTaskByThread(groupId, threadId) {
|
|
569
|
+
const group = this.group(groupId);
|
|
570
|
+
if (!group || group.dm)
|
|
571
|
+
return undefined;
|
|
572
|
+
return group.tasks?.find((task) => task.threadId === threadId);
|
|
573
|
+
}
|
|
574
|
+
createGroupTask(groupId, title, activate = true) {
|
|
575
|
+
const group = this.group(groupId);
|
|
576
|
+
if (!group || group.dm)
|
|
577
|
+
return null;
|
|
578
|
+
const task = {
|
|
579
|
+
threadId: newId(),
|
|
580
|
+
title: title?.trim().slice(0, 80) || UNTITLED_TASK,
|
|
581
|
+
createdAt: Date.now(),
|
|
582
|
+
};
|
|
583
|
+
group.tasks = [task, ...(group.tasks ?? [])];
|
|
584
|
+
if (activate) {
|
|
585
|
+
group.threadId = task.threadId;
|
|
586
|
+
group.pinnedCwd = undefined;
|
|
587
|
+
group.pinnedMessageId = undefined;
|
|
588
|
+
}
|
|
589
|
+
this.saveGroups();
|
|
590
|
+
this.emit({ type: "group", groupId });
|
|
591
|
+
return task;
|
|
592
|
+
}
|
|
593
|
+
switchGroupTask(groupId, threadId) {
|
|
594
|
+
const group = this.group(groupId);
|
|
595
|
+
const task = group?.tasks?.find((candidate) => candidate.threadId === threadId);
|
|
596
|
+
if (!group || group.dm || !task)
|
|
597
|
+
return null;
|
|
598
|
+
group.threadId = task.threadId;
|
|
599
|
+
group.pinnedCwd = task.pinnedCwd;
|
|
600
|
+
group.pinnedMessageId = task.pinnedMessageId;
|
|
601
|
+
this.saveGroups();
|
|
602
|
+
this.emit({ type: "group", groupId });
|
|
603
|
+
return group;
|
|
604
|
+
}
|
|
605
|
+
renameGroupTask(groupId, threadId, title) {
|
|
606
|
+
const task = this.groupTaskByThread(groupId, threadId);
|
|
607
|
+
if (!task)
|
|
608
|
+
return null;
|
|
609
|
+
task.title = title.trim().slice(0, 80) || UNTITLED_TASK;
|
|
610
|
+
this.saveGroups();
|
|
611
|
+
this.emit({ type: "group", groupId });
|
|
612
|
+
return task;
|
|
613
|
+
}
|
|
614
|
+
titleGroupTaskFromFirstMessage(groupId, text, threadId) {
|
|
615
|
+
const task = threadId ? this.groupTaskByThread(groupId, threadId) : this.activeGroupTask(groupId);
|
|
616
|
+
if (!task || task.title !== UNTITLED_TASK)
|
|
617
|
+
return;
|
|
618
|
+
task.title = titleFromMessage(text);
|
|
619
|
+
this.saveGroups();
|
|
620
|
+
this.emit({ type: "group", groupId });
|
|
621
|
+
}
|
|
622
|
+
deleteGroupTask(groupId, threadId) {
|
|
623
|
+
const group = this.group(groupId);
|
|
624
|
+
if (!group || group.dm || !group.tasks || group.tasks.length < 2)
|
|
625
|
+
return null;
|
|
626
|
+
if (!group.tasks.some((task) => task.threadId === threadId))
|
|
627
|
+
return null;
|
|
628
|
+
group.tasks = group.tasks.filter((task) => task.threadId !== threadId);
|
|
629
|
+
this.deleteThreadRecord(threadId);
|
|
630
|
+
if (group.threadId === threadId) {
|
|
631
|
+
const next = group.tasks[0];
|
|
632
|
+
group.threadId = next.threadId;
|
|
633
|
+
group.pinnedCwd = next.pinnedCwd;
|
|
634
|
+
group.pinnedMessageId = next.pinnedMessageId;
|
|
635
|
+
}
|
|
636
|
+
this.saveGroups();
|
|
637
|
+
this.emit({ type: "group", groupId });
|
|
638
|
+
return group;
|
|
639
|
+
}
|
|
640
|
+
/** Toggle an emoji reaction on a message ("user" or a member botId). */
|
|
641
|
+
toggleReaction(threadId, messageId, emoji, by) {
|
|
642
|
+
const existing = this.messagesFor(threadId).find((m) => m.id === messageId);
|
|
643
|
+
if (!existing)
|
|
644
|
+
return null;
|
|
645
|
+
const reactions = existing.reactions ?? [];
|
|
646
|
+
const at = reactions.findIndex((r) => r.emoji === emoji && r.by === by);
|
|
647
|
+
const next = at >= 0 ? reactions.filter((_, i) => i !== at) : [...reactions, { emoji, by }];
|
|
648
|
+
return this.patchMessage(threadId, messageId, { reactions: next.length ? next : undefined });
|
|
649
|
+
}
|
|
650
|
+
thread(threadId) {
|
|
651
|
+
let t = this.threads.get(threadId);
|
|
652
|
+
if (t)
|
|
653
|
+
return t;
|
|
654
|
+
// SQLite is the source of truth; a thread with no rows imports its
|
|
655
|
+
// legacy messages-<threadId>.json once, inside readThread
|
|
656
|
+
const { messages, activeLeafId: storedLeaf } = mdb.readThread(threadId, messagesFile(threadId));
|
|
657
|
+
let activeLeafId = storedLeaf;
|
|
658
|
+
// legacy rows carry no parentId — chain them in array order
|
|
659
|
+
let prev = null;
|
|
660
|
+
for (const m of messages) {
|
|
661
|
+
if (m.parentId === undefined)
|
|
662
|
+
m.parentId = prev;
|
|
663
|
+
prev = m.id;
|
|
664
|
+
}
|
|
665
|
+
if (!activeLeafId)
|
|
666
|
+
activeLeafId = messages.at(-1)?.id ?? null;
|
|
667
|
+
t = { messages, activeLeafId };
|
|
668
|
+
this.threads.set(threadId, t);
|
|
669
|
+
return t;
|
|
670
|
+
}
|
|
671
|
+
messagesFor(threadId) {
|
|
672
|
+
return this.thread(threadId).messages;
|
|
673
|
+
}
|
|
674
|
+
/** Used only with newly allocated import threads. No live actions are
|
|
675
|
+
* replayed: the importer supplies inert text and freshly remapped IDs. */
|
|
676
|
+
importTranscript(threadId, messages, activeLeafId) {
|
|
677
|
+
if (this.messagesFor(threadId).length)
|
|
678
|
+
throw new Error("Cannot import over an existing conversation");
|
|
679
|
+
mdb.importThread(threadId, messages, activeLeafId);
|
|
680
|
+
this.threads.delete(threadId);
|
|
681
|
+
}
|
|
682
|
+
activeLeaf(threadId) {
|
|
683
|
+
return this.thread(threadId).activeLeafId;
|
|
684
|
+
}
|
|
685
|
+
/** The visible conversation: root → activeLeafId. */
|
|
686
|
+
activePath(threadId) {
|
|
687
|
+
const t = this.thread(threadId);
|
|
688
|
+
const byId = new Map(t.messages.map((m) => [m.id, m]));
|
|
689
|
+
const path = [];
|
|
690
|
+
let cur = t.activeLeafId ? byId.get(t.activeLeafId) : undefined;
|
|
691
|
+
while (cur) {
|
|
692
|
+
path.push(cur);
|
|
693
|
+
cur = cur.parentId ? byId.get(cur.parentId) : undefined;
|
|
694
|
+
}
|
|
695
|
+
return path.reverse();
|
|
696
|
+
}
|
|
697
|
+
/** Mark the last assistant text on the active branch as this turn's final
|
|
698
|
+
* visible answer. If a provider ends after commentary without emitting a
|
|
699
|
+
* separate answer, that commentary remains visible as the safe fallback. */
|
|
700
|
+
markTerminalAssistantMessage(threadId, turnId) {
|
|
701
|
+
const path = this.activePath(threadId);
|
|
702
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
703
|
+
const message = path[i];
|
|
704
|
+
if (message.role === "bot" && message.kind === "text" && message.turnId === turnId) {
|
|
705
|
+
if (message.turnTerminal)
|
|
706
|
+
return message;
|
|
707
|
+
return this.patchMessage(threadId, message.id, { turnTerminal: true });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
appendMessage(threadId, message) {
|
|
713
|
+
const t = this.thread(threadId);
|
|
714
|
+
const full = { id: newId(), at: Date.now(), parentId: t.activeLeafId, ...redactBotAuthored(message) };
|
|
715
|
+
t.messages.push(full);
|
|
716
|
+
t.activeLeafId = full.id;
|
|
717
|
+
mdb.appendMessage(threadId, full);
|
|
718
|
+
if (full.kind === "screen") {
|
|
719
|
+
for (const pruned of this.pruneScreenFrames(t)) {
|
|
720
|
+
mdb.updateMessage(threadId, pruned);
|
|
721
|
+
this.emit({ type: "message.patch", threadId, message: pruned });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
this.emit({ type: "message", threadId, message: full });
|
|
725
|
+
// The first-run quiz is not a live ask. Talking past it hides it so the
|
|
726
|
+
// transcript is just the greeting plus what they said. Cards with a
|
|
727
|
+
// requestId are permission/question prompts and stay until answered.
|
|
728
|
+
if (full.role === "user" && full.kind === "text")
|
|
729
|
+
this.dismissOnboardingCard(threadId);
|
|
730
|
+
return full;
|
|
731
|
+
}
|
|
732
|
+
/** Insert a message into the active chain directly after `anchorId` — the
|
|
733
|
+
* home for turn artifacts that finish AFTER the world moved on (the
|
|
734
|
+
* settle-time screen capture races a fast follow-up send, which used to
|
|
735
|
+
* leave the user's message stranded above the screenshot). When the anchor
|
|
736
|
+
* is still the leaf this is a plain append; otherwise the anchor's
|
|
737
|
+
* children are re-parented onto the inserted message, so the transcript
|
|
738
|
+
* reads turn → artifact → follow-up and the leaf stays where it was. */
|
|
739
|
+
insertMessageAfter(threadId, anchorId, message) {
|
|
740
|
+
const t = this.thread(threadId);
|
|
741
|
+
const anchorExists = anchorId !== undefined && t.messages.some((m) => m.id === anchorId);
|
|
742
|
+
if (!anchorExists || t.activeLeafId === anchorId)
|
|
743
|
+
return this.appendMessage(threadId, message);
|
|
744
|
+
const full = { id: newId(), at: Date.now(), ...redactBotAuthored(message), parentId: anchorId };
|
|
745
|
+
const children = t.messages.filter((m) => m.parentId === anchorId);
|
|
746
|
+
t.messages.push(full);
|
|
747
|
+
mdb.appendMessage(threadId, full);
|
|
748
|
+
if (full.kind === "screen") {
|
|
749
|
+
for (const pruned of this.pruneScreenFrames(t)) {
|
|
750
|
+
mdb.updateMessage(threadId, pruned);
|
|
751
|
+
this.emit({ type: "message.patch", threadId, message: pruned });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
this.emit({ type: "message", threadId, message: full });
|
|
755
|
+
// announced after the insert so no client ever sees two siblings
|
|
756
|
+
// claiming the same parent
|
|
757
|
+
for (const child of children)
|
|
758
|
+
this.patchMessage(threadId, child.id, { parentId: full.id });
|
|
759
|
+
return full;
|
|
760
|
+
}
|
|
761
|
+
/** Hide the first-run quiz on this thread, if it is still open. */
|
|
762
|
+
dismissOnboardingCard(threadId) {
|
|
763
|
+
const t = this.thread(threadId);
|
|
764
|
+
const card = t.messages.find((message) => message.kind === "options" && message.card && !message.card.requestId && !message.card.dismissed);
|
|
765
|
+
if (!card?.card)
|
|
766
|
+
return null;
|
|
767
|
+
return this.patchMessage(threadId, card.id, { card: { ...card.card, dismissed: true } });
|
|
768
|
+
}
|
|
769
|
+
/** Screen frames are ~100-500KB of base64 each; keeping every frame of a
|
|
770
|
+
* long computer session bloats the transcript for nothing the client
|
|
771
|
+
* would ever show. The newest few keep their pixels; older ones stay in
|
|
772
|
+
* the transcript as placeholders. Mirrors the client's own frame cap.
|
|
773
|
+
* Returns the messages whose pixels were dropped so the caller can
|
|
774
|
+
* persist exactly those. */
|
|
775
|
+
pruneScreenFrames(t, keep = 4) {
|
|
776
|
+
const pruned = [];
|
|
777
|
+
let seen = 0;
|
|
778
|
+
for (let i = t.messages.length - 1; i >= 0 && seen < t.messages.length; i--) {
|
|
779
|
+
const m = t.messages[i];
|
|
780
|
+
if (m.kind !== "screen" || !m.png)
|
|
781
|
+
continue;
|
|
782
|
+
seen += 1;
|
|
783
|
+
if (seen > keep) {
|
|
784
|
+
m.png = undefined;
|
|
785
|
+
pruned.push(m);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return pruned;
|
|
789
|
+
}
|
|
790
|
+
/** Fork the conversation: a new user message that replaces `sourceId`
|
|
791
|
+
* (same parent, new text) and becomes the active leaf. */
|
|
792
|
+
branchMessage(threadId, sourceId, text) {
|
|
793
|
+
const t = this.thread(threadId);
|
|
794
|
+
const source = t.messages.find((m) => m.id === sourceId);
|
|
795
|
+
if (!source)
|
|
796
|
+
return null;
|
|
797
|
+
const full = {
|
|
798
|
+
id: newId(),
|
|
799
|
+
at: Date.now(),
|
|
800
|
+
role: "user",
|
|
801
|
+
kind: "text",
|
|
802
|
+
text,
|
|
803
|
+
parentId: source.parentId ?? null,
|
|
804
|
+
replyToId: source.replyToId,
|
|
805
|
+
};
|
|
806
|
+
t.messages.push(full);
|
|
807
|
+
t.activeLeafId = full.id;
|
|
808
|
+
mdb.appendMessage(threadId, full);
|
|
809
|
+
this.emit({ type: "message", threadId, message: full });
|
|
810
|
+
return full;
|
|
811
|
+
}
|
|
812
|
+
/** Point the visible conversation at the branch containing `messageId`,
|
|
813
|
+
* descending to that branch's most recently active leaf. */
|
|
814
|
+
setActiveLeaf(threadId, messageId) {
|
|
815
|
+
const t = this.thread(threadId);
|
|
816
|
+
if (!t.messages.some((m) => m.id === messageId))
|
|
817
|
+
return null;
|
|
818
|
+
let cur = messageId;
|
|
819
|
+
for (;;) {
|
|
820
|
+
const children = t.messages.filter((m) => m.parentId === cur);
|
|
821
|
+
if (!children.length)
|
|
822
|
+
break;
|
|
823
|
+
cur = children.reduce((a, b) => (b.at >= a.at ? b : a)).id;
|
|
824
|
+
}
|
|
825
|
+
t.activeLeafId = cur;
|
|
826
|
+
mdb.setActiveLeaf(threadId, cur);
|
|
827
|
+
this.emit({ type: "thread", threadId, activeLeafId: cur });
|
|
828
|
+
return cur;
|
|
829
|
+
}
|
|
830
|
+
patchMessage(threadId, messageId, patch) {
|
|
831
|
+
const t = this.thread(threadId);
|
|
832
|
+
const idx = t.messages.findIndex((m) => m.id === messageId);
|
|
833
|
+
if (idx === -1)
|
|
834
|
+
return null;
|
|
835
|
+
const next = { ...t.messages[idx], ...patch, card: patch.card ?? t.messages[idx].card };
|
|
836
|
+
// SQLite is the durable source of truth. Persist before changing memory so
|
|
837
|
+
// a failed write cannot make this process believe a card was answered
|
|
838
|
+
// while a restart would still show it as pending.
|
|
839
|
+
mdb.updateMessage(threadId, next);
|
|
840
|
+
t.messages[idx] = next;
|
|
841
|
+
this.emit({ type: "message.patch", threadId, message: next });
|
|
842
|
+
return next;
|
|
843
|
+
}
|
|
844
|
+
bot(id) {
|
|
845
|
+
return this.bots.find((b) => b.id === id) ?? null;
|
|
846
|
+
}
|
|
847
|
+
botByThread(threadId) {
|
|
848
|
+
return this.bots.find((b) => b.threadId === threadId || b.tasks?.some((t) => t.threadId === threadId)) ?? null;
|
|
849
|
+
}
|
|
850
|
+
createBot(profile = {}, opts = {}) {
|
|
851
|
+
const name = profile.name?.trim() || pickBotName(this.bots.map((b) => b.name));
|
|
852
|
+
const section = sectionKey(profile.section);
|
|
853
|
+
const bot = {
|
|
854
|
+
id: newId(),
|
|
855
|
+
threadId: newId(),
|
|
856
|
+
name,
|
|
857
|
+
title: profile.title ?? "",
|
|
858
|
+
description: profile.description ?? "",
|
|
859
|
+
notifications: true,
|
|
860
|
+
color: profile.color ?? COLORS[this.bots.length % COLORS.length],
|
|
861
|
+
...(profile.mascotExpression ? { mascotExpression: profile.mascotExpression } : {}),
|
|
862
|
+
...(profile.mascotBody ? { mascotBody: profile.mascotBody } : {}),
|
|
863
|
+
unread: false,
|
|
864
|
+
modelSelection: profile.modelSelection ?? this.defaultSelection(),
|
|
865
|
+
resumeCursors: {},
|
|
866
|
+
createdAt: Date.now(),
|
|
867
|
+
};
|
|
868
|
+
if (section)
|
|
869
|
+
bot.section = section;
|
|
870
|
+
bot.tasks = [{ threadId: bot.threadId, title: UNTITLED_TASK, createdAt: bot.createdAt, resumeCursors: {} }];
|
|
871
|
+
this.bots.unshift(bot);
|
|
872
|
+
this.saveBots();
|
|
873
|
+
// Announce the owner before its onboarding transcript. SSE clients need
|
|
874
|
+
// the bot/thread mapping before they can place either message.
|
|
875
|
+
this.emit({ type: "bot", botId: bot.id });
|
|
876
|
+
if (opts.seedMessages !== false) {
|
|
877
|
+
this.appendMessage(bot.threadId, {
|
|
878
|
+
role: "bot",
|
|
879
|
+
kind: "text",
|
|
880
|
+
text: `Hey — I'm ${name}. Nice to meet you.`,
|
|
881
|
+
});
|
|
882
|
+
this.appendMessage(bot.threadId, { role: "bot", kind: "options", card: onboardingCard() });
|
|
883
|
+
}
|
|
884
|
+
return bot;
|
|
885
|
+
}
|
|
886
|
+
deleteBot(id) {
|
|
887
|
+
const bot = this.bot(id);
|
|
888
|
+
if (!bot)
|
|
889
|
+
return false;
|
|
890
|
+
this.bots = this.bots.filter((b) => b.id !== id);
|
|
891
|
+
// every task's transcript goes with the bot, not just the open one
|
|
892
|
+
for (const threadId of new Set([bot.threadId, ...(bot.tasks ?? []).map((t) => t.threadId)])) {
|
|
893
|
+
this.deleteThreadRecord(threadId);
|
|
894
|
+
}
|
|
895
|
+
// the bot's workspace (files + memory) goes with it — same rule as its
|
|
896
|
+
// transcripts: deleting a bot deletes what it knew
|
|
897
|
+
try {
|
|
898
|
+
rmSync(workspaceDir(id), { recursive: true, force: true });
|
|
899
|
+
}
|
|
900
|
+
catch { }
|
|
901
|
+
// Approval state deliberately lives outside the bot-writable workspace.
|
|
902
|
+
// It still belongs to the bot, so deleting the bot must remove staged
|
|
903
|
+
// proposals, manifests, and native-link ownership records with it.
|
|
904
|
+
try {
|
|
905
|
+
rmSync(join(DATA_DIR, "skill-state", id), { recursive: true, force: true });
|
|
906
|
+
}
|
|
907
|
+
catch { }
|
|
908
|
+
this.saveBots();
|
|
909
|
+
this.emit({ type: "bot.deleted", botId: id });
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
patchBot(id, patch) {
|
|
913
|
+
const bot = this.bot(id);
|
|
914
|
+
if (!bot)
|
|
915
|
+
return null;
|
|
916
|
+
Object.assign(bot, patch);
|
|
917
|
+
this.saveBots();
|
|
918
|
+
this.emit({ type: "bot", botId: id });
|
|
919
|
+
return bot;
|
|
920
|
+
}
|
|
921
|
+
/** File visible bots into one sidebar section as a single durable write.
|
|
922
|
+
*
|
|
923
|
+
* This deliberately stages the complete next file before touching the
|
|
924
|
+
* live records. A missing/hidden target therefore changes nothing, and a
|
|
925
|
+
* failed atomic write cannot leave memory ahead of disk. A Chief collision
|
|
926
|
+
* is refused rather than silently removing somebody's coordinator role. */
|
|
927
|
+
setBotsSection(botIds, section) {
|
|
928
|
+
const ids = [...new Set(botIds)];
|
|
929
|
+
const targets = ids.map((id) => this.bot(id));
|
|
930
|
+
if (targets.some((bot) => !bot || bot.hidden))
|
|
931
|
+
return { ok: false, reason: "unavailable" };
|
|
932
|
+
const targetSection = sectionKey(section);
|
|
933
|
+
const selected = targets;
|
|
934
|
+
const destinationChiefIds = new Set([
|
|
935
|
+
...selected.filter((bot) => bot.chiefOfStaff).map((bot) => bot.id),
|
|
936
|
+
...this.bots
|
|
937
|
+
.filter((bot) => bot.chiefOfStaff && sectionKey(bot.section) === targetSection)
|
|
938
|
+
.map((bot) => bot.id),
|
|
939
|
+
]);
|
|
940
|
+
if (destinationChiefIds.size > 1)
|
|
941
|
+
return { ok: false, reason: "chief-conflict" };
|
|
942
|
+
const patches = new Map();
|
|
943
|
+
for (const bot of selected) {
|
|
944
|
+
patches.set(bot.id, { section: targetSection || undefined });
|
|
945
|
+
}
|
|
946
|
+
const changedIds = new Set();
|
|
947
|
+
const nextBots = this.bots.map((bot) => {
|
|
948
|
+
const patch = patches.get(bot.id);
|
|
949
|
+
if (!patch)
|
|
950
|
+
return bot;
|
|
951
|
+
const next = { ...bot, ...patch };
|
|
952
|
+
if (JSON.stringify(next) !== JSON.stringify(bot))
|
|
953
|
+
changedIds.add(bot.id);
|
|
954
|
+
return next;
|
|
955
|
+
});
|
|
956
|
+
if (changedIds.size) {
|
|
957
|
+
this.saveBots(nextBots);
|
|
958
|
+
for (const bot of this.bots) {
|
|
959
|
+
const patch = patches.get(bot.id);
|
|
960
|
+
if (patch)
|
|
961
|
+
Object.assign(bot, patch);
|
|
962
|
+
}
|
|
963
|
+
for (const botId of changedIds)
|
|
964
|
+
this.emit({ type: "bot", botId });
|
|
965
|
+
}
|
|
966
|
+
return { ok: true, bots: ids.map((id) => this.bot(id)) };
|
|
967
|
+
}
|
|
968
|
+
/** The one way runtime state changes. Sets `activity` and derives `busy`
|
|
969
|
+
* from it, so a reader that only knows busy sees the same truth. */
|
|
970
|
+
setActivity(botId, activity) {
|
|
971
|
+
const bot = this.bot(botId);
|
|
972
|
+
if (!bot)
|
|
973
|
+
return null;
|
|
974
|
+
const busy = ACTIVITY_BUSY.has(activity);
|
|
975
|
+
if (bot.activity === activity && Boolean(bot.busy) === busy)
|
|
976
|
+
return bot;
|
|
977
|
+
bot.activity = activity;
|
|
978
|
+
bot.busy = busy;
|
|
979
|
+
this.saveBots();
|
|
980
|
+
this.emit({ type: "bot", botId });
|
|
981
|
+
return bot;
|
|
982
|
+
}
|
|
983
|
+
/** Elect one Chief of Staff in its section (or clear one section) as one persisted change.
|
|
984
|
+
* The changed records are returned so the server can update every open
|
|
985
|
+
* window, including the bot that just handed the role over. */
|
|
986
|
+
setChiefOfStaff(id, section) {
|
|
987
|
+
const selected = id ? this.bot(id) : null;
|
|
988
|
+
if (id && !selected)
|
|
989
|
+
return null;
|
|
990
|
+
const targetSection = sectionKey(selected?.section ?? section);
|
|
991
|
+
const changed = [];
|
|
992
|
+
for (const bot of this.bots) {
|
|
993
|
+
if (sectionKey(bot.section) !== targetSection)
|
|
994
|
+
continue;
|
|
995
|
+
const next = bot.id === id;
|
|
996
|
+
if (Boolean(bot.chiefOfStaff) === next && !(next && bot.hidden))
|
|
997
|
+
continue;
|
|
998
|
+
if (next) {
|
|
999
|
+
bot.chiefOfStaff = true;
|
|
1000
|
+
// A section's main contact must stay reachable in the sidebar.
|
|
1001
|
+
bot.hidden = false;
|
|
1002
|
+
}
|
|
1003
|
+
else {
|
|
1004
|
+
bot.chiefOfStaff = false;
|
|
1005
|
+
}
|
|
1006
|
+
changed.push(bot);
|
|
1007
|
+
}
|
|
1008
|
+
if (changed.length)
|
|
1009
|
+
this.saveBots();
|
|
1010
|
+
for (const bot of changed)
|
|
1011
|
+
this.emit({ type: "bot", botId: bot.id });
|
|
1012
|
+
return changed;
|
|
1013
|
+
}
|
|
1014
|
+
setResumeCursor(botId, instanceId, cursor, threadId) {
|
|
1015
|
+
const bot = this.bot(botId);
|
|
1016
|
+
if (!bot)
|
|
1017
|
+
return;
|
|
1018
|
+
// the cursor belongs to the task that produced it, not to the bot
|
|
1019
|
+
const task = threadId ? this.taskByThread(botId, threadId) : this.activeTask(botId);
|
|
1020
|
+
if (task)
|
|
1021
|
+
task.resumeCursors[instanceId] = cursor;
|
|
1022
|
+
// The legacy mirror follows the task visible in chat, never a detached
|
|
1023
|
+
// routine task working in the background.
|
|
1024
|
+
if (!threadId || bot.threadId === threadId)
|
|
1025
|
+
bot.resumeCursors[instanceId] = cursor;
|
|
1026
|
+
this.saveBots();
|
|
1027
|
+
this.emit({ type: "bot", botId });
|
|
1028
|
+
}
|
|
1029
|
+
/** Record which instance just took a turn on this task. Called at
|
|
1030
|
+
* dispatch, not at cursor time — transcript-replay engines never
|
|
1031
|
+
* produce a cursor, and they still count as having run last. */
|
|
1032
|
+
markTaskDispatched(botId, threadId, instanceId) {
|
|
1033
|
+
const task = this.taskByThread(botId, threadId);
|
|
1034
|
+
if (!task || task.lastInstanceId === instanceId)
|
|
1035
|
+
return;
|
|
1036
|
+
task.lastInstanceId = instanceId;
|
|
1037
|
+
this.saveBots();
|
|
1038
|
+
}
|
|
1039
|
+
/** Bank one settled turn onto its task. Called once per turn.completed;
|
|
1040
|
+
* the running per-driver token indicator is deliberately not used here
|
|
1041
|
+
* because its meaning differs by driver. */
|
|
1042
|
+
addTaskUsage(botId, threadId, turn) {
|
|
1043
|
+
const task = this.taskByThread(botId, threadId);
|
|
1044
|
+
if (!task)
|
|
1045
|
+
return null;
|
|
1046
|
+
const prev = { input: 0, output: 0, costUsd: null, turns: 0, ...task.usage };
|
|
1047
|
+
const cost = typeof turn.costUsd === "number" && Number.isFinite(turn.costUsd) ? turn.costUsd : null;
|
|
1048
|
+
const prevCost = typeof prev.costUsd === "number" ? prev.costUsd : null;
|
|
1049
|
+
// providers occasionally report NaN or a negative on a partial turn —
|
|
1050
|
+
// never let that poison a running tally
|
|
1051
|
+
const clean = (n) => (typeof n === "number" && Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 0);
|
|
1052
|
+
// the cached share exists on a record only once a driver has reported
|
|
1053
|
+
// it — a driver that never does leaves the record shaped as before
|
|
1054
|
+
const cachedKnown = typeof prev.cachedInput === "number" || typeof turn.cachedInput === "number";
|
|
1055
|
+
const prevInput = clean(prev.input);
|
|
1056
|
+
const turnInput = clean(turn.input);
|
|
1057
|
+
const nextCachedInput = Math.min(clean(prev.cachedInput), prevInput)
|
|
1058
|
+
+ Math.min(clean(turn.cachedInput), turnInput);
|
|
1059
|
+
task.usage = {
|
|
1060
|
+
input: prevInput + turnInput,
|
|
1061
|
+
output: prev.output + clean(turn.output),
|
|
1062
|
+
...(cachedKnown ? { cachedInput: nextCachedInput } : {}),
|
|
1063
|
+
costUsd: cost === null ? prevCost : (prevCost ?? 0) + cost,
|
|
1064
|
+
turns: prev.turns + 1,
|
|
1065
|
+
};
|
|
1066
|
+
this.saveBots();
|
|
1067
|
+
this.emit({ type: "bot", botId });
|
|
1068
|
+
return task.usage;
|
|
1069
|
+
}
|
|
1070
|
+
/** The folder a task's turn runs in. Pins on first call from the bot's
|
|
1071
|
+
* current folder — unless the task already has a session (a thread from
|
|
1072
|
+
* before folders existed), which pins to the default so the folder can't
|
|
1073
|
+
* move under it. Returns the pinned value: a path, or null for default. */
|
|
1074
|
+
pinTaskCwd(botId, threadId, fallbackCwd, opts = {}) {
|
|
1075
|
+
const bot = this.bot(botId);
|
|
1076
|
+
const task = bot ? this.taskByThread(botId, threadId) : undefined;
|
|
1077
|
+
if (!bot || !task)
|
|
1078
|
+
return null;
|
|
1079
|
+
if (opts.none) {
|
|
1080
|
+
if (task.cwd !== null) {
|
|
1081
|
+
task.cwd = null;
|
|
1082
|
+
this.saveBots();
|
|
1083
|
+
this.emit({ type: "bot", botId });
|
|
1084
|
+
}
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
if (task.cwd === undefined) {
|
|
1088
|
+
task.cwd = Object.keys(task.resumeCursors).length === 0 ? (bot.cwd ?? fallbackCwd ?? null) : null;
|
|
1089
|
+
this.saveBots();
|
|
1090
|
+
this.emit({ type: "bot", botId });
|
|
1091
|
+
}
|
|
1092
|
+
return task.cwd;
|
|
1093
|
+
}
|
|
1094
|
+
/** The folder a room's member turns run in. Pins on the first turn that
|
|
1095
|
+
* dispatches, from the room's `cwd` at that moment. Pinned, not read
|
|
1096
|
+
* live, for the same reason tasks pin (see pinTaskCwd): engines key
|
|
1097
|
+
* their sessions and files to the folder a thread starts in, and a room
|
|
1098
|
+
* lives on ONE thread forever — so changing the room's folder applies to
|
|
1099
|
+
* future rooms, never under a room that already started working
|
|
1100
|
+
* somewhere. Returns the pinned value: a path, or null = each member's
|
|
1101
|
+
* own default. */
|
|
1102
|
+
pinGroupCwd(groupId, threadId) {
|
|
1103
|
+
const group = this.group(groupId);
|
|
1104
|
+
if (!group)
|
|
1105
|
+
return null;
|
|
1106
|
+
const task = threadId ? this.groupTaskByThread(groupId, threadId) : this.activeGroupTask(groupId);
|
|
1107
|
+
// Direct-message channels retain the original single-thread contract.
|
|
1108
|
+
if (!task) {
|
|
1109
|
+
if (!group.dm)
|
|
1110
|
+
return null;
|
|
1111
|
+
if (group.pinnedCwd === undefined) {
|
|
1112
|
+
group.pinnedCwd = group.cwd ?? null;
|
|
1113
|
+
this.saveGroups();
|
|
1114
|
+
this.emit({ type: "group", groupId: group.id });
|
|
1115
|
+
}
|
|
1116
|
+
return group.pinnedCwd;
|
|
1117
|
+
}
|
|
1118
|
+
if (task.pinnedCwd === undefined) {
|
|
1119
|
+
task.pinnedCwd = group.cwd ?? null;
|
|
1120
|
+
if (group.threadId === task.threadId)
|
|
1121
|
+
group.pinnedCwd = task.pinnedCwd;
|
|
1122
|
+
this.saveGroups();
|
|
1123
|
+
this.emit({ type: "group", groupId: group.id });
|
|
1124
|
+
}
|
|
1125
|
+
return task.pinnedCwd;
|
|
1126
|
+
}
|
|
1127
|
+
// ── tasks ─────────────────────────────────────────────────────────────
|
|
1128
|
+
/** The first thing the human asked in a thread — a task's natural name. */
|
|
1129
|
+
firstUserLine(threadId) {
|
|
1130
|
+
const first = this.messagesFor(threadId).find((m) => m.role === "user" && m.kind === "text" && m.text?.trim());
|
|
1131
|
+
return first?.text ? titleFromMessage(first.text) : null;
|
|
1132
|
+
}
|
|
1133
|
+
tasks(botId) {
|
|
1134
|
+
return this.bot(botId)?.tasks ?? [];
|
|
1135
|
+
}
|
|
1136
|
+
activeTask(botId) {
|
|
1137
|
+
const bot = this.bot(botId);
|
|
1138
|
+
return bot?.tasks?.find((t) => t.threadId === bot.threadId);
|
|
1139
|
+
}
|
|
1140
|
+
taskByThread(botId, threadId) {
|
|
1141
|
+
return this.bot(botId)?.tasks?.find((t) => t.threadId === threadId);
|
|
1142
|
+
}
|
|
1143
|
+
/** A fresh context on the same bot: new thread, new session, same
|
|
1144
|
+
* persona/tools/computer. Becomes the active task. */
|
|
1145
|
+
createTask(botId, title, activate = true) {
|
|
1146
|
+
const bot = this.bot(botId);
|
|
1147
|
+
if (!bot)
|
|
1148
|
+
return null;
|
|
1149
|
+
const task = {
|
|
1150
|
+
threadId: newId(),
|
|
1151
|
+
title: title?.trim() || UNTITLED_TASK,
|
|
1152
|
+
createdAt: Date.now(),
|
|
1153
|
+
resumeCursors: {},
|
|
1154
|
+
};
|
|
1155
|
+
bot.tasks = [task, ...(bot.tasks ?? [])];
|
|
1156
|
+
if (activate) {
|
|
1157
|
+
bot.threadId = task.threadId;
|
|
1158
|
+
bot.resumeCursors = {}; // legacy mirror follows the active task
|
|
1159
|
+
}
|
|
1160
|
+
this.saveBots();
|
|
1161
|
+
this.emit({ type: "bot", botId });
|
|
1162
|
+
return task;
|
|
1163
|
+
}
|
|
1164
|
+
switchTask(botId, threadId) {
|
|
1165
|
+
const bot = this.bot(botId);
|
|
1166
|
+
const task = bot?.tasks?.find((t) => t.threadId === threadId);
|
|
1167
|
+
if (!bot || !task)
|
|
1168
|
+
return null;
|
|
1169
|
+
bot.threadId = task.threadId;
|
|
1170
|
+
bot.resumeCursors = { ...task.resumeCursors };
|
|
1171
|
+
this.saveBots();
|
|
1172
|
+
this.emit({ type: "bot", botId });
|
|
1173
|
+
return bot;
|
|
1174
|
+
}
|
|
1175
|
+
renameTask(botId, threadId, title) {
|
|
1176
|
+
const task = this.bot(botId)?.tasks?.find((t) => t.threadId === threadId);
|
|
1177
|
+
if (!task)
|
|
1178
|
+
return null;
|
|
1179
|
+
task.title = title.trim().slice(0, 80) || UNTITLED_TASK;
|
|
1180
|
+
this.saveBots();
|
|
1181
|
+
this.emit({ type: "bot", botId });
|
|
1182
|
+
return task;
|
|
1183
|
+
}
|
|
1184
|
+
/** Name a task after its first message, once. */
|
|
1185
|
+
titleTaskFromFirstMessage(botId, text, threadId) {
|
|
1186
|
+
const task = threadId ? this.taskByThread(botId, threadId) : this.activeTask(botId);
|
|
1187
|
+
if (!task || task.title !== UNTITLED_TASK)
|
|
1188
|
+
return;
|
|
1189
|
+
task.title = titleFromMessage(text);
|
|
1190
|
+
this.saveBots();
|
|
1191
|
+
this.emit({ type: "bot", botId });
|
|
1192
|
+
}
|
|
1193
|
+
/** Delete a task and its transcript. A bot always keeps one. */
|
|
1194
|
+
deleteTask(botId, threadId) {
|
|
1195
|
+
const bot = this.bot(botId);
|
|
1196
|
+
if (!bot || !bot.tasks || bot.tasks.length < 2)
|
|
1197
|
+
return null;
|
|
1198
|
+
if (!bot.tasks.some((t) => t.threadId === threadId))
|
|
1199
|
+
return null;
|
|
1200
|
+
bot.tasks = bot.tasks.filter((t) => t.threadId !== threadId);
|
|
1201
|
+
this.deleteThreadRecord(threadId);
|
|
1202
|
+
if (bot.threadId === threadId) {
|
|
1203
|
+
bot.threadId = bot.tasks[0].threadId;
|
|
1204
|
+
bot.resumeCursors = { ...bot.tasks[0].resumeCursors };
|
|
1205
|
+
}
|
|
1206
|
+
this.saveBots();
|
|
1207
|
+
this.emit({ type: "bot", botId });
|
|
1208
|
+
return bot;
|
|
1209
|
+
}
|
|
1210
|
+
/** First-run seed: one bot so the app never opens empty — it gets a
|
|
1211
|
+
* random friendly name like every other bot. */
|
|
1212
|
+
seedIfEmpty() {
|
|
1213
|
+
if (this.bots.length)
|
|
1214
|
+
return;
|
|
1215
|
+
this.createBot();
|
|
1216
|
+
}
|
|
1217
|
+
}
|