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,676 @@
|
|
|
1
|
+
export function isAttachment(value) {
|
|
2
|
+
if (!value || typeof value !== "object")
|
|
3
|
+
return false;
|
|
4
|
+
const attachment = value;
|
|
5
|
+
if (typeof attachment.id !== "string" || !validSize(attachment.size))
|
|
6
|
+
return false;
|
|
7
|
+
if (attachment.kind === "paste") {
|
|
8
|
+
return (typeof attachment.text === "string" &&
|
|
9
|
+
typeof attachment.lines === "number" &&
|
|
10
|
+
Number.isInteger(attachment.lines) &&
|
|
11
|
+
attachment.lines >= 1);
|
|
12
|
+
}
|
|
13
|
+
if (attachment.kind === "file") {
|
|
14
|
+
return (typeof attachment.path === "string" &&
|
|
15
|
+
attachment.path.length > 0 &&
|
|
16
|
+
typeof attachment.name === "string");
|
|
17
|
+
}
|
|
18
|
+
if (attachment.kind === "image") {
|
|
19
|
+
return (typeof attachment.path === "string" &&
|
|
20
|
+
attachment.path.length > 0 &&
|
|
21
|
+
typeof attachment.name === "string" &&
|
|
22
|
+
typeof attachment.mime === "string" &&
|
|
23
|
+
attachment.mime.startsWith("image/"));
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function validSize(value) {
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
29
|
+
}
|
|
30
|
+
/** Past this, a paste stops reading as typing and becomes an attachment.
|
|
31
|
+
* Long-but-narrow (a stack trace, a log) counts by line, not just chars. */
|
|
32
|
+
export const PASTE_CHARS = 900;
|
|
33
|
+
export const PASTE_LINES = 12;
|
|
34
|
+
export function isLongPaste(text) {
|
|
35
|
+
return text.length >= PASTE_CHARS || countLines(text) >= PASTE_LINES;
|
|
36
|
+
}
|
|
37
|
+
export function countLines(text) {
|
|
38
|
+
return text.split("\n").length;
|
|
39
|
+
}
|
|
40
|
+
function newId() {
|
|
41
|
+
return globalThis.crypto?.randomUUID?.() ?? `a${Math.random().toString(36).slice(2)}`;
|
|
42
|
+
}
|
|
43
|
+
/** The upload id is a server-validated UUID and remains stable across the
|
|
44
|
+
* one recovery attempt. That makes a lost success response safe to retry:
|
|
45
|
+
* the server returns the already committed attachment instead of creating a
|
|
46
|
+
* second file. */
|
|
47
|
+
function newUploadId() {
|
|
48
|
+
if (globalThis.crypto?.randomUUID)
|
|
49
|
+
return globalThis.crypto.randomUUID();
|
|
50
|
+
const bytes = new Uint8Array(16);
|
|
51
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
52
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
56
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
60
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
61
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
|
|
62
|
+
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
|
|
63
|
+
}
|
|
64
|
+
export function fileAttachment(name, path, size) {
|
|
65
|
+
return { kind: "file", id: newId(), path, name, size };
|
|
66
|
+
}
|
|
67
|
+
/** Matches the server's IMAGE_MAX_BYTES — checked client-side so an
|
|
68
|
+
* oversized paste is refused before the upload starts, not mid-stream. */
|
|
69
|
+
export const IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
|
70
|
+
export const FILE_MAX_BYTES = 25 * 1024 * 1024;
|
|
71
|
+
const DOCUMENT_MIMES = {
|
|
72
|
+
txt: "text/plain",
|
|
73
|
+
md: "text/markdown",
|
|
74
|
+
markdown: "text/markdown",
|
|
75
|
+
csv: "text/csv",
|
|
76
|
+
tsv: "text/tab-separated-values",
|
|
77
|
+
json: "application/json",
|
|
78
|
+
pdf: "application/pdf",
|
|
79
|
+
rtf: "application/rtf",
|
|
80
|
+
doc: "application/msword",
|
|
81
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
82
|
+
xls: "application/vnd.ms-excel",
|
|
83
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
84
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
85
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
86
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
87
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
88
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
89
|
+
};
|
|
90
|
+
const ACCEPTED_DOCUMENT_MIMES = new Set(Object.values(DOCUMENT_MIMES));
|
|
91
|
+
export function documentMime(file) {
|
|
92
|
+
const declared = file.type.split(";", 1)[0].trim().toLowerCase();
|
|
93
|
+
if (ACCEPTED_DOCUMENT_MIMES.has(declared))
|
|
94
|
+
return declared;
|
|
95
|
+
const extension = file.name.split(".").at(-1)?.toLowerCase() ?? "";
|
|
96
|
+
return DOCUMENT_MIMES[extension] ?? null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Checks if a given file or descriptor has a supported image MIME type.
|
|
100
|
+
*
|
|
101
|
+
* @param file - Object with a MIME `type` and optional `size`.
|
|
102
|
+
* @returns `true` if the file is a supported image format (PNG, JPEG, GIF, WebP).
|
|
103
|
+
*/
|
|
104
|
+
export function isImageFile(file) {
|
|
105
|
+
return (file.type.startsWith("image/") &&
|
|
106
|
+
["image/png", "image/jpeg", "image/gif", "image/webp"].includes(file.type.split(";")[0].trim().toLowerCase()));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Extracts valid image files from clipboard data, checking items first because
|
|
110
|
+
* Chromium on macOS exposes screenshots and copied image bitmaps via items
|
|
111
|
+
* while clipboardData.files may remain empty.
|
|
112
|
+
*
|
|
113
|
+
* @param clipboardData - The clipboard DataTransfer object or mock data.
|
|
114
|
+
* @returns Array of valid image File objects found in the clipboard.
|
|
115
|
+
*/
|
|
116
|
+
export function clipboardImageFiles(clipboardData) {
|
|
117
|
+
if (!clipboardData)
|
|
118
|
+
return [];
|
|
119
|
+
if (clipboardData.items) {
|
|
120
|
+
const fromItems = [];
|
|
121
|
+
for (const item of clipboardData.items) {
|
|
122
|
+
if (item.kind === "file" && item.type.startsWith("image/")) {
|
|
123
|
+
const file = item.getAsFile();
|
|
124
|
+
if (file && isImageFile(file)) {
|
|
125
|
+
fromItems.push(file);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (fromItems.length > 0)
|
|
130
|
+
return fromItems;
|
|
131
|
+
}
|
|
132
|
+
if (clipboardData.files) {
|
|
133
|
+
return Array.from(clipboardData.files).filter(isImageFile);
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Detects if the clipboard data contains any image items or files.
|
|
139
|
+
*
|
|
140
|
+
* @param clipboardData - The clipboard DataTransfer object or mock data.
|
|
141
|
+
* @returns `true` if any image file or item is present in the clipboard.
|
|
142
|
+
*/
|
|
143
|
+
export function clipboardHasImages(clipboardData) {
|
|
144
|
+
if (!clipboardData)
|
|
145
|
+
return false;
|
|
146
|
+
if (clipboardData.items) {
|
|
147
|
+
for (const item of clipboardData.items) {
|
|
148
|
+
if (item.kind === "file" && item.type.startsWith("image/")) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (clipboardData.files) {
|
|
154
|
+
for (const file of clipboardData.files) {
|
|
155
|
+
if (isImageFile(file))
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const IMAGE_EXTENSION_BY_MIME = {
|
|
162
|
+
"image/png": ".png",
|
|
163
|
+
"image/jpeg": ".jpg",
|
|
164
|
+
"image/gif": ".gif",
|
|
165
|
+
"image/webp": ".webp",
|
|
166
|
+
};
|
|
167
|
+
const UPLOAD_ATTEMPTS = 2;
|
|
168
|
+
function retryableUploadStatus(status) {
|
|
169
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
170
|
+
}
|
|
171
|
+
/** Retry only failures where the server may already have committed the body.
|
|
172
|
+
* A validation/authentication response is final, while a network failure or
|
|
173
|
+
* transient response gets one replay with the same upload id. */
|
|
174
|
+
async function uploadWithRetry(url, init) {
|
|
175
|
+
for (let attempt = 0; attempt < UPLOAD_ATTEMPTS; attempt += 1) {
|
|
176
|
+
let response;
|
|
177
|
+
try {
|
|
178
|
+
response = await fetch(url, init);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const aborted = typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
182
|
+
if (aborted || attempt === UPLOAD_ATTEMPTS - 1)
|
|
183
|
+
throw error;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
if (retryableUploadStatus(response.status) && attempt < UPLOAD_ATTEMPTS - 1) {
|
|
188
|
+
await response.body?.cancel().catch(() => undefined);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const detail = (await response.json().catch(() => ({ error: response.statusText })));
|
|
192
|
+
throw Object.assign(new Error(detail.error ?? "upload failed"), { status: response.status });
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
return await response.json();
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (attempt === UPLOAD_ATTEMPTS - 1) {
|
|
199
|
+
throw new Error("upload returned an invalid response", { cause: error });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
throw new Error("upload failed");
|
|
204
|
+
}
|
|
205
|
+
function canonicalImageName(originalName, path, mime) {
|
|
206
|
+
const normalizedMime = mime.split(";", 1)[0].trim().toLowerCase();
|
|
207
|
+
const extension = IMAGE_EXTENSION_BY_MIME[normalizedMime];
|
|
208
|
+
const pathName = attachmentBasename(path);
|
|
209
|
+
if (!extension || !pathName.toLowerCase().endsWith(extension)) {
|
|
210
|
+
throw new Error("upload returned invalid image metadata");
|
|
211
|
+
}
|
|
212
|
+
const original = attachmentBasename(originalName.trim());
|
|
213
|
+
const dot = original.lastIndexOf(".");
|
|
214
|
+
const stem = (dot > 0 ? original.slice(0, dot) : original).trim() || "pasted image";
|
|
215
|
+
return `${stem}${extension}`;
|
|
216
|
+
}
|
|
217
|
+
/** Persist a pasted image server-side and return the attachment chip data.
|
|
218
|
+
* The server writes ~/.openmausbot/attachments/<uuid>.<ext> and answers
|
|
219
|
+
* with the path; the prompt references that path so every CLI can open it. */
|
|
220
|
+
export function optimisticImageAttachment(file) {
|
|
221
|
+
if (!isImageFile(file))
|
|
222
|
+
return null;
|
|
223
|
+
if (file.size > IMAGE_MAX_BYTES)
|
|
224
|
+
throw Object.assign(new Error(`${file.name} exceeds 10 MB`), { status: 413 });
|
|
225
|
+
const mime = file.type.split(";", 1)[0].trim().toLowerCase();
|
|
226
|
+
const previewUrl = typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : undefined;
|
|
227
|
+
return {
|
|
228
|
+
kind: "image",
|
|
229
|
+
id: newId(),
|
|
230
|
+
path: "",
|
|
231
|
+
name: file.name || "image",
|
|
232
|
+
size: file.size,
|
|
233
|
+
mime,
|
|
234
|
+
...(previewUrl ? { previewUrl } : {}),
|
|
235
|
+
uploading: true,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export async function imageAttachmentFromFile(file, optimistic) {
|
|
239
|
+
if (!isImageFile(file))
|
|
240
|
+
return null;
|
|
241
|
+
if (file.size > IMAGE_MAX_BYTES)
|
|
242
|
+
throw Object.assign(new Error(`${file.name} exceeds 10 MB`), { status: 413 });
|
|
243
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
244
|
+
const uploadId = newUploadId();
|
|
245
|
+
const saved = await uploadWithRetry(`/api/attachments?uploadId=${encodeURIComponent(uploadId)}`, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers: { "content-type": file.type },
|
|
248
|
+
body: bytes,
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
kind: "image",
|
|
252
|
+
id: optimistic?.id ?? newId(),
|
|
253
|
+
path: saved.path,
|
|
254
|
+
name: canonicalImageName(file.name, saved.path, saved.mime),
|
|
255
|
+
size: saved.bytes,
|
|
256
|
+
mime: saved.mime,
|
|
257
|
+
...(optimistic?.previewUrl ? { previewUrl: optimistic.previewUrl } : {}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/** Copy a supported document into the private attachment store. The prompt
|
|
261
|
+
* then carries the same durable path for the local agent and paired phones,
|
|
262
|
+
* instead of exposing an arbitrary Finder path to the companion route. */
|
|
263
|
+
export async function fileAttachmentFromFile(file) {
|
|
264
|
+
const mime = documentMime(file);
|
|
265
|
+
if (!mime)
|
|
266
|
+
return null;
|
|
267
|
+
if (file.size > FILE_MAX_BYTES) {
|
|
268
|
+
throw Object.assign(new Error(`${file.name} exceeds 25 MB`), { status: 413 });
|
|
269
|
+
}
|
|
270
|
+
const uploadId = newUploadId();
|
|
271
|
+
const saved = await uploadWithRetry(`/api/files?name=${encodeURIComponent(file.name)}&uploadId=${encodeURIComponent(uploadId)}`, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: { "content-type": mime },
|
|
274
|
+
body: file,
|
|
275
|
+
});
|
|
276
|
+
return fileAttachment(saved.name || file.name, saved.path, saved.bytes);
|
|
277
|
+
}
|
|
278
|
+
export function pasteAttachment(text) {
|
|
279
|
+
const id = newId();
|
|
280
|
+
// measured once, here: a chip re-renders on every keystroke in the
|
|
281
|
+
// composer, and encoding half a megabyte each time would be felt
|
|
282
|
+
return { kind: "paste", id, text, size: byteLength(text), lines: countLines(text) };
|
|
283
|
+
}
|
|
284
|
+
/** Move a pasted attachment into the editable composer draft without
|
|
285
|
+
* running the text back through the paste threshold. */
|
|
286
|
+
export function appendPastedText(text, pasted) {
|
|
287
|
+
if (!text)
|
|
288
|
+
return pasted;
|
|
289
|
+
return `${text}${text.endsWith("\n") ? "" : "\n\n"}${pasted}`;
|
|
290
|
+
}
|
|
291
|
+
export const INLINE_DROP_LIMIT = 512 * 1024;
|
|
292
|
+
/** Turn a browser drop into composer attachments. Electron-backed files
|
|
293
|
+
* keep their disk path; small pathless text drops keep their contents.
|
|
294
|
+
* Promise.all preserves the user's drop order even when text reads finish
|
|
295
|
+
* in a different order. */
|
|
296
|
+
export async function attachmentsFromDroppedFiles(files, getPath) {
|
|
297
|
+
const results = await Promise.all(files.map(async (file) => {
|
|
298
|
+
let path = "";
|
|
299
|
+
try {
|
|
300
|
+
path = getPath(file);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// A browser or older desktop shell has no disk path to expose.
|
|
304
|
+
}
|
|
305
|
+
if (path)
|
|
306
|
+
return { attachment: fileAttachment(file.name, path, file.size) };
|
|
307
|
+
if (isInlineText(file) && file.size <= INLINE_DROP_LIMIT) {
|
|
308
|
+
try {
|
|
309
|
+
return { attachment: pasteAttachment(await file.text()) };
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Treat an unreadable browser drag like any other pathless file.
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { rejectedName: file.name };
|
|
316
|
+
}));
|
|
317
|
+
return {
|
|
318
|
+
attachments: results.flatMap((result) => "attachment" in result && result.attachment ? [result.attachment] : []),
|
|
319
|
+
rejectedNames: results.flatMap((result) => "rejectedName" in result && result.rejectedName ? [result.rejectedName] : []),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function isInlineText(file) {
|
|
323
|
+
return file.type.startsWith("text/") || file.type === "application/json";
|
|
324
|
+
}
|
|
325
|
+
/** What the paste actually weighs — String#length counts UTF-16 units, so
|
|
326
|
+
* it reads a third under on accented text and half under on CJK. */
|
|
327
|
+
export function byteLength(text) {
|
|
328
|
+
return new TextEncoder().encode(text).length;
|
|
329
|
+
}
|
|
330
|
+
/** "12 lines, 3.4 KB" — what the chip says under the preview. */
|
|
331
|
+
export function pasteSummary(a) {
|
|
332
|
+
return `${a.lines} lines, ${formatSize(a.size)}`;
|
|
333
|
+
}
|
|
334
|
+
export function formatSize(bytes) {
|
|
335
|
+
if (bytes < 1024)
|
|
336
|
+
return `${bytes} B`;
|
|
337
|
+
if (bytes < 1024 * 1024)
|
|
338
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
339
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
340
|
+
}
|
|
341
|
+
/** The prompt the bot receives: what was typed, then one block per
|
|
342
|
+
* attachment. Tagged blocks rather than fences — pasted code and markdown
|
|
343
|
+
* carry fences of their own, and nesting them loses the boundary. A file
|
|
344
|
+
* carries its path for the agent and its original name for the transcript. */
|
|
345
|
+
export function composeMessage(text, attachments) {
|
|
346
|
+
const parts = [text.trim()];
|
|
347
|
+
attachments.forEach((a, i) => {
|
|
348
|
+
if (a.kind === "paste") {
|
|
349
|
+
parts.push(`<pasted-text index="${i + 1}">\n${a.text}\n</pasted-text>`);
|
|
350
|
+
}
|
|
351
|
+
else if (a.kind === "image") {
|
|
352
|
+
parts.push(`<attached-image path="${escapeAttribute(a.path)}" name="${escapeAttribute(a.name)}" />`);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
parts.push(`<attached-file path="${escapeAttribute(a.path)}" name="${escapeAttribute(a.name)}" />`);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
return parts.filter(Boolean).join("\n\n");
|
|
359
|
+
}
|
|
360
|
+
/** File paths are untrusted prompt content. Keep them inside the quoted
|
|
361
|
+
* attribute even when a filename contains XML characters or line breaks. */
|
|
362
|
+
export function escapeAttribute(value) {
|
|
363
|
+
return value
|
|
364
|
+
.replaceAll("&", "&")
|
|
365
|
+
.replaceAll('"', """)
|
|
366
|
+
.replaceAll("<", "<")
|
|
367
|
+
.replaceAll(">", ">")
|
|
368
|
+
.replaceAll("\t", "	")
|
|
369
|
+
.replaceAll("\r", " ")
|
|
370
|
+
.replaceAll("\n", " ");
|
|
371
|
+
}
|
|
372
|
+
/** Decode only entities emitted by escapeAttribute. A second encoded pass
|
|
373
|
+
* stays encoded, rather than turning attacker-controlled text into markup. */
|
|
374
|
+
function decodeAttachmentAttribute(value) {
|
|
375
|
+
return value.replace(/&(quot|lt|gt|amp);|&#(9|10|13);/g, (entity, named, numeric) => {
|
|
376
|
+
if (numeric === "9")
|
|
377
|
+
return "\t";
|
|
378
|
+
if (numeric === "10")
|
|
379
|
+
return "\n";
|
|
380
|
+
if (numeric === "13")
|
|
381
|
+
return "\r";
|
|
382
|
+
if (named === "quot")
|
|
383
|
+
return '"';
|
|
384
|
+
if (named === "lt")
|
|
385
|
+
return "<";
|
|
386
|
+
if (named === "gt")
|
|
387
|
+
return ">";
|
|
388
|
+
if (named === "amp")
|
|
389
|
+
return "&";
|
|
390
|
+
return entity;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/** Keep transcript-provided names compact and visually honest. File chips
|
|
394
|
+
* are deliberately not links, but control and bidi characters can still
|
|
395
|
+
* make an untrusted name misleading. */
|
|
396
|
+
function transcriptFileName(path, suppliedName) {
|
|
397
|
+
const decoded = suppliedName ? decodeAttachmentAttribute(suppliedName) : attachmentBasename(path);
|
|
398
|
+
const clean = (value) => Array.from(value, (character) => {
|
|
399
|
+
const code = character.codePointAt(0) ?? 0;
|
|
400
|
+
const control = code <= 31 || (code >= 127 && code <= 159);
|
|
401
|
+
const bidiControl = (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069);
|
|
402
|
+
return control || bidiControl ? " " : character;
|
|
403
|
+
}).join("").replace(/\s+/g, " ").trim();
|
|
404
|
+
const safe = clean(attachmentBasename(decoded));
|
|
405
|
+
const fallback = clean(attachmentBasename(path));
|
|
406
|
+
return Array.from(safe || fallback || "Attached file").slice(0, 180).join("");
|
|
407
|
+
}
|
|
408
|
+
export function isPrivateAttachmentPath(path) {
|
|
409
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.[a-z0-9]+$/i
|
|
410
|
+
.test(attachmentBasename(path));
|
|
411
|
+
}
|
|
412
|
+
/** Recognise CommonMark-style fenced code without pulling a Markdown parser
|
|
413
|
+
* into the composer bundle. An unterminated fence deliberately protects the
|
|
414
|
+
* rest of the message: examples must never turn into actionable attachments. */
|
|
415
|
+
function transcriptFenceMarker(line) {
|
|
416
|
+
let index = 0;
|
|
417
|
+
while (index < line.length && index < 4 && line[index] === " ")
|
|
418
|
+
index += 1;
|
|
419
|
+
if (index > 3)
|
|
420
|
+
return null;
|
|
421
|
+
const marker = line[index];
|
|
422
|
+
if (marker !== "`" && marker !== "~")
|
|
423
|
+
return null;
|
|
424
|
+
const start = index;
|
|
425
|
+
while (index < line.length && line[index] === marker)
|
|
426
|
+
index += 1;
|
|
427
|
+
const length = index - start;
|
|
428
|
+
if (length < 3)
|
|
429
|
+
return null;
|
|
430
|
+
return { marker, length, remainder: line.slice(index) };
|
|
431
|
+
}
|
|
432
|
+
const COMMONMARK_BLOCK_TAGS = [
|
|
433
|
+
"address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", "col",
|
|
434
|
+
"colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure",
|
|
435
|
+
"footer", "form", "frame", "frameset", "h[1-6]", "head", "header", "hr", "html", "iframe", "legend",
|
|
436
|
+
"li", "link", "main", "menu", "menuitem", "nav", "noframes", "ol", "optgroup", "option", "p",
|
|
437
|
+
"param", "search", "section", "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title",
|
|
438
|
+
"tr", "track", "ul",
|
|
439
|
+
].join("|");
|
|
440
|
+
const COMMONMARK_TYPE_1 = /^ {0,3}<(script|pre|style|textarea)(?:[\t ]|>|$)/i;
|
|
441
|
+
const COMMONMARK_TYPE_6 = new RegExp(`^ {0,3}</?(?:${COMMONMARK_BLOCK_TAGS})(?:[\\t ]|/?>|$)`, "i");
|
|
442
|
+
const HTML_ATTRIBUTE_NAME = "[A-Za-z_:][A-Za-z0-9_.:-]*";
|
|
443
|
+
const HTML_ATTRIBUTE_VALUE = `(?:[^\\s"'=<>\\x60]+|'[^']*'|"[^"]*")`;
|
|
444
|
+
const HTML_ATTRIBUTE = `(?:[\\t ]+${HTML_ATTRIBUTE_NAME}(?:[\\t ]*=[\\t ]*${HTML_ATTRIBUTE_VALUE})?)`;
|
|
445
|
+
const COMMONMARK_TYPE_7 = new RegExp(`^ {0,3}(?:<[A-Za-z][A-Za-z0-9-]*${HTML_ATTRIBUTE}*[\\t ]*/?>|</[A-Za-z][A-Za-z0-9-]*[\\t ]*>)[\\t ]*$`);
|
|
446
|
+
/** Attachment-looking examples inside CommonMark HTML blocks stay literal.
|
|
447
|
+
* Types 1-5 use their specified terminator; types 6-7 last through the next
|
|
448
|
+
* blank line. The app's pasted-text wrapper is deliberately stronger than a
|
|
449
|
+
* generic custom tag and lasts through its closing tag, including blanks. */
|
|
450
|
+
function transcriptBlockStarting(line) {
|
|
451
|
+
const lower = line.toLowerCase();
|
|
452
|
+
const commentStart = lower.indexOf("<!--");
|
|
453
|
+
if (commentStart >= 0 && lower.indexOf("-->", commentStart + 4) < 0) {
|
|
454
|
+
return { kind: "untilToken", closingToken: "-->" };
|
|
455
|
+
}
|
|
456
|
+
const content = line.match(/^ {0,3}(.*)$/)?.[1];
|
|
457
|
+
if (content === undefined)
|
|
458
|
+
return null;
|
|
459
|
+
const lowerContent = content.toLowerCase();
|
|
460
|
+
const pastedText = /^<pasted-text(?:[\t >]|$)/i.exec(content);
|
|
461
|
+
if (pastedText) {
|
|
462
|
+
return lowerContent.includes("</pasted-text>")
|
|
463
|
+
? null
|
|
464
|
+
: { kind: "untilToken", closingToken: "</pasted-text>" };
|
|
465
|
+
}
|
|
466
|
+
const typeOne = COMMONMARK_TYPE_1.exec(line);
|
|
467
|
+
if (typeOne) {
|
|
468
|
+
const closingToken = `</${typeOne[1].toLowerCase()}>`;
|
|
469
|
+
return lower.includes(closingToken)
|
|
470
|
+
? null
|
|
471
|
+
: { kind: "untilToken", closingToken };
|
|
472
|
+
}
|
|
473
|
+
const processing = lowerContent.indexOf("<?");
|
|
474
|
+
if (processing === 0) {
|
|
475
|
+
return lowerContent.indexOf("?>", 2) >= 0
|
|
476
|
+
? null
|
|
477
|
+
: { kind: "untilToken", closingToken: "?>" };
|
|
478
|
+
}
|
|
479
|
+
const cdata = lowerContent.indexOf("<![cdata[");
|
|
480
|
+
if (cdata === 0) {
|
|
481
|
+
return lowerContent.indexOf("]]>", 9) >= 0
|
|
482
|
+
? null
|
|
483
|
+
: { kind: "untilToken", closingToken: "]]>" };
|
|
484
|
+
}
|
|
485
|
+
if (/^<![A-Za-z]/.test(content)) {
|
|
486
|
+
return content.indexOf(">", 2) >= 0
|
|
487
|
+
? null
|
|
488
|
+
: { kind: "untilToken", closingToken: ">" };
|
|
489
|
+
}
|
|
490
|
+
if (COMMONMARK_TYPE_6.test(line) || COMMONMARK_TYPE_7.test(line)) {
|
|
491
|
+
return { kind: "untilBlank" };
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
const TRANSCRIPT_ATTACHMENT_TAG = /^<attached-(image|file)[\t ]+path="([^"\r\n]*)"(?:[\t ]+name="([^"\r\n]*)")?[\t ]*\/>[\t ]*$/;
|
|
496
|
+
/** Split a stored user message into its display text and attachments for
|
|
497
|
+
* transcript rendering. Prompt-only tags never show in the bubble. */
|
|
498
|
+
export function splitTranscriptAttachments(text) {
|
|
499
|
+
const images = [];
|
|
500
|
+
const files = [];
|
|
501
|
+
let display = "";
|
|
502
|
+
let fence = null;
|
|
503
|
+
let block = null;
|
|
504
|
+
let cursor = 0;
|
|
505
|
+
while (cursor < text.length) {
|
|
506
|
+
const newline = text.indexOf("\n", cursor);
|
|
507
|
+
const lineEnd = newline >= 0 ? newline : text.length;
|
|
508
|
+
const wholeLineEnd = newline >= 0 ? newline + 1 : text.length;
|
|
509
|
+
const rawLine = text.slice(cursor, lineEnd);
|
|
510
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
511
|
+
const marker = transcriptFenceMarker(line);
|
|
512
|
+
let consumed = false;
|
|
513
|
+
if (fence) {
|
|
514
|
+
if (marker &&
|
|
515
|
+
marker.marker === fence.marker &&
|
|
516
|
+
marker.length >= fence.length &&
|
|
517
|
+
/^[\t ]*$/.test(marker.remainder)) {
|
|
518
|
+
fence = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else if (block) {
|
|
522
|
+
if (block.kind === "untilBlank") {
|
|
523
|
+
if (/^[\t ]*$/.test(line))
|
|
524
|
+
block = null;
|
|
525
|
+
}
|
|
526
|
+
else if (line.toLowerCase().includes(block.closingToken)) {
|
|
527
|
+
block = null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else if (marker) {
|
|
531
|
+
fence = { marker: marker.marker, length: marker.length };
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
const match = TRANSCRIPT_ATTACHMENT_TAG.exec(line);
|
|
535
|
+
if (match) {
|
|
536
|
+
const kind = match[1];
|
|
537
|
+
const path = decodeAttachmentAttribute(match[2]);
|
|
538
|
+
if (path) {
|
|
539
|
+
const attachment = {
|
|
540
|
+
path,
|
|
541
|
+
name: transcriptFileName(path, match[3]),
|
|
542
|
+
...(isPrivateAttachmentPath(path) ? { private: true } : {}),
|
|
543
|
+
};
|
|
544
|
+
if (kind === "image")
|
|
545
|
+
images.push(attachment);
|
|
546
|
+
else
|
|
547
|
+
files.push(attachment);
|
|
548
|
+
consumed = true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (!consumed)
|
|
552
|
+
block = transcriptBlockStarting(line);
|
|
553
|
+
}
|
|
554
|
+
if (!consumed)
|
|
555
|
+
display += text.slice(cursor, wholeLineEnd);
|
|
556
|
+
cursor = wholeLineEnd;
|
|
557
|
+
}
|
|
558
|
+
return { display: display.trim(), images, files };
|
|
559
|
+
}
|
|
560
|
+
/** Kept for callers outside the desktop bundle that used the old helper. */
|
|
561
|
+
export function splitAttachedImages(text) {
|
|
562
|
+
const { display, images } = splitTranscriptAttachments(text);
|
|
563
|
+
return { display, images: images.map((image) => image.path) };
|
|
564
|
+
}
|
|
565
|
+
/** The bare filename a saved attachment path ends in — what the serving
|
|
566
|
+
* route expects. Works for POSIX and Windows separators. */
|
|
567
|
+
export function attachmentBasename(path) {
|
|
568
|
+
const parts = path.split(/[\\/]/);
|
|
569
|
+
return parts[parts.length - 1] ?? "";
|
|
570
|
+
}
|
|
571
|
+
const previewHandoffs = new Map();
|
|
572
|
+
const PREVIEW_HANDOFF_MS = 60_000;
|
|
573
|
+
function revokeBlobUrl(url) {
|
|
574
|
+
if (!url?.startsWith("blob:") || typeof URL.revokeObjectURL !== "function")
|
|
575
|
+
return;
|
|
576
|
+
URL.revokeObjectURL(url);
|
|
577
|
+
}
|
|
578
|
+
/** Keep the already-decoded local image visible while the canonical message
|
|
579
|
+
* and its cacheable server URL replace the optimistic row. */
|
|
580
|
+
export function handoffAttachmentImagePreview(path, previewUrl) {
|
|
581
|
+
if (!path || !previewUrl?.startsWith("blob:"))
|
|
582
|
+
return;
|
|
583
|
+
const previous = previewHandoffs.get(path);
|
|
584
|
+
if (previous) {
|
|
585
|
+
clearTimeout(previous.timer);
|
|
586
|
+
if (previous.url !== previewUrl)
|
|
587
|
+
revokeBlobUrl(previous.url);
|
|
588
|
+
}
|
|
589
|
+
const timer = setTimeout(() => {
|
|
590
|
+
if (previewHandoffs.get(path)?.url !== previewUrl)
|
|
591
|
+
return;
|
|
592
|
+
previewHandoffs.delete(path);
|
|
593
|
+
revokeBlobUrl(previewUrl);
|
|
594
|
+
}, PREVIEW_HANDOFF_MS);
|
|
595
|
+
timer.unref?.();
|
|
596
|
+
previewHandoffs.set(path, { url: previewUrl, timer });
|
|
597
|
+
}
|
|
598
|
+
/** Remove a draft image and its local pixels. Sent images deliberately skip
|
|
599
|
+
* this path so their optimistic-to-server handoff can finish. */
|
|
600
|
+
export function releaseAttachmentImagePreview(attachment) {
|
|
601
|
+
const handed = attachment.path ? previewHandoffs.get(attachment.path) : undefined;
|
|
602
|
+
if (handed && handed.url === attachment.previewUrl) {
|
|
603
|
+
clearTimeout(handed.timer);
|
|
604
|
+
previewHandoffs.delete(attachment.path);
|
|
605
|
+
}
|
|
606
|
+
revokeBlobUrl(attachment.previewUrl);
|
|
607
|
+
}
|
|
608
|
+
/** The renderer never loads a transcript-provided URL directly. Only names
|
|
609
|
+
* the attachment server itself can have generated become same-origin image
|
|
610
|
+
* URLs; malformed and executable-image paths render nothing, while a string
|
|
611
|
+
* that looks remote can at most resolve to a local generated filename. */
|
|
612
|
+
export function attachmentImageUrl(path) {
|
|
613
|
+
const local = previewHandoffs.get(path);
|
|
614
|
+
if (local)
|
|
615
|
+
return local.url;
|
|
616
|
+
const name = attachmentBasename(path);
|
|
617
|
+
if (!/^[A-Za-z0-9-]+\.(png|jpg|gif|webp)$/.test(name))
|
|
618
|
+
return null;
|
|
619
|
+
return `/api/attachments/${encodeURIComponent(name)}`;
|
|
620
|
+
}
|
|
621
|
+
/** One intake path for files arriving by drop OR by the composer's attach
|
|
622
|
+
* button, so a picked file and a dropped one can never behave differently.
|
|
623
|
+
* Uploaders are injected for deterministic tests: callers own the network,
|
|
624
|
+
* while this function owns ordering and the sentence shown on failure. */
|
|
625
|
+
export async function intakeFiles(_files, _opts) {
|
|
626
|
+
const files = [..._files];
|
|
627
|
+
const { allowImages, getPath, uploadImage } = _opts;
|
|
628
|
+
const uploadFile = _opts.uploadFile ?? (async (file) => {
|
|
629
|
+
if (!file.arrayBuffer)
|
|
630
|
+
return null;
|
|
631
|
+
return fileAttachmentFromFile(file);
|
|
632
|
+
});
|
|
633
|
+
// Promise.all starts every optimistic image preview immediately while its
|
|
634
|
+
// result array still preserves the order in which files were selected.
|
|
635
|
+
const results = await Promise.all(files.map(async (file) => {
|
|
636
|
+
if (allowImages && isImageFile(file)) {
|
|
637
|
+
try {
|
|
638
|
+
const attachment = await uploadImage(file);
|
|
639
|
+
return { attachments: attachment ? [attachment] : [], rejectedNames: [] };
|
|
640
|
+
}
|
|
641
|
+
catch (err) {
|
|
642
|
+
return {
|
|
643
|
+
attachments: [],
|
|
644
|
+
rejectedNames: [],
|
|
645
|
+
uploadError: `${file.name}: ${err instanceof Error ? err.message : "upload failed"}`,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
const attachment = await uploadFile(file);
|
|
651
|
+
if (attachment) {
|
|
652
|
+
return { attachments: [attachment], rejectedNames: [] };
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
return {
|
|
657
|
+
attachments: [],
|
|
658
|
+
rejectedNames: [],
|
|
659
|
+
uploadError: `${file.name}: ${err instanceof Error ? err.message : "upload failed"}`,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const result = await attachmentsFromDroppedFiles([file], getPath);
|
|
663
|
+
return result;
|
|
664
|
+
}));
|
|
665
|
+
const attachments = results.flatMap((result) => result.attachments);
|
|
666
|
+
const rejectedNames = results.flatMap((result) => result.rejectedNames);
|
|
667
|
+
const uploadErrors = results.flatMap((result) => result.uploadError ? [result.uploadError] : []);
|
|
668
|
+
const pathless = rejectedNames.length
|
|
669
|
+
? `${rejectedNames.join(", ")} — that file has no path on disk. Save it first, then attach it from Finder.`
|
|
670
|
+
: null;
|
|
671
|
+
const failed = uploadErrors.length ? uploadErrors.join("; ") : null;
|
|
672
|
+
return {
|
|
673
|
+
attachments,
|
|
674
|
+
notice: pathless && failed ? `${pathless} (${failed})` : (pathless ?? failed),
|
|
675
|
+
};
|
|
676
|
+
}
|