@mavea/mavea 1.0.7
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/CHANGELOG.md +159 -0
- package/DISCLAIMER.md +16 -0
- package/LICENSE +75 -0
- package/PRIVACY.md +127 -0
- package/README.md +248 -0
- package/SECURITY.md +72 -0
- package/SUPPORT.md +13 -0
- package/TERMS.md +122 -0
- package/THIRD-PARTY.txt +639 -0
- package/TRADEMARKS.md +15 -0
- package/bin/mavea.mjs +1079 -0
- package/dist/.vite/manifest.json +4261 -0
- package/dist/_headers +52 -0
- package/dist/assets/AppCommandPalette-DUMSp3rd.js +1 -0
- package/dist/assets/AtlasView-B5fkQhO6.css +1 -0
- package/dist/assets/AtlasView-Dl2fssaG.js +1 -0
- package/dist/assets/BlockEmpty-CLtVQf6c.js +1 -0
- package/dist/assets/BriefingPlayer-DO205Qkh.js +1 -0
- package/dist/assets/BriefingPlayer-TpFZB77N.css +1 -0
- package/dist/assets/CanvasSkeleton-BZZPCuRZ.js +2 -0
- package/dist/assets/CanvasSkeleton-iy6DBvde.css +1 -0
- package/dist/assets/CardEditor-BY1bZiV6.css +1 -0
- package/dist/assets/CardEditor-DzJXb0G7.js +1 -0
- package/dist/assets/CheckLogRail-F4AlgAcc.css +1 -0
- package/dist/assets/CheckLogRail-wUfby67Z.js +1 -0
- package/dist/assets/CodeOutput-Kh6ldxQz.js +97 -0
- package/dist/assets/CommandPalette-Bb-eErih.css +1 -0
- package/dist/assets/CommandPalette-Kl00mMZm.js +1 -0
- package/dist/assets/CourseLessonReader-COmTQdSK.css +1 -0
- package/dist/assets/CourseLessonReader-MBHoknUz.js +2 -0
- package/dist/assets/CourseRail-CUa7uIGG.css +1 -0
- package/dist/assets/CourseRail-q6x5cCpW.js +1 -0
- package/dist/assets/CoursesApp-v89OVJvg.js +2 -0
- package/dist/assets/CrossExamPanel-CZ0ShGma.css +1 -0
- package/dist/assets/CrossExamPanel-DH04p6VS.js +1 -0
- package/dist/assets/DashPresent-BYXbPuGj.js +1 -0
- package/dist/assets/DashPresent-DQYcgp-W.css +1 -0
- package/dist/assets/DashboardDetail-B50ZkeVK.js +2 -0
- package/dist/assets/DashboardLoopRuntime-CFXPx3v_.js +1 -0
- package/dist/assets/DashboardOverview-C-FdGWM5.js +1 -0
- package/dist/assets/DashboardSettings-Dmw6JTbI.js +1 -0
- package/dist/assets/DashboardsApp-Dx-EsaPW.css +1 -0
- package/dist/assets/DashboardsApp-JSY_bVfi.js +2 -0
- package/dist/assets/DeepZoomApp-BFNwli7l.js +37 -0
- package/dist/assets/DeepZoomApp-fRXCJHHZ.css +1 -0
- package/dist/assets/DelegatePanel-BK7rEhIc.js +26 -0
- package/dist/assets/DelegatePanel-_9RZiLHM.css +1 -0
- package/dist/assets/DemoGallery-BPtVG7_y.js +1 -0
- package/dist/assets/DropSelect-DUhdQSta.js +1 -0
- package/dist/assets/ExportModal-D4WpAg2c.css +1 -0
- package/dist/assets/ExportModal-D65cGyj_.js +2 -0
- package/dist/assets/ExtractionPreview-Iwfg2Ogl.js +18 -0
- package/dist/assets/FallbackCard-B489rTd9.js +1 -0
- package/dist/assets/FallbackCard-D_6ps1xA.css +1 -0
- package/dist/assets/FeatureUseNotice-CWt7gOwS.css +1 -0
- package/dist/assets/FeatureUseNotice-DPtQ0OGb.js +1 -0
- package/dist/assets/FlagshipCTA-BXntKhxb.js +1 -0
- package/dist/assets/FlagshipCommandPalette-BDJfasI-.js +1 -0
- package/dist/assets/FlagshipShowcase-DJ8Gxk_G.js +1 -0
- package/dist/assets/FlashcardsApp-CLNo4WX7.js +1 -0
- package/dist/assets/ForecastPanel-Da2ZBUrE.css +1 -0
- package/dist/assets/ForecastPanel-PBO2-dgS.js +1 -0
- package/dist/assets/GalleryApp-B1CB3_81.css +1 -0
- package/dist/assets/GalleryApp-DM03CMRy.js +1 -0
- package/dist/assets/HonestByDesign-Dqyth6nW.js +1 -0
- package/dist/assets/HowItWorks-C0zosIvq.js +1 -0
- package/dist/assets/LegalApp-DZBdExTy.js +1 -0
- package/dist/assets/LegalMarkdownDocument-CSZhRG5r.js +3 -0
- package/dist/assets/LegalPageShell-C_HottbD.css +1 -0
- package/dist/assets/LegalPageShell-DTeXqWyc.js +1 -0
- package/dist/assets/LeverPanel-DhpJY76O.css +1 -0
- package/dist/assets/LeverPanel-aZfhtVJX.js +1 -0
- package/dist/assets/Library-D2XGNXz7.js +1 -0
- package/dist/assets/Library-DmrtHjUf.css +1 -0
- package/dist/assets/LibraryOverlay-B2WQSgwr.js +1 -0
- package/dist/assets/LibraryOverlay-CnKWDMG_.css +1 -0
- package/dist/assets/LiveApp-B9zwXlGC.js +61 -0
- package/dist/assets/LiveApp-BVZGpJ1S.css +1 -0
- package/dist/assets/LiveSettings-CmgrMqvK.css +1 -0
- package/dist/assets/LiveSettings-D9OcWlm6.js +6 -0
- package/dist/assets/MindMapViewerDrawer-YYyZqmAd.js +1 -0
- package/dist/assets/MindShape-Ca3MXEgK.css +1 -0
- package/dist/assets/MindShape-mTlN5mPZ.js +1 -0
- package/dist/assets/Orb-B0NEVfqL.js +1 -0
- package/dist/assets/PinToDashboard-_kIOHs0k.js +1 -0
- package/dist/assets/PlanReview-33V8GTNB.js +1 -0
- package/dist/assets/Presence-CzLJIeSD.js +1 -0
- package/dist/assets/PresentationDeck-BDoGecxr.js +1 -0
- package/dist/assets/PrismApp-PfBTFhMF.js +2 -0
- package/dist/assets/PrismAskController-DBoxQSJR.css +1 -0
- package/dist/assets/PrismAskController-DbQDrlU9.js +37 -0
- package/dist/assets/PrismBriefingPlayer-1fvlfLR8.js +2 -0
- package/dist/assets/PrismOverlay-B7lEspdu.js +131 -0
- package/dist/assets/PrismOverlay-BTfAicBt.css +1 -0
- package/dist/assets/PrivacyApp-CR4lRs0L.js +128 -0
- package/dist/assets/Quiz-CUwtekfu.js +1 -0
- package/dist/assets/Recap-DCiiWNiV.css +1 -0
- package/dist/assets/Recap-H5fhj5Tc.js +1 -0
- package/dist/assets/ReelJelly-3jSVsJsy.js +1 -0
- package/dist/assets/RehearsalPanel-BCU615wg.css +1 -0
- package/dist/assets/RehearsalPanel-BVgjrOPg.js +6 -0
- package/dist/assets/ReplayCard-BpYO5vX-.js +2 -0
- package/dist/assets/ReplayOverlay-DXmVdeIr.js +1 -0
- package/dist/assets/RewindOverlay-BusfWeFq.js +1 -0
- package/dist/assets/RewindOverlay-C92qgvmi.css +1 -0
- package/dist/assets/RippleApp-9KrJPYgN.js +2 -0
- package/dist/assets/RippleAskController-BNAUoIW0.css +1 -0
- package/dist/assets/RippleAskController-CO9wE7hU.js +34 -0
- package/dist/assets/RippleOverlay-DMHLbfh8.js +6 -0
- package/dist/assets/RippleOverlay-_iiZsQVZ.css +1 -0
- package/dist/assets/SeeDontRead-C389viue.js +1 -0
- package/dist/assets/ShareModal-C8S7ySls.css +1 -0
- package/dist/assets/ShareModal-ClnEkudw.js +32 -0
- package/dist/assets/ShipCascade-BUtwvQoW.js +1 -0
- package/dist/assets/ShipCascade-CyFnrlET.css +1 -0
- package/dist/assets/ShipCourse-B7lG6DYW.js +1 -0
- package/dist/assets/ShipCourse-DcT7PUSm.css +1 -0
- package/dist/assets/ShipGate-BVZfCllb.css +1 -0
- package/dist/assets/ShipGate-BYYu9WVq.js +2 -0
- package/dist/assets/ShipHotspots-Bj2CovtT.js +1 -0
- package/dist/assets/ShipHotspots-CLOMutPv.css +1 -0
- package/dist/assets/ShipIncident-BUHvFFiC.css +1 -0
- package/dist/assets/ShipIncident-DcmindmE.js +2 -0
- package/dist/assets/ShipMigration-BznCskNg.css +1 -0
- package/dist/assets/ShipMigration-s0hVjDQl.js +1 -0
- package/dist/assets/ShipOnboarding-BC9Vxkpa.css +1 -0
- package/dist/assets/ShipOnboarding-ekbHL7OH.js +1 -0
- package/dist/assets/ShipRead-BVNgQT6L.js +1 -0
- package/dist/assets/ShipRollout-BdUQzMyP.js +1 -0
- package/dist/assets/ShipRollout-CqeObx5b.css +1 -0
- package/dist/assets/ShipSuggestions-B6fKYh8L.js +1 -0
- package/dist/assets/ShipSuggestions-BKRvk5Mj.css +1 -0
- package/dist/assets/ShipWorkspace-3D3Rrs1p.js +2 -0
- package/dist/assets/ShipWorkspace-D6XQpGEN.css +1 -0
- package/dist/assets/SignatureLoop-C60r8Wm5.js +1 -0
- package/dist/assets/SrsReview-BoaYb6XN.js +1 -0
- package/dist/assets/SrsReview-DmGdGxT0.css +1 -0
- package/dist/assets/SynthesisApp-vJ2Jq0Qe.js +2 -0
- package/dist/assets/SynthesisOverlay-CkERphvG.js +66 -0
- package/dist/assets/TermsApp-C70tW6m6.js +123 -0
- package/dist/assets/TopicCanvas-CT0eSj2M.js +2 -0
- package/dist/assets/TopicCanvas-D2Nax9II.css +1 -0
- package/dist/assets/TourPrism-BvucSkcs.js +1 -0
- package/dist/assets/TrackerTile-CasZNmt4.js +2 -0
- package/dist/assets/TwoSurfaces-C90X7Aj5.js +1 -0
- package/dist/assets/UnderstoodPanel-CHPD2Ttj.js +1 -0
- package/dist/assets/UnderstoodPanel-Q6GX1TFq.css +1 -0
- package/dist/assets/WhyMachineOverlay-BZk3PVTl.css +1 -0
- package/dist/assets/WhyMachineOverlay-CB0_n6Sy.js +1 -0
- package/dist/assets/WowFeatures-Ci_H5QYX.js +1 -0
- package/dist/assets/ZoomDeck-B2SjNwY7.css +1 -0
- package/dist/assets/ZoomDeck-BQHO79ZU.js +1 -0
- package/dist/assets/__vite-browser-external-BufR4LQa.js +1 -0
- package/dist/assets/ai-Ba4lZJ9Y.js +12 -0
- package/dist/assets/alternateFinishes-DZ-rkpkX.js +494 -0
- package/dist/assets/annotationAuto-B0nihA4F.js +1 -0
- package/dist/assets/annotationReel-BcU9Q0Sw.js +1 -0
- package/dist/assets/annotations-D2K9A5GB.js +1 -0
- package/dist/assets/backup-c-Btapzp.js +1 -0
- package/dist/assets/blockLabel-DAqaHKy3.js +1 -0
- package/dist/assets/c-DWGmR78Y.js +1 -0
- package/dist/assets/c-SSSlmE39.js +1 -0
- package/dist/assets/cache-BumECILO.js +1 -0
- package/dist/assets/canvas-QTEIiK23.js +1 -0
- package/dist/assets/catalog-BHjl-1uH.js +2 -0
- package/dist/assets/catalog-Dqaq2YFd.js +1 -0
- package/dist/assets/cfo.generated-Bjl5OGay.js +1 -0
- package/dist/assets/chapters-BiUsDClQ.js +1 -0
- package/dist/assets/charts1-EOoxU_K3.js +1 -0
- package/dist/assets/charts2-tyUiQPff.js +1 -0
- package/dist/assets/chunk-BVpWp9h4.js +1 -0
- package/dist/assets/citation-BcvmjKS8.js +1 -0
- package/dist/assets/code-CpjYCNS8.js +8 -0
- package/dist/assets/codeContext-0UMRSGRr.js +5 -0
- package/dist/assets/colors-D49Req-e.js +1 -0
- package/dist/assets/compose-BJ9EfcQS.js +3 -0
- package/dist/assets/contentVault-DzTYWcbD.js +1 -0
- package/dist/assets/core-DEFyprhs.js +1 -0
- package/dist/assets/corpus.generated-oX9uU93S.js +11 -0
- package/dist/assets/courseSeed-CZdNBTTs.js +1 -0
- package/dist/assets/courseStore-DfafnL-9.js +1 -0
- package/dist/assets/courses-BkH1ntDL.css +1 -0
- package/dist/assets/cpp-BGdv8dzs.js +1 -0
- package/dist/assets/cpp-CNAAXZl-.js +1 -0
- package/dist/assets/csharp-oqKa8noW.js +1 -0
- package/dist/assets/css-CdtkT07m.js +1 -0
- package/dist/assets/css-LbU1hoFO.js +1 -0
- package/dist/assets/dag-C3Xx8pOD.js +2 -0
- package/dist/assets/dashboardEvents-DR9d_c74.js +2 -0
- package/dist/assets/dashboards-rcWxdoXI.css +1 -0
- package/dist/assets/deckLine-NBd3MyGT.js +1 -0
- package/dist/assets/demoPaletteItems-NPCSaSU6.js +1 -0
- package/dist/assets/dev.generated-DYgVxL_4.js +73 -0
- package/dist/assets/diagrams-CaSfjT4I.js +1 -0
- package/dist/assets/diff-woXpYk--.js +1 -0
- package/dist/assets/display-OvC57CMc.js +9 -0
- package/dist/assets/dist-BFx3nZg4.js +2785 -0
- package/dist/assets/dist-xRGLOZwv.js +14 -0
- package/dist/assets/docs-B2aDtm5K.js +1 -0
- package/dist/assets/everyday-CoZa4kDZ.js +1 -0
- package/dist/assets/explode-WBykfhqW.js +13 -0
- package/dist/assets/extractNumbers-CuhagRkd.js +1 -0
- package/dist/assets/facts-BBTt3ImG.js +1 -0
- package/dist/assets/finance-Bxn_Q1p0.js +1 -0
- package/dist/assets/fit-0HxU-rtB.js +1 -0
- package/dist/assets/fitText-CDFmCQzl.js +1 -0
- package/dist/assets/flows-BL81J7yN.js +1 -0
- package/dist/assets/format-DlZTyeMy.js +1 -0
- package/dist/assets/format-hc2QOoIu.js +1 -0
- package/dist/assets/forms-DeoXOxdD.js +1 -0
- package/dist/assets/generate-8odaTyr4.js +17 -0
- package/dist/assets/generateBeats-b64CL54v.js +1 -0
- package/dist/assets/generateCourse-Cj_-vDWu.js +28 -0
- package/dist/assets/generateLive-D4B30fnF.js +52 -0
- package/dist/assets/geometry-DXAbnb0J.js +1 -0
- package/dist/assets/gesture-DabMpPcq.js +2 -0
- package/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/assets/githubBrowser-DywfDeYx.js +1 -0
- package/dist/assets/githubToken-DtcLYqwm.js +11 -0
- package/dist/assets/go-rLFTqkRN.js +1 -0
- package/dist/assets/homeTarget-BgWKI_E1.js +1 -0
- package/dist/assets/html-BcACZWSP.js +1 -0
- package/dist/assets/html-DDNsd9lV.js +1 -0
- package/dist/assets/http-CJCtjYpA.js +2 -0
- package/dist/assets/icons-CHVz6kv_.js +1 -0
- package/dist/assets/index-CepQEz75.css +1 -0
- package/dist/assets/index-y8GL67bB.js +22 -0
- package/dist/assets/index.es-CRLiWwiA.js +5 -0
- package/dist/assets/info-CiqHR5cs.js +1 -0
- package/dist/assets/java-BtIQNnN0.js +1 -0
- package/dist/assets/java-DxlWrXwe.js +1 -0
- package/dist/assets/javascript-BzbQS41l.js +1 -0
- package/dist/assets/javascript-Yelw-5ZN.js +1 -0
- package/dist/assets/json-B0N3XRZU.js +1 -0
- package/dist/assets/json-B88Oqtu5.js +1 -0
- package/dist/assets/json-CaljABEy.js +1 -0
- package/dist/assets/jspdf.es.min-C-Pf79px.js +79 -0
- package/dist/assets/jsx-D3KKoC5L.js +1 -0
- package/dist/assets/jsx-hEbJ82HF.js +1 -0
- package/dist/assets/jsx-runtime-DaSxXjOY.js +1 -0
- package/dist/assets/katex-BMCG0xok.js +257 -0
- package/dist/assets/keyVault-DEsNh9FR.js +1 -0
- package/dist/assets/kokoro-D4GKpSL0.js +1 -0
- package/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/dist/assets/layout-BFQOzXtG.js +1 -0
- package/dist/assets/layout-CkO0Psde.js +1 -0
- package/dist/assets/lazyRetry-D50B1UMY.js +1 -0
- package/dist/assets/leaflet-src-C_xhOQY7.js +1 -0
- package/dist/assets/learn-DVfqN-nf.js +1 -0
- package/dist/assets/lib-DMAxsZKm.css +1 -0
- package/dist/assets/lib-VFqp6Y3w.js +1 -0
- package/dist/assets/links--u_phi0x.js +1 -0
- package/dist/assets/liveSchema-CJ49PX5R.js +98 -0
- package/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/assets/mastery-BPjxP0fN.js +4 -0
- package/dist/assets/media-CsrUl279.js +43 -0
- package/dist/assets/model-BE0UHu1w.js +27 -0
- package/dist/assets/motion-S0NRruNj.js +1 -0
- package/dist/assets/nav-Czm-NzPV.js +1 -0
- package/dist/assets/neighborhoods-C-1Tjr1r.js +1 -0
- package/dist/assets/notes-A9t4jdfx.css +1 -0
- package/dist/assets/notes-lROPCorS.js +1 -0
- package/dist/assets/notify-Bz4wuE4T.js +1 -0
- package/dist/assets/now-C9-UAzTC.js +1 -0
- package/dist/assets/number-DXgzss0v.js +1 -0
- package/dist/assets/officeDoc-B6nEhNp4.js +3 -0
- package/dist/assets/openchemlib-nMLMmg26.js +23 -0
- package/dist/assets/overlays-JxbHtA2o.js +1 -0
- package/dist/assets/owners-FXDGInVq.js +2 -0
- package/dist/assets/pageVisibility-BxUVsw7Z.js +1 -0
- package/dist/assets/pdf-Df7PCH7q.js +11 -0
- package/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/dist/assets/penStrokes-9Q_l7NTt.js +1 -0
- package/dist/assets/perfProbe-COAp9PAa.js +1 -0
- package/dist/assets/php-CYrBA5iX.js +1 -0
- package/dist/assets/pickers-B4-3HOR8.js +1 -0
- package/dist/assets/pin-Bya3BL_a.css +1 -0
- package/dist/assets/pin-CXSuPJYL.js +1 -0
- package/dist/assets/planTracker-7AMCQPNx.js +1 -0
- package/dist/assets/portal-DMw7C32j.js +1 -0
- package/dist/assets/pptxgen.es-CvTILmV9.js +115 -0
- package/dist/assets/predictions-CYS7Vo39.js +1 -0
- package/dist/assets/prewarm--whjQhWJ.js +1 -0
- package/dist/assets/prism-app-CXYElq8-.css +1 -0
- package/dist/assets/prism.generated-BhbHDHR6.js +41 -0
- package/dist/assets/prismExtract.worker-Cws9K1NG.js +13 -0
- package/dist/assets/prismGround.worker-CIvKqxm2.js +1 -0
- package/dist/assets/prismLayout.worker-CZO2CY7B.js +1 -0
- package/dist/assets/providers-wzm7E8Pa.js +1 -0
- package/dist/assets/purify.es-adlwq8Pz.js +3 -0
- package/dist/assets/python-gzcpVVnB.js +1 -0
- package/dist/assets/queue-DHeyJ32F.css +1 -0
- package/dist/assets/queue-J4h7E3uc.js +1 -0
- package/dist/assets/quietHours-BvcsBKKK.js +1 -0
- package/dist/assets/react-BQSORdFI.js +1 -0
- package/dist/assets/reference-sJzyMiL4.js +1 -0
- package/dist/assets/registry-1GuPBfQK.js +1 -0
- package/dist/assets/registry-2GJZ46M8.css +1 -0
- package/dist/assets/registry-2v0jlh5Q.js +2 -0
- package/dist/assets/registry-B8DStMJk.js +1 -0
- package/dist/assets/registry-B940W2j5.js +1 -0
- package/dist/assets/registry-BAoRB8rd.css +1 -0
- package/dist/assets/registry-BPvIPLke.js +1 -0
- package/dist/assets/registry-BVHiKEwE.css +1 -0
- package/dist/assets/registry-BcZXBr3y.css +1 -0
- package/dist/assets/registry-BffpZK5w.js +1 -0
- package/dist/assets/registry-BgGeRiDA.js +2 -0
- package/dist/assets/registry-Buq5uuCv.js +1 -0
- package/dist/assets/registry-BzNLfje1.css +1 -0
- package/dist/assets/registry-C3ezxLR3.css +1 -0
- package/dist/assets/registry-CAusoyBh.js +1 -0
- package/dist/assets/registry-CEVrY72Z.js +1 -0
- package/dist/assets/registry-CKjKq6ct.js +1 -0
- package/dist/assets/registry-CNgybLQv.css +1 -0
- package/dist/assets/registry-CRM7y8QT.css +1 -0
- package/dist/assets/registry-CRisz4Ca.css +1 -0
- package/dist/assets/registry-Cg7qUZPS.js +1 -0
- package/dist/assets/registry-Cix-yqch.js +1 -0
- package/dist/assets/registry-CtbuZWYz.css +1 -0
- package/dist/assets/registry-DCI_BeFJ.js +5 -0
- package/dist/assets/registry-DDLSYDVA.js +1 -0
- package/dist/assets/registry-DI1wSG1Y.js +1 -0
- package/dist/assets/registry-DJinB5ye.js +2 -0
- package/dist/assets/registry-DUvXX_d0.js +1 -0
- package/dist/assets/registry-DVzLacl_.css +1 -0
- package/dist/assets/registry-DWcNVyI2.css +1 -0
- package/dist/assets/registry-D_URPoL4.js +1 -0
- package/dist/assets/registry-Dkc7hN5T.js +1 -0
- package/dist/assets/registry-DmQlNN3y.js +1 -0
- package/dist/assets/registry-DpaaVbmL.css +1 -0
- package/dist/assets/registry-Dpb8EXHn.js +49 -0
- package/dist/assets/registry-Dru0JcTa.css +1 -0
- package/dist/assets/registry-DujBAgn_.js +1 -0
- package/dist/assets/registry-Dye5m3qt.css +1 -0
- package/dist/assets/registry-GeSy4i7R.css +1 -0
- package/dist/assets/registry-QcULZpdE.js +4 -0
- package/dist/assets/registry-T3i2Q9SI.css +1 -0
- package/dist/assets/registry-UFIuwh3i.css +1 -0
- package/dist/assets/registry-Xzq_Rpmm.css +1 -0
- package/dist/assets/registry-ZOWzhOHk.css +1 -0
- package/dist/assets/registry-aIH-Im5Y.css +1 -0
- package/dist/assets/registry-aazwkBJp.js +1 -0
- package/dist/assets/registry-ei6fIgBT.css +1 -0
- package/dist/assets/registry-types-DMuK0vJ6.js +1 -0
- package/dist/assets/registry-upbw6pyj.css +1 -0
- package/dist/assets/registry-zRPjWNl5.js +1 -0
- package/dist/assets/relate-A9ZeaaAI.js +1 -0
- package/dist/assets/richText-BkxJhl9N.js +1 -0
- package/dist/assets/ruby-BhrksQMt.js +1 -0
- package/dist/assets/run-BYP2YlZT.js +23 -0
- package/dist/assets/run-C_0B_41W.js +21 -0
- package/dist/assets/run-DSVvshaB.js +21 -0
- package/dist/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/assets/safeCssColor-di93BXgG.js +1 -0
- package/dist/assets/safeImageUrl-CUkucDUu.js +1 -0
- package/dist/assets/scale-Bcgfo6Hp.js +1 -0
- package/dist/assets/screen-DLR7p_yP.js +1 -0
- package/dist/assets/search-DnjHupRf.js +1 -0
- package/dist/assets/seed-b1xc0dj2.js +1 -0
- package/dist/assets/shapes-RqjqC0ay.js +1 -0
- package/dist/assets/shard000-DM_rqD7W.js +1 -0
- package/dist/assets/shard001-gQEQs7_F.js +1 -0
- package/dist/assets/shard002-DbDas-W-.js +1 -0
- package/dist/assets/shard003-DxMszG4_.js +1 -0
- package/dist/assets/shard004-DZMpLBw1.js +1 -0
- package/dist/assets/shard005-DLVkh9IH.js +1 -0
- package/dist/assets/shard006-B6efKHov.js +1 -0
- package/dist/assets/shard007-AFdZLe38.js +1 -0
- package/dist/assets/shard008-LbSBzwRx.js +1 -0
- package/dist/assets/shard009-D_6-SUDb.js +1 -0
- package/dist/assets/shard010-DbOj-uZW.js +1 -0
- package/dist/assets/shard011-BYSz7EEW.js +1 -0
- package/dist/assets/shard012-B5Ypj7OV.js +1 -0
- package/dist/assets/shard013-BtsliNIe.js +1 -0
- package/dist/assets/shard014-15sjmk68.js +1 -0
- package/dist/assets/shard015-nnXrqru_.js +1 -0
- package/dist/assets/shard016-BayXFE13.js +1 -0
- package/dist/assets/shard017-Cy4cGexs.js +1 -0
- package/dist/assets/shard018-aO-BYJ8H.js +1 -0
- package/dist/assets/shard019-gxaTeA3I.js +1 -0
- package/dist/assets/shard020-BibJdMI0.js +1 -0
- package/dist/assets/shard021-2FE-tze4.js +1 -0
- package/dist/assets/shard022-IXiyG-JK.js +1 -0
- package/dist/assets/shard023-314F6NJS.js +1 -0
- package/dist/assets/shard024-cN5rpQ_X.js +1 -0
- package/dist/assets/shard025-KKTaXOGE.js +1 -0
- package/dist/assets/shard026-C2Dkd1t6.js +1 -0
- package/dist/assets/shard027-Xy-8x9As.js +1 -0
- package/dist/assets/shard028-Bul-klUj.js +1 -0
- package/dist/assets/shard029-BbRSUcUZ.js +1 -0
- package/dist/assets/shard030-GYllsFVC.js +1 -0
- package/dist/assets/shard031-DMigeiyn.js +1 -0
- package/dist/assets/shard032-CcvmzK0l.js +1 -0
- package/dist/assets/shard033-11AU702M.js +1 -0
- package/dist/assets/shard034-Bx3urxjM.js +1 -0
- package/dist/assets/shard035-DBdEeyCt.js +1 -0
- package/dist/assets/shard036-vYgoRBqS.js +1 -0
- package/dist/assets/shard037-DP4MMw91.js +1 -0
- package/dist/assets/shard038-ffxsv7d6.js +1 -0
- package/dist/assets/shard039-C1JHHNp9.js +1 -0
- package/dist/assets/shard040-JbVvCSO_.js +1 -0
- package/dist/assets/shard041-5L_eFwGx.js +1 -0
- package/dist/assets/shard042-C9nsGL79.js +1 -0
- package/dist/assets/shard043-Dx_9HSKI.js +1 -0
- package/dist/assets/shard044-B3QQNC4A.js +1 -0
- package/dist/assets/shard045-B7z0WHO6.js +1 -0
- package/dist/assets/shard046-D1yDablJ.js +1 -0
- package/dist/assets/shard047-DJi3AgeT.js +1 -0
- package/dist/assets/shard048-D_2ipfUr.js +1 -0
- package/dist/assets/shard049-1aGgWZdd.js +1 -0
- package/dist/assets/shard050-Bjwa0Og4.js +1 -0
- package/dist/assets/shard051-Cx_GAk3v.js +1 -0
- package/dist/assets/shard052-LaeOD3V5.js +1 -0
- package/dist/assets/shard053-D-da7Foj.js +1 -0
- package/dist/assets/shard054-CM8eSRQk.js +1 -0
- package/dist/assets/shard055-QRpDl4Vs.js +1 -0
- package/dist/assets/shard056-DkY7CQm3.js +1 -0
- package/dist/assets/shard057-BVCgf7o0.js +1 -0
- package/dist/assets/shard058-93aGtdrX.js +1 -0
- package/dist/assets/shard059-rJE9X1_I.js +1 -0
- package/dist/assets/shard060-Lu_hfl1r.js +1 -0
- package/dist/assets/shard061-C9A8iNiq.js +1 -0
- package/dist/assets/shard062-BBIpEYZO.js +1 -0
- package/dist/assets/shard063-C_uen3Ub.js +1 -0
- package/dist/assets/shard064-DAHUsHST.js +1 -0
- package/dist/assets/shard065-C2edvdvh.js +1 -0
- package/dist/assets/shard066-DFcuVhZm.js +1 -0
- package/dist/assets/shard067-BN8DM8JZ.js +1 -0
- package/dist/assets/shard068-i464K77p.js +1 -0
- package/dist/assets/shard069-Dm9m2a5U.js +1 -0
- package/dist/assets/shard070-BPVvR8GH.js +1 -0
- package/dist/assets/shard071-Bal_0d9L.js +1 -0
- package/dist/assets/shard072-CBQu62kt.js +1 -0
- package/dist/assets/shard073-D5BRae0w.js +1 -0
- package/dist/assets/shard074-DtFvcBnZ.js +1 -0
- package/dist/assets/share-QHGHNDhQ.js +1 -0
- package/dist/assets/shellscript-BiGZg9Om.js +1 -0
- package/dist/assets/shellscript-CqHdAlCp.js +1 -0
- package/dist/assets/shikiHighlight-DUCYbsJ2.js +153 -0
- package/dist/assets/sourceHost-BV3t2XDb.js +1 -0
- package/dist/assets/speed-DwPw9KSv.js +1 -0
- package/dist/assets/spokenText-D4zPX9JY.js +1 -0
- package/dist/assets/sql-0Qcw9ht3.js +1 -0
- package/dist/assets/sql-CP0rg-5b.js +1 -0
- package/dist/assets/src-DbgF7cRH.js +2 -0
- package/dist/assets/stats-BeP5Bws0.js +1 -0
- package/dist/assets/status-BY02LIJt.js +1 -0
- package/dist/assets/stepDriver-DwjFet6U.js +1 -0
- package/dist/assets/store-4JNNn732.js +1 -0
- package/dist/assets/store-BFC7DZcg.js +1 -0
- package/dist/assets/store-BFrJaSxJ.js +1 -0
- package/dist/assets/store-DXmC_fP2.js +0 -0
- package/dist/assets/store-DgG0C1Qx.js +1 -0
- package/dist/assets/streamParse-DRgMHWye.js +2 -0
- package/dist/assets/student.generated-DS_acPaJ.js +1 -0
- package/dist/assets/swift-DonLKvLd.js +1 -0
- package/dist/assets/tables-Dzd3rTqG.js +1 -0
- package/dist/assets/time-BHTmJR1s.js +1 -0
- package/dist/assets/toast-BBNs2h3L.js +1 -0
- package/dist/assets/toml-CcmNWLt0.js +1 -0
- package/dist/assets/traveler.generated-Beun9X7y.js +1 -0
- package/dist/assets/tsx-5f3ulCja.js +1 -0
- package/dist/assets/tsx-BOvsFOI6.js +1 -0
- package/dist/assets/typeof-B5XbjTb1.js +1 -0
- package/dist/assets/typescript-Utq2Cl8c.js +1 -0
- package/dist/assets/typescript-j_1H8WHN.js +1 -0
- package/dist/assets/useDashboardLoop-B6ByHWoE.js +5 -0
- package/dist/assets/useFocusTrap-DbfG6zbM.js +1 -0
- package/dist/assets/useLiveConfig-X9Wxh90k.js +1 -0
- package/dist/assets/useSpatialCanvas-DogK9457.js +1 -0
- package/dist/assets/useSpotlightWalk-rMbmNBKK.js +1 -0
- package/dist/assets/useSrsCards-CagZefwX.js +1 -0
- package/dist/assets/useStudy-Bflreq2a.js +1 -0
- package/dist/assets/useTimeout-BRYvu2jQ.js +1 -0
- package/dist/assets/useValidatedImage-D2fhA198.js +1 -0
- package/dist/assets/useZoomGesture-CYc3tf2z.js +1 -0
- package/dist/assets/verbatim-CBMzUDlf.js +1 -0
- package/dist/assets/worker-BbLKfGzm.js +2 -0
- package/dist/assets/xml-WY0zc4PG.js +1 -0
- package/dist/assets/yaml-CxbubiML.js +1 -0
- package/dist/assets/yaml-DijMHRDA.js +1 -0
- package/dist/demo-assets/CREDITS.md +40 -0
- package/dist/demo-assets/images/basalt-cliffs.jpg +0 -0
- package/dist/demo-assets/images/capelinhos.jpg +0 -0
- package/dist/demo-assets/images/cliff-fields.jpg +0 -0
- package/dist/demo-assets/images/crater-panorama.jpg +0 -0
- package/dist/demo-assets/images/furnas-lake.jpg +0 -0
- package/dist/demo-assets/images/green-coast.jpg +0 -0
- package/dist/demo-assets/images/overcast-coast.jpg +0 -0
- package/dist/demo-assets/images/patchwork-fields.jpg +0 -0
- package/dist/demo-assets/images/ponta-delgada.jpg +0 -0
- package/dist/demo-assets/images/sete-cidades.jpg +0 -0
- package/dist/demo-assets/images/slide-placeholder-light.svg +9 -0
- package/dist/demo-assets/images/slide-placeholder.svg +9 -0
- package/dist/demo-assets/images/village-street.jpg +0 -0
- package/dist/demo-assets/pdf/cfd-primer.pdf +0 -0
- package/dist/demo-assets/video/azores-film.mp4 +0 -0
- package/dist/favicon.svg +19 -0
- package/dist/fonts/LICENSE.txt +34 -0
- package/dist/fonts/archivo-var-latin.woff2 +0 -0
- package/dist/fonts/bodoni-moda-400-italic-latin.woff2 +0 -0
- package/dist/fonts/bodoni-moda-var-latin.woff2 +0 -0
- package/dist/fonts/eb-garamond-var-italic-latin.woff2 +0 -0
- package/dist/fonts/eb-garamond-var-latin.woff2 +0 -0
- package/dist/fonts/export/families/archivo.css +12 -0
- package/dist/fonts/export/families/bodoni-moda.css +23 -0
- package/dist/fonts/export/families/eb-garamond.css +22 -0
- package/dist/fonts/export/families/ibm-plex-sans.css +12 -0
- package/dist/fonts/export/families/instrument-serif.css +22 -0
- package/dist/fonts/export/families/jetbrains-mono.css +12 -0
- package/dist/fonts/export/families/libre-franklin.css +12 -0
- package/dist/fonts/export/families/lora.css +22 -0
- package/dist/fonts/export/families/source-serif-4.css +12 -0
- package/dist/fonts/export/families/space-grotesk.css +12 -0
- package/dist/fonts/export/families/spectral.css +52 -0
- package/dist/fonts/fonts.css +141 -0
- package/dist/fonts/hanken-grotesk-var-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-var-latin.woff2 +0 -0
- package/dist/fonts/instrument-serif-400-italic-latin.woff2 +0 -0
- package/dist/fonts/instrument-serif-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-var-latin.woff2 +0 -0
- package/dist/fonts/libre-franklin-var-latin.woff2 +0 -0
- package/dist/fonts/lora-var-italic-latin.woff2 +0 -0
- package/dist/fonts/lora-var-latin.woff2 +0 -0
- package/dist/fonts/newsreader-var-italic-latin.woff2 +0 -0
- package/dist/fonts/newsreader-var-latin.woff2 +0 -0
- package/dist/fonts/plex-mono-400-latin.woff2 +0 -0
- package/dist/fonts/plex-mono-500-latin.woff2 +0 -0
- package/dist/fonts/plex-mono-600-latin.woff2 +0 -0
- package/dist/fonts/source-serif-4-var-latin.woff2 +0 -0
- package/dist/fonts/space-grotesk-var-latin.woff2 +0 -0
- package/dist/fonts/spectral-400-italic-latin.woff2 +0 -0
- package/dist/fonts/spectral-400-latin.woff2 +0 -0
- package/dist/fonts/spectral-500-italic-latin.woff2 +0 -0
- package/dist/fonts/spectral-500-latin.woff2 +0 -0
- package/dist/fonts/spectral-600-latin.woff2 +0 -0
- package/dist/index.html +117 -0
- package/dist/legal/DISCLAIMER.md +16 -0
- package/dist/legal/LICENSE.txt +75 -0
- package/dist/legal/PRIVACY.md +127 -0
- package/dist/legal/SECURITY.md +72 -0
- package/dist/legal/SUPPORT.md +13 -0
- package/dist/legal/TERMS.md +122 -0
- package/dist/legal/THIRD-PARTY.txt +639 -0
- package/dist/legal/TRADEMARKS.md +15 -0
- package/dist/ort-wasm-simd-threaded.mjs +59 -0
- package/dist/sw.js +30 -0
- package/dist/vad.worklet.bundle.min.js +1 -0
- package/docker-compose.yml +33 -0
- package/gateway/README.md +99 -0
- package/gateway/actions-gateway.mjs +537 -0
- package/gateway/connectors.d.mts +27 -0
- package/gateway/connectors.mjs +463 -0
- package/gateway/guard.mjs +110 -0
- package/gateway/tokenStore.mjs +134 -0
- package/package.json +143 -0
package/bin/mavea.mjs
ADDED
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mavea CLI — serves the prebuilt app locally, same-origin proxies included, so `npx mavea`
|
|
3
|
+
// works exactly the way `pnpm dev` does in the source repo (see vite.config.ts, which this
|
|
4
|
+
// mirrors) without needing Node deps, a clone, or a build step.
|
|
5
|
+
import { createServer } from 'node:http';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import https from 'node:https';
|
|
8
|
+
import { createReadStream, existsSync, statSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { writeFile } from 'node:fs/promises';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { dirname, join, extname, normalize, resolve, sep } from 'node:path';
|
|
12
|
+
import { spawn, execSync } from 'node:child_process';
|
|
13
|
+
import { constants, createBrotliCompress, createGzip } from 'node:zlib';
|
|
14
|
+
import { homedir, platform, tmpdir } from 'node:os';
|
|
15
|
+
import readline from 'node:readline';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const ROOT = join(__dirname, '..');
|
|
19
|
+
const DIST = join(ROOT, 'dist');
|
|
20
|
+
const COMPOSE_FILE = join(ROOT, 'docker-compose.yml');
|
|
21
|
+
|
|
22
|
+
// The CLI is intentionally a local application, not a remotely reachable deployment server.
|
|
23
|
+
// Binding an API-key-forwarding proxy to 0.0.0.0 turns every laptop on a shared network into an
|
|
24
|
+
// unauthenticated relay. A hosted Mavéa deployment must put its own authenticated HTTPS edge in
|
|
25
|
+
// front of the app; this executable always stays on the IPv4 loopback interface.
|
|
26
|
+
export const LOOPBACK_HOST = '127.0.0.1';
|
|
27
|
+
|
|
28
|
+
const MAX_REQUEST_TARGET_BYTES = 8 * 1024;
|
|
29
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
30
|
+
'connection',
|
|
31
|
+
'keep-alive',
|
|
32
|
+
'proxy-authenticate',
|
|
33
|
+
'proxy-authorization',
|
|
34
|
+
'te',
|
|
35
|
+
'trailer',
|
|
36
|
+
'transfer-encoding',
|
|
37
|
+
'upgrade',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// HSTS is deliberately absent: this server is plain HTTP on loopback. Hosted HTTPS deployments
|
|
41
|
+
// receive HSTS from public/_headers. The remaining protections are valid on both HTTP and HTTPS.
|
|
42
|
+
export const LOCAL_SECURITY_HEADERS = Object.freeze({
|
|
43
|
+
'Content-Security-Policy': "frame-ancestors 'none'",
|
|
44
|
+
'Permissions-Policy': 'microphone=(self), display-capture=(self), camera=(), geolocation=()',
|
|
45
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
46
|
+
'X-Content-Type-Options': 'nosniff',
|
|
47
|
+
'X-Frame-Options': 'DENY',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// The ONNX runtime WASM (13MB) and Silero VAD model (2.3MB) are real functional assets voice
|
|
51
|
+
// mode needs, not waste — but bundling them into every `npx mavea` download costs everyone that
|
|
52
|
+
// weight even if they never touch voice (most turns are text). Both are ALSO already public,
|
|
53
|
+
// permanently-versioned npm package assets, so instead of shipping them in dist/ (see the `files`
|
|
54
|
+
// exclusion in package.json), they're fetched ONCE from jsDelivr's npm CDN the first time voice
|
|
55
|
+
// actually starts, cached to a persistent per-OS cache dir, and served from there on every request
|
|
56
|
+
// after — a normal user who never uses voice never downloads either file. Pinned to the exact
|
|
57
|
+
// version this package was built against (see devDependencies) so a version bump here and a
|
|
58
|
+
// version bump there can never silently drift apart.
|
|
59
|
+
const LAZY_ASSETS = {
|
|
60
|
+
'ort-wasm-simd-threaded.wasm': {
|
|
61
|
+
url: 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.26.0/dist/ort-wasm-simd-threaded.wasm',
|
|
62
|
+
mime: 'application/wasm',
|
|
63
|
+
},
|
|
64
|
+
'silero_vad_v5.onnx': {
|
|
65
|
+
url: 'https://cdn.jsdelivr.net/npm/@ricky0123/vad-web@0.0.30/dist/silero_vad_v5.onnx',
|
|
66
|
+
mime: 'application/octet-stream',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Standard per-OS cache location (the same convention env-paths/XDG use) — persists across
|
|
71
|
+
// `npx mavea` invocations (npx's own package cache does NOT persist these, since they're fetched
|
|
72
|
+
// at runtime, not installed), so a user only pays the download once, ever, per machine.
|
|
73
|
+
function lazyCacheDir() {
|
|
74
|
+
const plat = platform();
|
|
75
|
+
if (plat === 'darwin') return join(homedir(), 'Library', 'Caches', 'mavea');
|
|
76
|
+
if (plat === 'win32') {
|
|
77
|
+
return join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'mavea', 'Cache');
|
|
78
|
+
}
|
|
79
|
+
return join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'mavea');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// In-flight downloads by cache path, so two requests racing for the same missing asset (e.g. the
|
|
83
|
+
// .wasm and its already-loaded .mjs glue both kicking off near-simultaneously) share one fetch
|
|
84
|
+
// instead of downloading twice.
|
|
85
|
+
const inFlightDownloads = new Map();
|
|
86
|
+
|
|
87
|
+
async function fetchToCache(url, cachePath) {
|
|
88
|
+
if (existsSync(cachePath)) return;
|
|
89
|
+
const existing = inFlightDownloads.get(cachePath);
|
|
90
|
+
if (existing) return existing;
|
|
91
|
+
const task = (async () => {
|
|
92
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
93
|
+
const ctrl = new AbortController();
|
|
94
|
+
const timer = setTimeout(() => ctrl.abort(), 30_000);
|
|
95
|
+
// Download to a per-process tmp file, then rename — a request that arrives mid-download (or a
|
|
96
|
+
// process killed mid-download) never sees or leaves behind a truncated cache file.
|
|
97
|
+
const tmpPath = join(tmpdir(), `mavea-dl-${process.pid}-${Date.now()}.tmp`);
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
100
|
+
if (!res.ok) throw new Error(`fetch ${url} -> ${res.status}`);
|
|
101
|
+
await writeFile(tmpPath, Buffer.from(await res.arrayBuffer()));
|
|
102
|
+
renameSync(tmpPath, cachePath);
|
|
103
|
+
} finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
if (existsSync(tmpPath)) unlinkSync(tmpPath);
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
inFlightDownloads.set(cachePath, task);
|
|
109
|
+
try {
|
|
110
|
+
await task;
|
|
111
|
+
} finally {
|
|
112
|
+
inFlightDownloads.delete(cachePath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function serveLazyAsset(name, asset, res) {
|
|
117
|
+
const cachePath = join(lazyCacheDir(), name);
|
|
118
|
+
try {
|
|
119
|
+
await fetchToCache(asset.url, cachePath);
|
|
120
|
+
} catch {
|
|
121
|
+
// Same failure mode as a missing asset always had here: the client (onnxruntime-web / Silero
|
|
122
|
+
// VAD) treats a 404/502 on this path as "voice model unavailable" and falls back gracefully
|
|
123
|
+
// (WebSpeech) rather than breaking the turn — offline or a CDN hiccup degrades, never crashes.
|
|
124
|
+
sendProblem(res, 502, 'The optional voice asset is unavailable.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
res.writeHead(200, {
|
|
128
|
+
...LOCAL_SECURITY_HEADERS,
|
|
129
|
+
'Content-Type': asset.mime,
|
|
130
|
+
'Cache-Control': 'no-cache',
|
|
131
|
+
});
|
|
132
|
+
const stream = createReadStream(cachePath);
|
|
133
|
+
stream.on('error', () => res.destroy());
|
|
134
|
+
stream.pipe(res);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function parseArgs(argv, env = process.env) {
|
|
138
|
+
const envPort = env.PORT == null || env.PORT === '' ? 4173 : Number(env.PORT);
|
|
139
|
+
const out = { port: envPort, open: true, voice: true };
|
|
140
|
+
for (let i = 0; i < argv.length; i++) {
|
|
141
|
+
const a = argv[i];
|
|
142
|
+
if (a === '--port' || a === '-p') {
|
|
143
|
+
const value = argv[++i];
|
|
144
|
+
if (value == null) throw new Error(`${a} requires a port number`);
|
|
145
|
+
out.port = Number(value);
|
|
146
|
+
} else if (a === '--no-open') out.open = false;
|
|
147
|
+
else if (a === '--no-voice') out.voice = false;
|
|
148
|
+
else if (a === '--help' || a === '-h') out.help = true;
|
|
149
|
+
else throw new Error(`unknown option: ${a}`);
|
|
150
|
+
}
|
|
151
|
+
if (!Number.isInteger(out.port) || out.port < 0 || out.port > 65_535) {
|
|
152
|
+
throw new Error(`invalid port: ${String(env.PORT ?? out.port)}`);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Same-origin forwarders — the production equivalent of the /llm, /search, /actions, /tts, /stt
|
|
158
|
+
// dev proxies in vite.config.ts. BYOK keys travel browser → this local server → provider, same
|
|
159
|
+
// as dev; nothing is stored or logged here.
|
|
160
|
+
const MiB = 1024 * 1024;
|
|
161
|
+
|
|
162
|
+
export const PROXIES = [
|
|
163
|
+
{
|
|
164
|
+
prefix: '/tts',
|
|
165
|
+
target: process.env.KOKORO_URL || 'http://localhost:8880',
|
|
166
|
+
methods: ['GET', 'POST'],
|
|
167
|
+
bodyLimit: 2 * MiB,
|
|
168
|
+
responseLimit: 32 * MiB,
|
|
169
|
+
requestsPerMinute: 120,
|
|
170
|
+
maxConcurrent: 4,
|
|
171
|
+
timeoutMs: 120_000,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
prefix: '/stt',
|
|
175
|
+
target: process.env.WHISPER_URL || 'http://localhost:8100',
|
|
176
|
+
methods: ['GET', 'POST'],
|
|
177
|
+
bodyLimit: 32 * MiB,
|
|
178
|
+
responseLimit: 4 * MiB,
|
|
179
|
+
requestsPerMinute: 60,
|
|
180
|
+
maxConcurrent: 2,
|
|
181
|
+
timeoutMs: 120_000,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
prefix: '/llm/anthropic',
|
|
185
|
+
target: 'https://api.anthropic.com',
|
|
186
|
+
// GET too: every provider adapter's readiness probe (the wizard/settings "Test" check) hits
|
|
187
|
+
// GET .../v1/models before ever sending a POST — restricting this to POST 405'd that probe,
|
|
188
|
+
// which surfaced as a permanent, key-independent "Not reachable." on this local server.
|
|
189
|
+
methods: ['GET', 'POST'],
|
|
190
|
+
bodyLimit: 96 * MiB,
|
|
191
|
+
responseLimit: 32 * MiB,
|
|
192
|
+
requestsPerMinute: 60,
|
|
193
|
+
maxConcurrent: 2,
|
|
194
|
+
timeoutMs: 120_000,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
prefix: '/llm/openai',
|
|
198
|
+
target: 'https://api.openai.com',
|
|
199
|
+
methods: ['GET', 'POST'], // GET: the readiness probe (see the anthropic route's comment)
|
|
200
|
+
bodyLimit: 96 * MiB,
|
|
201
|
+
responseLimit: 32 * MiB,
|
|
202
|
+
requestsPerMinute: 60,
|
|
203
|
+
maxConcurrent: 2,
|
|
204
|
+
timeoutMs: 120_000,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
prefix: '/llm/gemini',
|
|
208
|
+
target: 'https://generativelanguage.googleapis.com',
|
|
209
|
+
methods: ['GET', 'POST'], // GET: the readiness probe (see the anthropic route's comment)
|
|
210
|
+
bodyLimit: 96 * MiB,
|
|
211
|
+
responseLimit: 32 * MiB,
|
|
212
|
+
requestsPerMinute: 60,
|
|
213
|
+
maxConcurrent: 2,
|
|
214
|
+
timeoutMs: 120_000,
|
|
215
|
+
injectGeminiKey: true,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
prefix: '/llm/grok',
|
|
219
|
+
target: 'https://api.x.ai',
|
|
220
|
+
methods: ['GET', 'POST'], // GET: the readiness probe (see the anthropic route's comment)
|
|
221
|
+
bodyLimit: 96 * MiB,
|
|
222
|
+
responseLimit: 32 * MiB,
|
|
223
|
+
requestsPerMinute: 60,
|
|
224
|
+
maxConcurrent: 2,
|
|
225
|
+
timeoutMs: 120_000,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
prefix: '/llm/openrouter',
|
|
229
|
+
target: 'https://openrouter.ai',
|
|
230
|
+
methods: ['GET', 'POST'], // GET: the readiness probe (see the anthropic route's comment)
|
|
231
|
+
bodyLimit: 96 * MiB,
|
|
232
|
+
responseLimit: 32 * MiB,
|
|
233
|
+
requestsPerMinute: 60,
|
|
234
|
+
maxConcurrent: 2,
|
|
235
|
+
timeoutMs: 120_000,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
prefix: '/search/brave',
|
|
239
|
+
target: 'https://api.search.brave.com',
|
|
240
|
+
methods: ['GET'],
|
|
241
|
+
bodyLimit: 0,
|
|
242
|
+
responseLimit: 4 * MiB,
|
|
243
|
+
requestsPerMinute: 120,
|
|
244
|
+
maxConcurrent: 6,
|
|
245
|
+
timeoutMs: 30_000,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
prefix: '/search/tavily',
|
|
249
|
+
target: 'https://api.tavily.com',
|
|
250
|
+
methods: ['POST'],
|
|
251
|
+
bodyLimit: 2 * MiB,
|
|
252
|
+
responseLimit: 4 * MiB,
|
|
253
|
+
requestsPerMinute: 120,
|
|
254
|
+
maxConcurrent: 6,
|
|
255
|
+
timeoutMs: 30_000,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
prefix: '/actions',
|
|
259
|
+
target: process.env.ACTIONS_URL || 'http://127.0.0.1:8910',
|
|
260
|
+
methods: ['GET', 'POST', 'DELETE'],
|
|
261
|
+
bodyLimit: 2 * MiB,
|
|
262
|
+
responseLimit: 8 * MiB,
|
|
263
|
+
requestsPerMinute: 120,
|
|
264
|
+
maxConcurrent: 4,
|
|
265
|
+
timeoutMs: 30_000,
|
|
266
|
+
forwardOrigin: true,
|
|
267
|
+
crossSiteNavigationPaths: ['/oauth/google/callback'],
|
|
268
|
+
injectGatewaySecret: true,
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
function send(res, status, body = '', headers = {}) {
|
|
273
|
+
if (res.headersSent) return res.end();
|
|
274
|
+
const payload = body ? Buffer.from(body) : null;
|
|
275
|
+
res.writeHead(status, {
|
|
276
|
+
...LOCAL_SECURITY_HEADERS,
|
|
277
|
+
'Cache-Control': 'no-store',
|
|
278
|
+
...(payload ? { 'Content-Length': String(payload.byteLength) } : {}),
|
|
279
|
+
...headers,
|
|
280
|
+
});
|
|
281
|
+
res.end(payload);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function sendProblem(res, status, message, headers = {}) {
|
|
285
|
+
send(res, status, `${message}\n`, {
|
|
286
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
287
|
+
...headers,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseRequestTarget(rawUrl) {
|
|
292
|
+
if (!rawUrl || Buffer.byteLength(rawUrl) > MAX_REQUEST_TARGET_BYTES) {
|
|
293
|
+
throw new Error('request target is empty or too long');
|
|
294
|
+
}
|
|
295
|
+
// Only origin-form request targets belong on this local origin. Rejecting absolute-form targets
|
|
296
|
+
// prevents the CLI from accidentally behaving like a general-purpose forward proxy.
|
|
297
|
+
if (!rawUrl.startsWith('/')) throw new Error('absolute request targets are not supported');
|
|
298
|
+
const parsed = new URL(rawUrl, 'http://127.0.0.1');
|
|
299
|
+
const decodedPathname = decodeURIComponent(parsed.pathname);
|
|
300
|
+
if (decodedPathname.includes('\0')) throw new Error('request target contains a null byte');
|
|
301
|
+
return {
|
|
302
|
+
pathname: parsed.pathname,
|
|
303
|
+
decodedPathname,
|
|
304
|
+
search: parsed.search,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function routeMatches(pathname, prefix) {
|
|
309
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function localUrlFromHeader(value) {
|
|
313
|
+
if (!value || Array.isArray(value)) return null;
|
|
314
|
+
try {
|
|
315
|
+
const parsed = new URL(value.includes('://') ? value : `http://${value}`);
|
|
316
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
317
|
+
if (!['127.0.0.1', 'localhost', '[::1]', '::1'].includes(hostname)) return null;
|
|
318
|
+
return parsed;
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isTrustedBrowserRequest(req, requestTarget, route) {
|
|
325
|
+
const host = localUrlFromHeader(req.headers.host);
|
|
326
|
+
if (!host) return false;
|
|
327
|
+
|
|
328
|
+
const crossSitePath = requestTarget.pathname.slice(route.prefix.length) || '/';
|
|
329
|
+
if (
|
|
330
|
+
req.method === 'GET' &&
|
|
331
|
+
route.crossSiteNavigationPaths?.some((path) => crossSitePath === path)
|
|
332
|
+
) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const origin = localUrlFromHeader(req.headers.origin);
|
|
337
|
+
if (origin) return origin.host === host.host;
|
|
338
|
+
|
|
339
|
+
const referer = localUrlFromHeader(req.headers.referer);
|
|
340
|
+
if (referer) return referer.host === host.host;
|
|
341
|
+
|
|
342
|
+
// Fetch Metadata is supplied by current browsers even for same-origin GETs that omit Origin.
|
|
343
|
+
// A raw socket/curl request supplies none of these browser proofs and cannot consume the proxy.
|
|
344
|
+
return req.headers['sec-fetch-site'] === 'same-origin';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function createRateLimiter(now = Date.now) {
|
|
348
|
+
const windows = new Map();
|
|
349
|
+
let calls = 0;
|
|
350
|
+
return {
|
|
351
|
+
take(key, limit) {
|
|
352
|
+
const current = now();
|
|
353
|
+
const cutoff = current - 60_000;
|
|
354
|
+
const recent = (windows.get(key) || []).filter((time) => time > cutoff);
|
|
355
|
+
if (recent.length >= limit) {
|
|
356
|
+
return {
|
|
357
|
+
allowed: false,
|
|
358
|
+
retryAfter: Math.max(1, Math.ceil((recent[0] + 60_000 - current) / 1000)),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
recent.push(current);
|
|
362
|
+
windows.set(key, recent);
|
|
363
|
+
// Keep a long-running CLI bounded even if many transient client keys appear.
|
|
364
|
+
if (++calls % 256 === 0) {
|
|
365
|
+
for (const [client, hits] of windows) {
|
|
366
|
+
if (!hits.some((time) => time > cutoff)) windows.delete(client);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return { allowed: true, retryAfter: 0 };
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function readRequestBody(req, limit) {
|
|
375
|
+
const declared = Number(req.headers['content-length']);
|
|
376
|
+
if (Number.isFinite(declared) && declared > limit) {
|
|
377
|
+
req.resume();
|
|
378
|
+
return Promise.reject(Object.assign(new Error('request body too large'), { statusCode: 413 }));
|
|
379
|
+
}
|
|
380
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
381
|
+
const chunks = [];
|
|
382
|
+
let received = 0;
|
|
383
|
+
let settled = false;
|
|
384
|
+
const reject = (error) => {
|
|
385
|
+
if (settled) return;
|
|
386
|
+
settled = true;
|
|
387
|
+
rejectBody(error);
|
|
388
|
+
};
|
|
389
|
+
req.on('data', (chunk) => {
|
|
390
|
+
received += chunk.length;
|
|
391
|
+
if (received > limit) {
|
|
392
|
+
reject(Object.assign(new Error('request body too large'), { statusCode: 413 }));
|
|
393
|
+
req.resume();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
chunks.push(chunk);
|
|
397
|
+
});
|
|
398
|
+
req.on('end', () => {
|
|
399
|
+
if (settled) return;
|
|
400
|
+
settled = true;
|
|
401
|
+
resolveBody(Buffer.concat(chunks, received));
|
|
402
|
+
});
|
|
403
|
+
req.on('aborted', () =>
|
|
404
|
+
reject(Object.assign(new Error('request aborted'), { statusCode: 400 })),
|
|
405
|
+
);
|
|
406
|
+
req.on('error', reject);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function requestHeadersForUpstream(req, route, targetUrl, body) {
|
|
411
|
+
const headers = {};
|
|
412
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
413
|
+
const lower = name.toLowerCase();
|
|
414
|
+
if (
|
|
415
|
+
HOP_BY_HOP_HEADERS.has(lower) ||
|
|
416
|
+
lower === 'host' ||
|
|
417
|
+
lower === 'cookie' ||
|
|
418
|
+
lower === 'content-length' ||
|
|
419
|
+
(!route.forwardOrigin && (lower === 'origin' || lower === 'referer'))
|
|
420
|
+
) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (value != null) headers[lower] = value;
|
|
424
|
+
}
|
|
425
|
+
headers.host = targetUrl.host;
|
|
426
|
+
if (body.byteLength > 0) headers['content-length'] = String(body.byteLength);
|
|
427
|
+
if (route.injectGeminiKey && process.env.GEMINI_API_KEY && !headers['x-goog-api-key']) {
|
|
428
|
+
headers['x-goog-api-key'] = process.env.GEMINI_API_KEY;
|
|
429
|
+
}
|
|
430
|
+
if (route.injectGatewaySecret && process.env.GATEWAY_SECRET) {
|
|
431
|
+
headers['x-gateway-secret'] = process.env.GATEWAY_SECRET;
|
|
432
|
+
}
|
|
433
|
+
return headers;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function responseHeadersForClient(proxyHeaders) {
|
|
437
|
+
const headers = { ...LOCAL_SECURITY_HEADERS, 'Cache-Control': 'no-store' };
|
|
438
|
+
for (const [name, value] of Object.entries(proxyHeaders)) {
|
|
439
|
+
const lower = name.toLowerCase();
|
|
440
|
+
if (
|
|
441
|
+
HOP_BY_HOP_HEADERS.has(lower) ||
|
|
442
|
+
lower === 'set-cookie' ||
|
|
443
|
+
lower.startsWith('access-control-') ||
|
|
444
|
+
lower === 'content-length'
|
|
445
|
+
) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (value != null) headers[lower] = value;
|
|
449
|
+
}
|
|
450
|
+
return headers;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function streamBoundedResponse(proxyRes, res, limit, onDone) {
|
|
454
|
+
const declared = Number(proxyRes.headers['content-length']);
|
|
455
|
+
if (Number.isFinite(declared) && declared > limit) {
|
|
456
|
+
proxyRes.destroy();
|
|
457
|
+
sendProblem(res, 502, 'Upstream response exceeded the configured limit.');
|
|
458
|
+
onDone();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
res.writeHead(proxyRes.statusCode || 502, responseHeadersForClient(proxyRes.headers));
|
|
463
|
+
let received = 0;
|
|
464
|
+
proxyRes.on('data', (chunk) => {
|
|
465
|
+
received += chunk.length;
|
|
466
|
+
if (received > limit) {
|
|
467
|
+
proxyRes.destroy();
|
|
468
|
+
res.destroy(new Error('upstream response exceeded the configured limit'));
|
|
469
|
+
onDone();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (!res.write(chunk)) {
|
|
473
|
+
proxyRes.pause();
|
|
474
|
+
res.once('drain', () => proxyRes.resume());
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
proxyRes.on('end', () => {
|
|
478
|
+
res.end();
|
|
479
|
+
onDone();
|
|
480
|
+
});
|
|
481
|
+
proxyRes.on('aborted', () => {
|
|
482
|
+
res.destroy();
|
|
483
|
+
onDone();
|
|
484
|
+
});
|
|
485
|
+
proxyRes.on('error', () => {
|
|
486
|
+
if (!res.headersSent) sendProblem(res, 502, 'The upstream service failed.');
|
|
487
|
+
else res.destroy();
|
|
488
|
+
onDone();
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function proxyRequest(req, res, route, requestTarget) {
|
|
493
|
+
let body;
|
|
494
|
+
try {
|
|
495
|
+
body = await readRequestBody(req, route.bodyLimit);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
sendProblem(res, error.statusCode || 400, error.message || 'Invalid request body.');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const targetUrl = new URL(route.target);
|
|
502
|
+
if (targetUrl.protocol !== 'http:' && targetUrl.protocol !== 'https:') {
|
|
503
|
+
sendProblem(res, 502, 'The upstream service has an invalid protocol.');
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const restPath = requestTarget.pathname.slice(route.prefix.length) || '/';
|
|
507
|
+
const rest = restPath + requestTarget.search;
|
|
508
|
+
const isHttps = targetUrl.protocol === 'https:';
|
|
509
|
+
const mod = isHttps ? https : http;
|
|
510
|
+
const headers = requestHeadersForUpstream(req, route, targetUrl, body);
|
|
511
|
+
|
|
512
|
+
await new Promise((resolveProxy) => {
|
|
513
|
+
let settled = false;
|
|
514
|
+
const settle = () => {
|
|
515
|
+
if (settled) return;
|
|
516
|
+
settled = true;
|
|
517
|
+
res.off('close', onClientClose);
|
|
518
|
+
resolveProxy();
|
|
519
|
+
};
|
|
520
|
+
const proxyReq = mod.request(
|
|
521
|
+
{
|
|
522
|
+
protocol: targetUrl.protocol,
|
|
523
|
+
hostname: targetUrl.hostname,
|
|
524
|
+
port: targetUrl.port || (isHttps ? 443 : 80),
|
|
525
|
+
path: (targetUrl.pathname === '/' ? '' : targetUrl.pathname) + rest,
|
|
526
|
+
method: req.method,
|
|
527
|
+
headers,
|
|
528
|
+
},
|
|
529
|
+
(proxyRes) => {
|
|
530
|
+
proxyRes.setTimeout(route.timeoutMs, () => {
|
|
531
|
+
proxyRes.destroy(new Error('upstream response timeout'));
|
|
532
|
+
});
|
|
533
|
+
streamBoundedResponse(proxyRes, res, route.responseLimit, settle);
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
const onClientClose = () => {
|
|
537
|
+
if (!res.writableEnded) proxyReq.destroy(new Error('client disconnected'));
|
|
538
|
+
settle();
|
|
539
|
+
};
|
|
540
|
+
res.once('close', onClientClose);
|
|
541
|
+
proxyReq.setTimeout(route.timeoutMs, () => {
|
|
542
|
+
proxyReq.destroy(new Error('upstream request timeout'));
|
|
543
|
+
});
|
|
544
|
+
proxyReq.on('error', () => {
|
|
545
|
+
// Connection refused (voice/gateway not running) surfaces as an honest failure on the card.
|
|
546
|
+
if (!res.destroyed && !res.headersSent) {
|
|
547
|
+
sendProblem(res, 502, 'The upstream service is unavailable.');
|
|
548
|
+
} else if (!res.destroyed) {
|
|
549
|
+
res.destroy();
|
|
550
|
+
}
|
|
551
|
+
settle();
|
|
552
|
+
});
|
|
553
|
+
if (body.byteLength > 0) proxyReq.write(body);
|
|
554
|
+
proxyReq.end();
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const MIME = {
|
|
559
|
+
'.html': 'text/html',
|
|
560
|
+
'.js': 'text/javascript',
|
|
561
|
+
'.mjs': 'text/javascript',
|
|
562
|
+
'.css': 'text/css',
|
|
563
|
+
'.json': 'application/json',
|
|
564
|
+
'.svg': 'image/svg+xml',
|
|
565
|
+
'.png': 'image/png',
|
|
566
|
+
'.jpg': 'image/jpeg',
|
|
567
|
+
'.jpeg': 'image/jpeg',
|
|
568
|
+
'.webp': 'image/webp',
|
|
569
|
+
'.pdf': 'application/pdf',
|
|
570
|
+
'.woff2': 'font/woff2',
|
|
571
|
+
'.wasm': 'application/wasm',
|
|
572
|
+
'.onnx': 'application/octet-stream',
|
|
573
|
+
'.ico': 'image/x-icon',
|
|
574
|
+
'.webmanifest': 'application/manifest+json',
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Text assets are stored uncompressed, so serving them verbatim spends roughly three bytes for
|
|
578
|
+
// every one a browser needs: the app shell alone is 396 kB raw against 129 kB gzipped. Anything
|
|
579
|
+
// already carrying its own compression (fonts, images, PDFs, media) is left alone — re-compressing
|
|
580
|
+
// it costs CPU and returns nothing. `.wasm` is here because it is plain bytecode and halves.
|
|
581
|
+
const COMPRESSIBLE = new Set([
|
|
582
|
+
'.css',
|
|
583
|
+
'.html',
|
|
584
|
+
'.js',
|
|
585
|
+
'.json',
|
|
586
|
+
'.map',
|
|
587
|
+
'.mjs',
|
|
588
|
+
'.svg',
|
|
589
|
+
'.txt',
|
|
590
|
+
'.wasm',
|
|
591
|
+
'.xml',
|
|
592
|
+
]);
|
|
593
|
+
|
|
594
|
+
// Quality 5 rather than brotli's default 11: on a local server the file is compressed on every
|
|
595
|
+
// request, and 11 spends seconds of CPU to save a few percent over 5 — a trade that makes the
|
|
596
|
+
// first paint slower, not faster. Encoding the size hint lets brotli size its window to the file.
|
|
597
|
+
const BROTLI_OPTIONS = {
|
|
598
|
+
params: {
|
|
599
|
+
[constants.BROTLI_PARAM_QUALITY]: 5,
|
|
600
|
+
[constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT,
|
|
601
|
+
},
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
/** The best encoding this client accepts for this file, or null to send it verbatim. Brotli beats
|
|
605
|
+
* gzip by ~15% on our bundles and every browser that can run the app supports it, so it wins when
|
|
606
|
+
* offered; gzip is the floor for anything older or a proxy that stripped br. */
|
|
607
|
+
export function negotiateEncoding(acceptEncoding, ext) {
|
|
608
|
+
if (!COMPRESSIBLE.has(ext.toLowerCase())) return null;
|
|
609
|
+
const accepted = String(acceptEncoding ?? '').toLowerCase();
|
|
610
|
+
if (/\bbr\b/.test(accepted)) return 'br';
|
|
611
|
+
if (/\bgzip\b/.test(accepted)) return 'gzip';
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Content-hashed assets are immutable by definition — the same URL can never return different
|
|
616
|
+
// bytes — so compressing one twice is pure waste, and on the small machines this exists to serve,
|
|
617
|
+
// brotli-ing a 400 kB bundle on every page load is CPU taken from the render. Compress once, hold
|
|
618
|
+
// the result, and every later request is a buffer write with a real Content-Length instead of a
|
|
619
|
+
// chunked stream. Only /assets/ is eligible: index.html is `no-cache` and may change under us.
|
|
620
|
+
const COMPRESSED_CACHE_BUDGET_BYTES = 32 * 1024 * 1024;
|
|
621
|
+
const compressedCache = new Map();
|
|
622
|
+
let compressedCacheBytes = 0;
|
|
623
|
+
|
|
624
|
+
function cacheCompressed(key, body) {
|
|
625
|
+
// A budget rather than an LRU: the set is the build's own asset list, which is bounded and small.
|
|
626
|
+
// The cap exists so a pathological dist/ cannot pin unbounded memory, not to manage churn.
|
|
627
|
+
if (compressedCacheBytes + body.length > COMPRESSED_CACHE_BUDGET_BYTES) return;
|
|
628
|
+
compressedCache.set(key, body);
|
|
629
|
+
compressedCacheBytes += body.length;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Compressed bytes for an immutable asset, or null if it is not worth holding one. */
|
|
633
|
+
function cachedCompression(filePath, encoding, isHashedAsset) {
|
|
634
|
+
if (!isHashedAsset) return null;
|
|
635
|
+
return compressedCache.get(`${encoding}${filePath}`) ?? null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function resetCompressionCacheForTest() {
|
|
639
|
+
compressedCache.clear();
|
|
640
|
+
compressedCacheBytes = 0;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function serveStatic(req, res, requestTarget, distDir = DIST) {
|
|
644
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
645
|
+
sendProblem(res, 405, 'Method not allowed.', { Allow: 'GET, HEAD' });
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const urlPath = requestTarget.decodedPathname;
|
|
650
|
+
const safePath = normalize(urlPath === '/' ? '/index.html' : urlPath);
|
|
651
|
+
let filePath = resolve(distDir, `.${safePath}`);
|
|
652
|
+
// Hash-routed app (#/live, #/gallery never hit the server) — no traversal outside dist/.
|
|
653
|
+
const distPrefix = distDir.endsWith(sep) ? distDir : `${distDir}${sep}`;
|
|
654
|
+
const inDist =
|
|
655
|
+
filePath.startsWith(distPrefix) && existsSync(filePath) && !statSync(filePath).isDirectory();
|
|
656
|
+
if (!inDist) {
|
|
657
|
+
// A published package omits these two large assets from dist/ (see package.json `files`) —
|
|
658
|
+
// fetch-and-cache them on first request instead of falling through to the SPA's index.html,
|
|
659
|
+
// which would hand onnxruntime-web an HTML page where it expects a .wasm/.onnx binary.
|
|
660
|
+
// A source checkout's own `pnpm build` still writes them straight into dist/, so this path
|
|
661
|
+
// never triggers there — `inDist` above is already true and this whole branch is skipped.
|
|
662
|
+
const asset = LAZY_ASSETS[safePath.replace(/^\//, '')];
|
|
663
|
+
if (asset) return void serveLazyAsset(safePath.replace(/^\//, ''), asset, res);
|
|
664
|
+
filePath = join(distDir, 'index.html');
|
|
665
|
+
}
|
|
666
|
+
if (!existsSync(filePath)) {
|
|
667
|
+
sendProblem(res, 404, 'Build entry point not found.');
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const isHtml = extname(filePath) === '.html';
|
|
671
|
+
const isPdf = extname(filePath).toLowerCase() === '.pdf';
|
|
672
|
+
const isHashedAsset = safePath.startsWith('/assets/');
|
|
673
|
+
const encoding = negotiateEncoding(req.headers['accept-encoding'], extname(filePath));
|
|
674
|
+
const cached = encoding ? cachedCompression(filePath, encoding, isHashedAsset) : null;
|
|
675
|
+
res.writeHead(200, {
|
|
676
|
+
...LOCAL_SECURITY_HEADERS,
|
|
677
|
+
...(isPdf
|
|
678
|
+
? {
|
|
679
|
+
// The app shell remains DENY/'none'. Only a PDF response may be framed, and then only
|
|
680
|
+
// by this same origin for Pdfreader's sandboxed iframe.
|
|
681
|
+
'Content-Security-Policy': "frame-ancestors 'self'",
|
|
682
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
683
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
684
|
+
'Content-Disposition': 'inline',
|
|
685
|
+
}
|
|
686
|
+
: {}),
|
|
687
|
+
'Cache-Control': isHtml
|
|
688
|
+
? 'no-cache'
|
|
689
|
+
: isHashedAsset
|
|
690
|
+
? 'public, max-age=31536000, immutable'
|
|
691
|
+
: 'no-cache',
|
|
692
|
+
'Content-Type': MIME[extname(filePath)] || 'application/octet-stream',
|
|
693
|
+
...(encoding ? { 'Content-Encoding': encoding, Vary: 'Accept-Encoding' } : {}),
|
|
694
|
+
// Known only for a cached body: a streamed compression cannot state its length up front, and
|
|
695
|
+
// guessing it from the uncompressed file would be a lie the client counts bytes against.
|
|
696
|
+
...(cached ? { 'Content-Length': String(cached.length) } : {}),
|
|
697
|
+
});
|
|
698
|
+
if (req.method === 'HEAD') return res.end();
|
|
699
|
+
if (cached) return void res.end(cached);
|
|
700
|
+
|
|
701
|
+
const stream = createReadStream(filePath);
|
|
702
|
+
stream.on('error', () => {
|
|
703
|
+
if (!res.headersSent) sendProblem(res, 500, 'Could not read the requested file.');
|
|
704
|
+
else res.destroy();
|
|
705
|
+
});
|
|
706
|
+
if (!encoding) return void stream.pipe(res);
|
|
707
|
+
|
|
708
|
+
const compressor = encoding === 'br' ? createBrotliCompress(BROTLI_OPTIONS) : createGzip();
|
|
709
|
+
compressor.on('error', () => res.destroy());
|
|
710
|
+
stream.pipe(compressor).pipe(res);
|
|
711
|
+
if (!isHashedAsset) return;
|
|
712
|
+
|
|
713
|
+
// Keep what the client is already being sent, so the next request for this immutable URL skips
|
|
714
|
+
// the compression entirely. Collecting alongside the pipe adds no latency — the response streams
|
|
715
|
+
// as it is produced either way — and 'end' lands once the last chunk has gone out.
|
|
716
|
+
const chunks = [];
|
|
717
|
+
compressor.on('data', (chunk) => chunks.push(chunk));
|
|
718
|
+
compressor.once('end', () => cacheCompressed(`${encoding} ${filePath}`, Buffer.concat(chunks)));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function createMaveaServer({ distDir = DIST, proxies = PROXIES, now = Date.now } = {}) {
|
|
722
|
+
const limiter = createRateLimiter(now);
|
|
723
|
+
const activeByRoute = new Map();
|
|
724
|
+
const server = createServer((req, res) => {
|
|
725
|
+
let requestTarget;
|
|
726
|
+
try {
|
|
727
|
+
requestTarget = parseRequestTarget(req.url);
|
|
728
|
+
} catch {
|
|
729
|
+
sendProblem(res, 400, 'Malformed request target.');
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const route = proxies.find((candidate) =>
|
|
734
|
+
routeMatches(requestTarget.pathname, candidate.prefix),
|
|
735
|
+
);
|
|
736
|
+
if (!route) {
|
|
737
|
+
try {
|
|
738
|
+
serveStatic(req, res, requestTarget, distDir);
|
|
739
|
+
} catch {
|
|
740
|
+
sendProblem(res, 500, 'The local server could not complete the request.');
|
|
741
|
+
}
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const allowedMethods = route.methods || [];
|
|
746
|
+
if (!allowedMethods.includes(req.method)) {
|
|
747
|
+
sendProblem(res, 405, 'Method not allowed.', { Allow: allowedMethods.join(', ') });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (!isTrustedBrowserRequest(req, requestTarget, route)) {
|
|
751
|
+
sendProblem(res, 403, 'Proxy requests must come from this local Mavéa page.');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const client = req.socket.remoteAddress || 'unknown';
|
|
756
|
+
const rate = limiter.take(`${route.prefix}:${client}`, route.requestsPerMinute);
|
|
757
|
+
if (!rate.allowed) {
|
|
758
|
+
sendProblem(res, 429, 'Too many proxy requests.', {
|
|
759
|
+
'Retry-After': String(rate.retryAfter),
|
|
760
|
+
});
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const active = activeByRoute.get(route.prefix) || 0;
|
|
765
|
+
if (active >= (route.maxConcurrent || 4)) {
|
|
766
|
+
sendProblem(res, 503, 'This local service is busy. Try again shortly.', {
|
|
767
|
+
'Retry-After': '1',
|
|
768
|
+
});
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
activeByRoute.set(route.prefix, active + 1);
|
|
772
|
+
|
|
773
|
+
void proxyRequest(req, res, route, requestTarget)
|
|
774
|
+
.catch(() => {
|
|
775
|
+
if (!res.headersSent) sendProblem(res, 502, 'The upstream service failed.');
|
|
776
|
+
else res.destroy();
|
|
777
|
+
})
|
|
778
|
+
.finally(() => {
|
|
779
|
+
const remaining = (activeByRoute.get(route.prefix) || 1) - 1;
|
|
780
|
+
if (remaining <= 0) activeByRoute.delete(route.prefix);
|
|
781
|
+
else activeByRoute.set(route.prefix, remaining);
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
server.headersTimeout = 10_000;
|
|
785
|
+
server.requestTimeout = 45_000;
|
|
786
|
+
server.keepAliveTimeout = 5_000;
|
|
787
|
+
server.maxHeadersCount = 100;
|
|
788
|
+
server.maxRequestsPerSocket = 1_000;
|
|
789
|
+
return server;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function listenOnLoopback(server, port) {
|
|
793
|
+
return new Promise((resolveListen, rejectListen) => {
|
|
794
|
+
const onError = (error) => {
|
|
795
|
+
server.off('listening', onListening);
|
|
796
|
+
rejectListen(error);
|
|
797
|
+
};
|
|
798
|
+
const onListening = () => {
|
|
799
|
+
server.off('error', onError);
|
|
800
|
+
resolveListen(server.address());
|
|
801
|
+
};
|
|
802
|
+
server.once('error', onError);
|
|
803
|
+
server.once('listening', onListening);
|
|
804
|
+
server.listen(port, LOOPBACK_HOST);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function openBrowser(url) {
|
|
809
|
+
const platform = process.platform;
|
|
810
|
+
try {
|
|
811
|
+
if (platform === 'darwin') spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
812
|
+
else if (platform === 'win32')
|
|
813
|
+
spawn('cmd', ['/c', 'start', '""', url], { stdio: 'ignore', detached: true }).unref();
|
|
814
|
+
else spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
815
|
+
} catch {
|
|
816
|
+
// best-effort only
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function dockerInstalled() {
|
|
821
|
+
try {
|
|
822
|
+
execSync('docker --version', { stdio: 'ignore' });
|
|
823
|
+
return true;
|
|
824
|
+
} catch {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function dockerRunning() {
|
|
830
|
+
try {
|
|
831
|
+
execSync('docker info', { stdio: 'ignore' });
|
|
832
|
+
return true;
|
|
833
|
+
} catch {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function commandExists(cmd) {
|
|
839
|
+
try {
|
|
840
|
+
execSync(process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`, {
|
|
841
|
+
stdio: 'ignore',
|
|
842
|
+
shell: true,
|
|
843
|
+
});
|
|
844
|
+
return true;
|
|
845
|
+
} catch {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function waitForDocker(timeoutMs = 90_000) {
|
|
851
|
+
return new Promise((resolve) => {
|
|
852
|
+
const started = Date.now();
|
|
853
|
+
const tick = () => {
|
|
854
|
+
if (dockerRunning()) return resolve(true);
|
|
855
|
+
if (Date.now() - started > timeoutMs) return resolve(false);
|
|
856
|
+
setTimeout(tick, 2000);
|
|
857
|
+
};
|
|
858
|
+
tick();
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function kokoroReachable(url) {
|
|
863
|
+
try {
|
|
864
|
+
const ctrl = new AbortController();
|
|
865
|
+
const timer = setTimeout(() => ctrl.abort(), 800);
|
|
866
|
+
await fetch(url, { signal: ctrl.signal });
|
|
867
|
+
clearTimeout(timer);
|
|
868
|
+
return true;
|
|
869
|
+
} catch {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function askYesNo(question) {
|
|
875
|
+
return new Promise((resolve) => {
|
|
876
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
877
|
+
rl.question(question, (answer) => {
|
|
878
|
+
rl.close();
|
|
879
|
+
resolve(/^y/i.test(answer.trim()) || answer.trim() === '');
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function startKokoro() {
|
|
885
|
+
console.log(' Starting Kokoro TTS (first run downloads the voice model, ~1 min)…');
|
|
886
|
+
spawn('docker', ['compose', '-f', COMPOSE_FILE, 'up', '-d'], { stdio: 'inherit' });
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const VOICE_INTRO =
|
|
890
|
+
'\n◌ Voice — Mavéa speaks its replies aloud using a local TTS model (Kokoro).\n' +
|
|
891
|
+
' It runs in a small Docker container, fully on your machine — no audio ever\n' +
|
|
892
|
+
' leaves your computer.\n';
|
|
893
|
+
|
|
894
|
+
async function offerDockerInstall() {
|
|
895
|
+
console.log(VOICE_INTRO);
|
|
896
|
+
console.log(" Docker isn't installed, so for now Mavéa is text-only:");
|
|
897
|
+
console.log(' ✗ no spoken replies — lines appear as captions instead');
|
|
898
|
+
console.log(' ✓ everything else works: demo, live mode, gallery, actions\n');
|
|
899
|
+
|
|
900
|
+
const later =
|
|
901
|
+
' To enable voice later: install Docker (https://docker.com/get-started),\n' +
|
|
902
|
+
' then re-run `npx mavea` — it will detect Docker and set voice up for you.';
|
|
903
|
+
|
|
904
|
+
if (process.platform === 'darwin' && commandExists('brew')) {
|
|
905
|
+
const yes = await askYesNo(
|
|
906
|
+
' Install Docker now via Homebrew? [Y/n] (~600 MB, then launch Docker\n' +
|
|
907
|
+
' Desktop once and re-run `npx mavea` — voice will offer to start itself) ',
|
|
908
|
+
);
|
|
909
|
+
if (!yes) return console.log(' Skipping.\n' + later);
|
|
910
|
+
const r = spawn('brew', ['install', '--cask', 'docker'], { stdio: 'inherit' });
|
|
911
|
+
r.on('exit', (code) => {
|
|
912
|
+
console.log(
|
|
913
|
+
code === 0
|
|
914
|
+
? ' ✓ Docker installed. Launch Docker Desktop once (to accept its service\n' +
|
|
915
|
+
' agreement), then re-run `npx mavea` to enable voice.'
|
|
916
|
+
: ' Homebrew install failed.\n' + later,
|
|
917
|
+
);
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (process.platform === 'win32' && commandExists('winget')) {
|
|
923
|
+
const yes = await askYesNo(
|
|
924
|
+
' Install Docker Desktop now via winget? [Y/n] (then launch it once and\n' +
|
|
925
|
+
' re-run `npx mavea` — voice will offer to start itself) ',
|
|
926
|
+
);
|
|
927
|
+
if (!yes) return console.log(' Skipping.\n' + later);
|
|
928
|
+
const r = spawn('winget', ['install', '-e', '--id', 'Docker.DockerDesktop'], {
|
|
929
|
+
stdio: 'inherit',
|
|
930
|
+
});
|
|
931
|
+
r.on('exit', (code) => {
|
|
932
|
+
console.log(
|
|
933
|
+
code === 0
|
|
934
|
+
? ' ✓ Docker installed. Launch Docker Desktop once, then re-run `npx mavea`.'
|
|
935
|
+
: ' winget install failed.\n' + later,
|
|
936
|
+
);
|
|
937
|
+
});
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (process.platform === 'linux') {
|
|
942
|
+
// Docker's install script needs sudo — print it, never run it on the user's behalf.
|
|
943
|
+
console.log(' To install Docker, run:\n');
|
|
944
|
+
console.log(' curl -fsSL https://get.docker.com | sh\n');
|
|
945
|
+
console.log(' then re-run `npx mavea` — it will detect Docker and set voice up for you.');
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const yes = await askYesNo(' Open the Docker download page in your browser? [Y/n] ');
|
|
950
|
+
if (yes) openBrowser('https://www.docker.com/get-started/');
|
|
951
|
+
console.log(later);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function offerDockerStart() {
|
|
955
|
+
console.log(VOICE_INTRO);
|
|
956
|
+
console.log(' Docker is installed but not running.\n');
|
|
957
|
+
if (process.platform === 'darwin' || process.platform === 'win32') {
|
|
958
|
+
const yes = await askYesNo(' Start Docker Desktop now? [Y/n] ');
|
|
959
|
+
if (!yes) {
|
|
960
|
+
console.log(' Skipping — captions only. Start Docker Desktop and re-run `npx mavea`.');
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (process.platform === 'darwin') {
|
|
964
|
+
spawn('open', ['-a', 'Docker'], { stdio: 'ignore', detached: true }).unref();
|
|
965
|
+
} else {
|
|
966
|
+
spawn('cmd', ['/c', 'start', '""', 'docker desktop'], {
|
|
967
|
+
stdio: 'ignore',
|
|
968
|
+
detached: true,
|
|
969
|
+
}).unref();
|
|
970
|
+
}
|
|
971
|
+
process.stdout.write(' … waiting for Docker to start ');
|
|
972
|
+
const up = await waitForDocker();
|
|
973
|
+
if (!up) {
|
|
974
|
+
console.log('\n Docker did not come up in time — captions only for now.');
|
|
975
|
+
console.log(' Once Docker Desktop is running, re-run `npx mavea` to enable voice.');
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
console.log('✓');
|
|
979
|
+
startKokoro();
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
console.log(' Start it (e.g. `sudo systemctl start docker`), then re-run `npx mavea`.');
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function maybeOfferVoice() {
|
|
986
|
+
const kokoroUrl = process.env.KOKORO_URL || 'http://localhost:8880';
|
|
987
|
+
if (await kokoroReachable(kokoroUrl)) return;
|
|
988
|
+
if (!existsSync(COMPOSE_FILE) || !process.stdin.isTTY) {
|
|
989
|
+
console.log(
|
|
990
|
+
"Voice (Kokoro TTS) isn't running — lines will show as captions only. " +
|
|
991
|
+
'Re-run `npx mavea` in a terminal to set it up, or start Kokoro yourself on :8880.',
|
|
992
|
+
);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (!dockerInstalled()) return offerDockerInstall();
|
|
996
|
+
if (!dockerRunning()) return offerDockerStart();
|
|
997
|
+
console.log(VOICE_INTRO);
|
|
998
|
+
const yes = await askYesNo(' Start it now? [Y/n] ');
|
|
999
|
+
if (!yes) {
|
|
1000
|
+
console.log(' Skipping — captions only. Re-run `npx mavea` any time to enable voice.');
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
startKokoro();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async function main() {
|
|
1007
|
+
let args;
|
|
1008
|
+
try {
|
|
1009
|
+
args = parseArgs(process.argv.slice(2));
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
console.error(`mavea: ${error.message}`);
|
|
1012
|
+
process.exitCode = 1;
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (args.help) {
|
|
1017
|
+
console.log(`mavea — run the Mavéa app locally
|
|
1018
|
+
|
|
1019
|
+
Usage: npx mavea [options]
|
|
1020
|
+
|
|
1021
|
+
Options:
|
|
1022
|
+
--port, -p <n> Port to serve on (default 4173, or $PORT)
|
|
1023
|
+
--no-open Don't open a browser automatically
|
|
1024
|
+
--no-voice Skip the Kokoro (local TTS) Docker prompt
|
|
1025
|
+
--help, -h Show this help
|
|
1026
|
+
|
|
1027
|
+
The CLI listens only on 127.0.0.1. Live mode needs your own model API key (BYOK) — add it in
|
|
1028
|
+
Settings inside the app. Requests pass through this local proxy to your selected providers.
|
|
1029
|
+
AI output may be inaccurate and is not professional advice. Provider terms and charges apply.
|
|
1030
|
+
|
|
1031
|
+
License: PolyForm Noncommercial 1.0.0 (noncommercial use only)
|
|
1032
|
+
Terms, privacy, disclaimer, and third-party notices ship with this package.
|
|
1033
|
+
`);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (!existsSync(DIST)) {
|
|
1038
|
+
console.error(
|
|
1039
|
+
'mavea: no build found in this package (missing dist/). If you are running from source, ' +
|
|
1040
|
+
'run `pnpm build` first; a published package should always ship a prebuilt dist/.',
|
|
1041
|
+
);
|
|
1042
|
+
process.exitCode = 1;
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const server = createMaveaServer();
|
|
1047
|
+
let address;
|
|
1048
|
+
try {
|
|
1049
|
+
address = await listenOnLoopback(server, args.port);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
console.error(`mavea: could not listen on ${LOOPBACK_HOST}:${args.port} (${error.message})`);
|
|
1052
|
+
process.exitCode = 1;
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const actualPort = typeof address === 'object' && address ? address.port : args.port;
|
|
1056
|
+
const base = `http://${LOOPBACK_HOST}:${actualPort}`;
|
|
1057
|
+
console.log(`\nMavéa is running at ${base}`);
|
|
1058
|
+
console.log(` Demo: ${base}/`);
|
|
1059
|
+
console.log(` Live: ${base}/#/live`);
|
|
1060
|
+
console.log(` Gallery: ${base}/#/gallery`);
|
|
1061
|
+
console.log('\nLive mode needs your own model API key (BYOK) — add it in Settings in the app.');
|
|
1062
|
+
console.log('Requests pass through this local proxy. Provider terms and charges apply.');
|
|
1063
|
+
console.log(
|
|
1064
|
+
'AI output may be inaccurate and is not professional advice; verify important information.',
|
|
1065
|
+
);
|
|
1066
|
+
console.log(`Terms: ${base}/legal/TERMS.md`);
|
|
1067
|
+
console.log(`Privacy: ${base}/legal/PRIVACY.md`);
|
|
1068
|
+
console.log(`License: ${base}/legal/LICENSE.txt (PolyForm Noncommercial 1.0.0)`);
|
|
1069
|
+
if (args.open) openBrowser(base);
|
|
1070
|
+
if (args.voice) await maybeOfferVoice();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
1074
|
+
if (isMain) {
|
|
1075
|
+
void main().catch((error) => {
|
|
1076
|
+
console.error(`mavea: ${error instanceof Error ? error.message : String(error)}`);
|
|
1077
|
+
process.exitCode = 1;
|
|
1078
|
+
});
|
|
1079
|
+
}
|