@sunshinelife83/hearth 1.0.0
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 +21 -0
- package/README.md +323 -0
- package/bin/hearth-agentd.js +4 -0
- package/bin/hearth.js +4 -0
- package/bin/run-entrypoint.js +20 -0
- package/dist/agent-tools.js +738 -0
- package/dist/app-version.js +16 -0
- package/dist/apply-patch.js +402 -0
- package/dist/artifact-error.js +8 -0
- package/dist/artifact-tools.js +377 -0
- package/dist/cli-workspace.js +36 -0
- package/dist/cli.js +1086 -0
- package/dist/config-migration.js +82 -0
- package/dist/config-schema.js +136 -0
- package/dist/config.js +115 -0
- package/dist/context/context-tools.js +150 -0
- package/dist/context/repo-map.js +102 -0
- package/dist/dashboard/app.js +226 -0
- package/dist/dashboard/index.html +45 -0
- package/dist/dashboard/landing.html +57 -0
- package/dist/dashboard/styles.css +23 -0
- package/dist/dashboard.js +271 -0
- package/dist/db/client.js +29 -0
- package/dist/db/migrations.js +262 -0
- package/dist/db/schema.js +85 -0
- package/dist/device-tokens.js +61 -0
- package/dist/git-worktrees.js +134 -0
- package/dist/git.js +41 -0
- package/dist/incoming-artifacts.js +378 -0
- package/dist/local-agent-acp.js +768 -0
- package/dist/local-agent-adapters.js +20 -0
- package/dist/local-agent-availability.js +92 -0
- package/dist/local-agent-catalog.js +51 -0
- package/dist/local-agent-claude.js +334 -0
- package/dist/local-agent-client.js +519 -0
- package/dist/local-agent-codex.js +532 -0
- package/dist/local-agent-config.js +34 -0
- package/dist/local-agent-daemon-lifecycle.js +195 -0
- package/dist/local-agent-daemon-main.js +86 -0
- package/dist/local-agent-daemon-protocol.js +302 -0
- package/dist/local-agent-daemon.js +407 -0
- package/dist/local-agent-errors.js +378 -0
- package/dist/local-agent-grok.js +202 -0
- package/dist/local-agent-manager.js +715 -0
- package/dist/local-agent-opencode.js +463 -0
- package/dist/local-agent-path.js +24 -0
- package/dist/local-agent-pi-sandbox.js +472 -0
- package/dist/local-agent-pi.js +298 -0
- package/dist/local-agent-presentation.js +104 -0
- package/dist/local-agent-profiles.js +127 -0
- package/dist/local-agent-runtime-pool.js +481 -0
- package/dist/local-agent-runtime.js +1 -0
- package/dist/local-agent-store.js +193 -0
- package/dist/local-agent-targets.js +97 -0
- package/dist/logger.js +97 -0
- package/dist/machine-id.js +35 -0
- package/dist/mcp-modern-server.js +65 -0
- package/dist/oauth-provider.js +331 -0
- package/dist/oauth-store.js +144 -0
- package/dist/onboarding.js +35 -0
- package/dist/orchestration/brief.js +70 -0
- package/dist/orchestration/lanes.js +145 -0
- package/dist/orchestration/result.js +46 -0
- package/dist/orchestration/scope.js +61 -0
- package/dist/pi-tools.js +58 -0
- package/dist/policy/command-policy.js +104 -0
- package/dist/policy/env-allowlist.js +96 -0
- package/dist/policy/sandbox.js +68 -0
- package/dist/policy/workspace-profiles.js +47 -0
- package/dist/process-journal.js +116 -0
- package/dist/process-platform.js +49 -0
- package/dist/process-sessions.js +388 -0
- package/dist/rate-limit.js +77 -0
- package/dist/request-meta.js +9 -0
- package/dist/review-checkpoints.js +349 -0
- package/dist/roots.js +37 -0
- package/dist/server-shutdown.js +12 -0
- package/dist/server.js +980 -0
- package/dist/skills.js +84 -0
- package/dist/snapshot-manager.js +123 -0
- package/dist/stdio-server.js +80 -0
- package/dist/task-store.js +203 -0
- package/dist/task-tools.js +455 -0
- package/dist/tool-surfaces/claude.js +220 -0
- package/dist/tool-surfaces/codex.js +255 -0
- package/dist/tool-surfaces/index.js +15 -0
- package/dist/tool-surfaces/shared.js +228 -0
- package/dist/tool-surfaces/types.js +27 -0
- package/dist/ui/.vite/manifest.json +2544 -0
- package/dist/ui/assets/abap-CLvhMVsD.js +1 -0
- package/dist/ui/assets/actionscript-3--17pq3dv.js +1 -0
- package/dist/ui/assets/ada-C5qYipkI.js +1 -0
- package/dist/ui/assets/andromeeda-vGVdxbeo.js +1 -0
- package/dist/ui/assets/angular-html-DrbO3_Xe.js +1 -0
- package/dist/ui/assets/angular-ts-CmTVzHJx.js +1 -0
- package/dist/ui/assets/apache-U0d_L8uA.js +1 -0
- package/dist/ui/assets/apex-CGTLDQj6.js +1 -0
- package/dist/ui/assets/apl-BOpCpqXf.js +1 -0
- package/dist/ui/assets/applescript-CCn79oCD.js +1 -0
- package/dist/ui/assets/ara-4CJ0cIlV.js +1 -0
- package/dist/ui/assets/asciidoc-DE70LPWp.js +1 -0
- package/dist/ui/assets/asm-Cmm7eHzH.js +1 -0
- package/dist/ui/assets/astro-CxPeBwei.js +1 -0
- package/dist/ui/assets/aurora-x-CDeNXAV0.js +1 -0
- package/dist/ui/assets/awk-BWXHIvNe.js +1 -0
- package/dist/ui/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/dist/ui/assets/ayu-light-C3h-C4tm.js +1 -0
- package/dist/ui/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/dist/ui/assets/ballerina-B7ZEbQpA.js +1 -0
- package/dist/ui/assets/bat-Bo4NYOV-.js +1 -0
- package/dist/ui/assets/beancount-D-usSTwE.js +1 -0
- package/dist/ui/assets/berry-DKpUyyne.js +1 -0
- package/dist/ui/assets/bibtex-Ci_nEsc7.js +1 -0
- package/dist/ui/assets/bicep-CUHmPFLl.js +1 -0
- package/dist/ui/assets/bird2-C2hNVINV.js +1 -0
- package/dist/ui/assets/blade-DtsAAG4P.js +1 -0
- package/dist/ui/assets/bsl-BkkzgIyY.js +1 -0
- package/dist/ui/assets/c-D9hLjNUM.js +1 -0
- package/dist/ui/assets/c3-CnJL0r0V.js +1 -0
- package/dist/ui/assets/cadence-CQ2zXKGN.js +1 -0
- package/dist/ui/assets/cairo-DLTphjLi.js +1 -0
- package/dist/ui/assets/catppuccin-frappe-3VR1Za6u.js +1 -0
- package/dist/ui/assets/catppuccin-latte-DwIHMF0Q.js +1 -0
- package/dist/ui/assets/catppuccin-macchiato-DYnBP6_5.js +1 -0
- package/dist/ui/assets/catppuccin-mocha-DYhrFGRu.js +1 -0
- package/dist/ui/assets/clarity-SemFz856.js +1 -0
- package/dist/ui/assets/clojure-DqKBuwfJ.js +1 -0
- package/dist/ui/assets/cmake-Bj61d0ZC.js +1 -0
- package/dist/ui/assets/cobol-C1SWlvZm.js +1 -0
- package/dist/ui/assets/codeowners-C8r90Shi.js +1 -0
- package/dist/ui/assets/codeql-oeQT6MSM.js +1 -0
- package/dist/ui/assets/coffee-B71R3v2U.js +1 -0
- package/dist/ui/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/dist/ui/assets/coq-BrsZFFmf.js +1 -0
- package/dist/ui/assets/cpp--5a4c3v4.js +1 -0
- package/dist/ui/assets/crystal-AtqWYtE0.js +1 -0
- package/dist/ui/assets/csharp-Ct8U2NOr.js +1 -0
- package/dist/ui/assets/css-CbqKSHI-.js +1 -0
- package/dist/ui/assets/csv-Dx-8-gkx.js +1 -0
- package/dist/ui/assets/cue-CE9AQfxI.js +1 -0
- package/dist/ui/assets/cypher-ClKdZ_lG.js +1 -0
- package/dist/ui/assets/d-qD-0Kul2.js +1 -0
- package/dist/ui/assets/dark-plus-Cs2F2srj.js +1 -0
- package/dist/ui/assets/dart-DkHntEIa.js +1 -0
- package/dist/ui/assets/dax-BkyTk9wS.js +1 -0
- package/dist/ui/assets/desktop-Dlh5hvp9.js +1 -0
- package/dist/ui/assets/diff-woXpYk--.js +1 -0
- package/dist/ui/assets/docker-IyjqRm3v.js +1 -0
- package/dist/ui/assets/dotenv-_5a1GRtc.js +1 -0
- package/dist/ui/assets/dracula-BHWKrbxM.js +1 -0
- package/dist/ui/assets/dracula-soft-5eyTD99u.js +1 -0
- package/dist/ui/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/dist/ui/assets/edge-BPwY_RrJ.js +1 -0
- package/dist/ui/assets/elixir-DVH2yLJf.js +1 -0
- package/dist/ui/assets/elm-BrleA6gS.js +1 -0
- package/dist/ui/assets/emacs-lisp-C9PiwqqW.js +1 -0
- package/dist/ui/assets/erb-2SHiYe30.js +1 -0
- package/dist/ui/assets/erlang-Cphh6RMH.js +1 -0
- package/dist/ui/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/dist/ui/assets/everforest-light-Df2xbC6M.js +1 -0
- package/dist/ui/assets/fennel-DQxkIbk2.js +1 -0
- package/dist/ui/assets/fish-BJitypiv.js +1 -0
- package/dist/ui/assets/fluent-C03EYrpw.js +1 -0
- package/dist/ui/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/dist/ui/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/dist/ui/assets/fsharp-D13ZGOAj.js +1 -0
- package/dist/ui/assets/gdresource-C0sCabJj.js +1 -0
- package/dist/ui/assets/gdscript-Cp2uCuqX.js +1 -0
- package/dist/ui/assets/gdshader-CBce3t8t.js +1 -0
- package/dist/ui/assets/genie-CV2tkWYe.js +1 -0
- package/dist/ui/assets/gherkin-DExj1W_8.js +1 -0
- package/dist/ui/assets/git-commit-BSykSTBG.js +1 -0
- package/dist/ui/assets/git-rebase-Bc7atMaV.js +1 -0
- package/dist/ui/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/ui/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/dist/ui/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/dist/ui/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/dist/ui/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/ui/assets/github-light-default-BXViO-2h.js +1 -0
- package/dist/ui/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/dist/ui/assets/gleam-CSRkHgEL.js +1 -0
- package/dist/ui/assets/glimmer-js-DXeTDPtW.js +1 -0
- package/dist/ui/assets/glimmer-ts-CEOoXHU4.js +1 -0
- package/dist/ui/assets/glsl-5-43-Gsg.js +1 -0
- package/dist/ui/assets/gn-ilITqXS6.js +1 -0
- package/dist/ui/assets/gnuplot-7GGW24-e.js +1 -0
- package/dist/ui/assets/go-BJwz_mda.js +1 -0
- package/dist/ui/assets/graphql-Bk7Ah1KA.js +1 -0
- package/dist/ui/assets/groovy-CacY0gHj.js +1 -0
- package/dist/ui/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/dist/ui/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/dist/ui/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/dist/ui/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/dist/ui/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/dist/ui/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/dist/ui/assets/hack-DUHz4rCJ.js +1 -0
- package/dist/ui/assets/haml-BPCuuTPR.js +1 -0
- package/dist/ui/assets/handlebars-CHxmUsnU.js +1 -0
- package/dist/ui/assets/haskell-D8IpX4py.js +1 -0
- package/dist/ui/assets/haxe-OTjmBuCE.js +1 -0
- package/dist/ui/assets/hcl-Dh228itO.js +1 -0
- package/dist/ui/assets/hjson-CxZEssPk.js +1 -0
- package/dist/ui/assets/hlsl-Cvrh5tZx.js +1 -0
- package/dist/ui/assets/horizon-CE9ld1lL.js +1 -0
- package/dist/ui/assets/horizon-bright-Br1oVSNq.js +1 -0
- package/dist/ui/assets/houston-CsvMBhTu.js +1 -0
- package/dist/ui/assets/html-BBJqizz0.js +1 -0
- package/dist/ui/assets/html-derivative-ShTPCcKN.js +1 -0
- package/dist/ui/assets/http-VFTyziwA.js +1 -0
- package/dist/ui/assets/hurl-B8yxTa4r.js +1 -0
- package/dist/ui/assets/hxml-B0Qn7Nwc.js +1 -0
- package/dist/ui/assets/hy-CZbG8q4J.js +1 -0
- package/dist/ui/assets/imba-DsUTQ-LC.js +1 -0
- package/dist/ui/assets/ini-B5eOa1yu.js +1 -0
- package/dist/ui/assets/java-BZ5DAgJZ.js +1 -0
- package/dist/ui/assets/javascript-C_zuIEaC.js +1 -0
- package/dist/ui/assets/jinja-DWd6xfNr.js +1 -0
- package/dist/ui/assets/jison-BbY63d1V.js +1 -0
- package/dist/ui/assets/json-C4DnajZs.js +1 -0
- package/dist/ui/assets/json5-BR5RXkoi.js +1 -0
- package/dist/ui/assets/jsonc-CYpm1nAK.js +1 -0
- package/dist/ui/assets/jsonl-CmCQp5Yx.js +1 -0
- package/dist/ui/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/dist/ui/assets/jssm-DXw9l8Rf.js +1 -0
- package/dist/ui/assets/jsx-C4AACieZ.js +1 -0
- package/dist/ui/assets/julia-sRu7EFMW.js +1 -0
- package/dist/ui/assets/just-DpZ9Sx1v.js +1 -0
- package/dist/ui/assets/kanagawa-dragon-CXtmUGW6.js +1 -0
- package/dist/ui/assets/kanagawa-lotus-BN08jTvb.js +1 -0
- package/dist/ui/assets/kanagawa-wave-CTweb8Dz.js +1 -0
- package/dist/ui/assets/kdl-CsD5j6eV.js +1 -0
- package/dist/ui/assets/kotlin-DhhofPvG.js +1 -0
- package/dist/ui/assets/kusto-BUv0MjJC.js +1 -0
- package/dist/ui/assets/laserwave-C_8bwKvT.js +1 -0
- package/dist/ui/assets/latex-B6a_uum7.js +1 -0
- package/dist/ui/assets/lean-CewbzKMR.js +1 -0
- package/dist/ui/assets/less-DVTAwKKz.js +1 -0
- package/dist/ui/assets/light-plus-DVQuIRkW.js +1 -0
- package/dist/ui/assets/liquid-B2sVpjjr.js +1 -0
- package/dist/ui/assets/llvm-Cm23YOpf.js +1 -0
- package/dist/ui/assets/log-BNLmms1o.js +1 -0
- package/dist/ui/assets/logo-Cluzi2Zq.js +1 -0
- package/dist/ui/assets/lua-DxJJll61.js +1 -0
- package/dist/ui/assets/luau-CNKltnaQ.js +1 -0
- package/dist/ui/assets/make-Dixweg8N.js +1 -0
- package/dist/ui/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/ui/assets/marko-Dx8mw-Wx.js +1 -0
- package/dist/ui/assets/material-theme-Bm3Qr25_.js +1 -0
- package/dist/ui/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/dist/ui/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/dist/ui/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/dist/ui/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/dist/ui/assets/matlab-D7qyCx1q.js +1 -0
- package/dist/ui/assets/mdc-vNKeXEwR.js +1 -0
- package/dist/ui/assets/mdx-DQZ5AkYe.js +1 -0
- package/dist/ui/assets/mermaid-Bk4SNUv9.js +1 -0
- package/dist/ui/assets/min-dark-BSWPekZh.js +1 -0
- package/dist/ui/assets/min-light-DDpmG2fV.js +1 -0
- package/dist/ui/assets/mipsasm-BMqwQI7S.js +1 -0
- package/dist/ui/assets/mojo-BgCJLMeH.js +1 -0
- package/dist/ui/assets/monokai-CdkpiU2Y.js +1 -0
- package/dist/ui/assets/moonbit-CaWjb8XO.js +1 -0
- package/dist/ui/assets/move-B1IS1UjX.js +1 -0
- package/dist/ui/assets/narrat-_X_XdTYD.js +1 -0
- package/dist/ui/assets/nextflow-Bbiyy34d.js +1 -0
- package/dist/ui/assets/nextflow-groovy-Dc_ddanL.js +1 -0
- package/dist/ui/assets/nginx-DScQAGLT.js +1 -0
- package/dist/ui/assets/night-owl-DhmEMT88.js +1 -0
- package/dist/ui/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/dist/ui/assets/nim-lAJch9uT.js +1 -0
- package/dist/ui/assets/nix-IvuFDN5E.js +1 -0
- package/dist/ui/assets/nord-Cb4Vim4T.js +1 -0
- package/dist/ui/assets/nushell-DcLAeLz5.js +1 -0
- package/dist/ui/assets/objective-c-D1A_Heim.js +1 -0
- package/dist/ui/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/dist/ui/assets/ocaml-O90oeIOV.js +1 -0
- package/dist/ui/assets/odin-B1RWQWA5.js +1 -0
- package/dist/ui/assets/one-dark-pro-CLwyXe_n.js +1 -0
- package/dist/ui/assets/one-light-D7Lr4KcI.js +1 -0
- package/dist/ui/assets/openscad-BUDT5pXO.js +1 -0
- package/dist/ui/assets/pascal-4ZHwLPI5.js +1 -0
- package/dist/ui/assets/perl-C_GkVo_E.js +1 -0
- package/dist/ui/assets/php-DziV3K6J.js +1 -0
- package/dist/ui/assets/pierre-dark-CpLgRqie.js +1 -0
- package/dist/ui/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/dist/ui/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/dist/ui/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/dist/ui/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/dist/ui/assets/pierre-light-CoaEpmwp.js +1 -0
- package/dist/ui/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/dist/ui/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/dist/ui/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/dist/ui/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/dist/ui/assets/pkl-ot-7Btpt.js +1 -0
- package/dist/ui/assets/plastic-DQwYfKfQ.js +1 -0
- package/dist/ui/assets/plsql-DGHpHOYJ.js +1 -0
- package/dist/ui/assets/po-BiJDBrnU.js +1 -0
- package/dist/ui/assets/poimandres-DRFjx7u4.js +1 -0
- package/dist/ui/assets/polar-C7UOKdEL.js +1 -0
- package/dist/ui/assets/postcss-BXeXVLqQ.js +1 -0
- package/dist/ui/assets/powerquery-DNMTfnFr.js +1 -0
- package/dist/ui/assets/powershell-DshXNtvi.js +1 -0
- package/dist/ui/assets/prisma-BsRQq5mF.js +1 -0
- package/dist/ui/assets/prolog-iXnhIJG7.js +1 -0
- package/dist/ui/assets/proto-DB4EqR-F.js +1 -0
- package/dist/ui/assets/pug-z75UIWVK.js +1 -0
- package/dist/ui/assets/puppet-CDv2pdJW.js +1 -0
- package/dist/ui/assets/purescript-9MfHhQsQ.js +1 -0
- package/dist/ui/assets/python-gzcpVVnB.js +1 -0
- package/dist/ui/assets/qml-Cmm4coz1.js +1 -0
- package/dist/ui/assets/qmldir-DCQb3MpD.js +1 -0
- package/dist/ui/assets/qss-Fe1Jh2GI.js +1 -0
- package/dist/ui/assets/r-Brd0axBE.js +1 -0
- package/dist/ui/assets/racket-DcIDlBhZ.js +1 -0
- package/dist/ui/assets/raku-B3gFvitq.js +1 -0
- package/dist/ui/assets/razor-DSbwmoiu.js +1 -0
- package/dist/ui/assets/red-CJ3rzSJv.js +1 -0
- package/dist/ui/assets/reg-CRGYupPL.js +1 -0
- package/dist/ui/assets/regexp-Bs3Uk-pY.js +1 -0
- package/dist/ui/assets/rel-BtDbiS_P.js +1 -0
- package/dist/ui/assets/review-payload-CbwCBKB1.js +1580 -0
- package/dist/ui/assets/riscv-Ckw8ddFX.js +1 -0
- package/dist/ui/assets/ron-VUp2lXgN.js +1 -0
- package/dist/ui/assets/rose-pine-BthvhNj6.js +1 -0
- package/dist/ui/assets/rose-pine-dawn-Dg85fqjY.js +1 -0
- package/dist/ui/assets/rose-pine-moon-hon4tzzS.js +1 -0
- package/dist/ui/assets/rosmsg-CAekHB0j.js +1 -0
- package/dist/ui/assets/rst-ChIyKpUC.js +1 -0
- package/dist/ui/assets/ruby-BdAmvOMO.js +1 -0
- package/dist/ui/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/ui/assets/sas-DpXn8hc5.js +1 -0
- package/dist/ui/assets/sass-DXrisJhu.js +1 -0
- package/dist/ui/assets/scala-DKOlJaKm.js +1 -0
- package/dist/ui/assets/scheme-DQCgrYNe.js +1 -0
- package/dist/ui/assets/scss-atX1Uuef.js +1 -0
- package/dist/ui/assets/sdbl-bTVj8UrX.js +1 -0
- package/dist/ui/assets/shaderlab-TOUzSsQk.js +1 -0
- package/dist/ui/assets/shellscript-BqgcAeMK.js +1 -0
- package/dist/ui/assets/shellsession-DfQSMRN5.js +1 -0
- package/dist/ui/assets/slack-dark-DnToyrRv.js +1 -0
- package/dist/ui/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/dist/ui/assets/smalltalk-B16xEiuN.js +1 -0
- package/dist/ui/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/dist/ui/assets/solarized-dark-DV17i1UV.js +1 -0
- package/dist/ui/assets/solarized-light-DSh2HLQt.js +1 -0
- package/dist/ui/assets/solidity-CKzVLygQ.js +1 -0
- package/dist/ui/assets/soy-DzP7zZTB.js +1 -0
- package/dist/ui/assets/sparql-D_iOobhT.js +1 -0
- package/dist/ui/assets/splunk-BC2Px7Mm.js +1 -0
- package/dist/ui/assets/sql-gmaLk66z.js +1 -0
- package/dist/ui/assets/ssh-config-BgfXC-Er.js +1 -0
- package/dist/ui/assets/stata-CJ0YTMcV.js +1 -0
- package/dist/ui/assets/stylus-B6D30XZt.js +1 -0
- package/dist/ui/assets/surrealql-LH7FciQz.js +1 -0
- package/dist/ui/assets/svelte-BME2PU6V.js +1 -0
- package/dist/ui/assets/swift-DonLKvLd.js +1 -0
- package/dist/ui/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/dist/ui/assets/system-verilog-DJ5XKQeo.js +1 -0
- package/dist/ui/assets/systemd-BxMlprV5.js +1 -0
- package/dist/ui/assets/talonscript-CohzipZa.js +1 -0
- package/dist/ui/assets/tasl-DMoTqEGO.js +1 -0
- package/dist/ui/assets/tcl-CZd0xW_V.js +1 -0
- package/dist/ui/assets/templ-DHBNIfYW.js +1 -0
- package/dist/ui/assets/terraform-DswuEJGm.js +1 -0
- package/dist/ui/assets/tex-C8-kPEzO.js +1 -0
- package/dist/ui/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/dist/ui/assets/toml-CcmNWLt0.js +1 -0
- package/dist/ui/assets/ts-tags-CiewV0vR.js +1 -0
- package/dist/ui/assets/tsv-sltzmVWM.js +1 -0
- package/dist/ui/assets/tsx-CUYPqmnq.js +1 -0
- package/dist/ui/assets/turtle-ByJddavk.js +1 -0
- package/dist/ui/assets/twig-BTkvsl0b.js +1 -0
- package/dist/ui/assets/typescript-DDQMaPwj.js +1 -0
- package/dist/ui/assets/typespec-B88KGewJ.js +1 -0
- package/dist/ui/assets/typst-DI99ib-x.js +1 -0
- package/dist/ui/assets/v-DETTlOr0.js +1 -0
- package/dist/ui/assets/vala-zf12oZj6.js +1 -0
- package/dist/ui/assets/vb-Djn5o6TS.js +1 -0
- package/dist/ui/assets/verilog-CiiDBU1e.js +1 -0
- package/dist/ui/assets/vesper-D5bVUKB1.js +1 -0
- package/dist/ui/assets/vhdl-BroJfC0k.js +1 -0
- package/dist/ui/assets/viml-DvXPmvsu.js +1 -0
- package/dist/ui/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/dist/ui/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/dist/ui/assets/vitesse-light-VbXTXTou.js +1 -0
- package/dist/ui/assets/vue-BH5SEWRH.js +1 -0
- package/dist/ui/assets/vue-html-BT7yeYez.js +1 -0
- package/dist/ui/assets/vue-vine-Cs5IN2rs.js +1 -0
- package/dist/ui/assets/vyper-CgoNMtux.js +1 -0
- package/dist/ui/assets/wasm-BnjxR4X6.js +1 -0
- package/dist/ui/assets/wasm-ByWQv1Qj.js +1 -0
- package/dist/ui/assets/wenyan-C8pVoKbM.js +1 -0
- package/dist/ui/assets/wgsl-BsKzXJz4.js +1 -0
- package/dist/ui/assets/wikitext-ClFFjSW2.js +1 -0
- package/dist/ui/assets/wit-DdvCle-K.js +1 -0
- package/dist/ui/assets/wolfram-DLL8P-h_.js +1 -0
- package/dist/ui/assets/workspace-app-B_BnSEHO.css +1 -0
- package/dist/ui/assets/workspace-app-DSFpnrIC.js +115 -0
- package/dist/ui/assets/xml-D3_1BEcO.js +1 -0
- package/dist/ui/assets/xsl-BBOCDn4Y.js +1 -0
- package/dist/ui/assets/yaml-lThBDT45.js +1 -0
- package/dist/ui/assets/zenscript-BnlCZFoB.js +1 -0
- package/dist/ui/assets/zig-CMLA9XwU.js +1 -0
- package/dist/ui/workspace-app.html +15 -0
- package/dist/user-config.js +228 -0
- package/dist/verification.js +74 -0
- package/dist/workspace-store.js +133 -0
- package/dist/workspaces.js +433 -0
- package/docs/adr/devspace-x-implementation-adrs.md +159 -0
- package/docs/agent-profile-schema.md +203 -0
- package/docs/artifact-exchange.md +44 -0
- package/docs/assets/devspace-logo-light.png +0 -0
- package/docs/assets/devspace-screenshot.png +0 -0
- package/docs/assets/v11-review-ui-after.png +0 -0
- package/docs/assets/v11-review-ui-before.png +0 -0
- package/docs/chatgpt-coding-workflow.md +215 -0
- package/docs/configuration.md +217 -0
- package/docs/gotchas.md +294 -0
- package/docs/local-agent-daemon.md +101 -0
- package/docs/security.md +197 -0
- package/docs/setup.md +174 -0
- package/examples/agents/claude-implementer.md +30 -0
- package/examples/agents/codex-explorer.md +27 -0
- package/examples/agents/codex-qa-tester.md +29 -0
- package/examples/agents/codex-worker.md +25 -0
- package/examples/agents/copilot-reviewer.md +25 -0
- package/examples/agents/cursor-agent-worker.md +26 -0
- package/examples/agents/grok-builder.md +10 -0
- package/examples/agents/opencode-explorer.md +27 -0
- package/examples/agents/pi-reviewer.md +27 -0
- package/package.json +102 -0
- package/schema/v1/hearth.schema.json +529 -0
- package/scripts/copy-dashboard.mjs +9 -0
- package/scripts/fix-node-pty-permissions.mjs +22 -0
- package/scripts/generate-config-schema.ts +9 -0
- package/skills/subagents/SKILL.md +57 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Hearth local dashboard. Same-origin fetch; session cookie is HttpOnly.
|
|
3
|
+
* All mutations send JSON (required by the server as fetch-only CSRF defense). */
|
|
4
|
+
const $ = (id) => document.getElementById(id);
|
|
5
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
6
|
+
|
|
7
|
+
async function api(path, options = {}) {
|
|
8
|
+
const res = await fetch(path, {
|
|
9
|
+
credentials: "same-origin",
|
|
10
|
+
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
|
|
11
|
+
...options,
|
|
12
|
+
});
|
|
13
|
+
if (res.status === 401) { showLogin(); throw new Error("unauthorized"); }
|
|
14
|
+
const body = await res.json().catch(() => ({}));
|
|
15
|
+
if (!res.ok) throw new Error(body.error || `request failed (${res.status})`);
|
|
16
|
+
return body;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function showLogin() {
|
|
20
|
+
$("login").hidden = false;
|
|
21
|
+
$("tabs").hidden = true;
|
|
22
|
+
$("logout").hidden = true;
|
|
23
|
+
document.querySelectorAll(".tab").forEach((el) => { el.hidden = true; });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function showApp() {
|
|
27
|
+
$("login").hidden = true;
|
|
28
|
+
$("tabs").hidden = false;
|
|
29
|
+
$("logout").hidden = false;
|
|
30
|
+
switchTab("overview");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function switchTab(name) {
|
|
34
|
+
document.querySelectorAll("#tabs button").forEach((b) => b.classList.toggle("active", b.dataset.tab === name));
|
|
35
|
+
document.querySelectorAll(".tab").forEach((el) => { el.hidden = el.id !== name; });
|
|
36
|
+
renderers[name]();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
$("tabs").addEventListener("click", (e) => {
|
|
40
|
+
const tab = e.target.closest("button")?.dataset.tab;
|
|
41
|
+
if (tab) switchTab(tab);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
$("login-form").addEventListener("submit", async (e) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
$("login-error").hidden = true;
|
|
47
|
+
try {
|
|
48
|
+
await api("/dashboard/api/login", { method: "POST", body: JSON.stringify({ ownerToken: $("owner-token").value }) });
|
|
49
|
+
$("owner-token").value = "";
|
|
50
|
+
showApp();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
$("login-error").textContent = err.message;
|
|
53
|
+
$("login-error").hidden = false;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
$("logout").addEventListener("click", async () => {
|
|
58
|
+
await api("/dashboard/api/logout", { method: "POST", body: "{}" }).catch(() => {});
|
|
59
|
+
showLogin();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const pill = (text, cls = "") => `<span class="pill ${cls}">${esc(text)}</span>`;
|
|
63
|
+
|
|
64
|
+
const renderers = {
|
|
65
|
+
async overview() {
|
|
66
|
+
const el = $("overview");
|
|
67
|
+
try {
|
|
68
|
+
const s = await api("/dashboard/api/status");
|
|
69
|
+
const tasks = Object.entries(s.tasks || {}).map(([k, v]) => `${pill(`${k}: ${v}`)}`).join(" ") || "<span class='muted'>no tasks</span>";
|
|
70
|
+
const providers = (s.providers || []).map((p) => `<tr><td>${esc(p.id)}</td><td>${p.enabled ? "yes" : "no"}</td><td>${p.available ? "<span class='good'>available</span>" : "<span class='muted'>unavailable</span>"}</td></tr>`).join("");
|
|
71
|
+
el.innerHTML = `
|
|
72
|
+
<div class="card"><h3>Status</h3>
|
|
73
|
+
<div>Version ${esc(s.version)} · mode ${pill(s.mode)} · sandbox ${esc(s.sandbox)} (${esc(s.sandboxAdapter)})</div>
|
|
74
|
+
<div class="muted">MCP ${esc("/mcp")} · health ${esc("/healthz")}</div></div>
|
|
75
|
+
<div class="card"><h3>Tasks</h3><div>${tasks}</div></div>
|
|
76
|
+
<div class="card"><h3>Providers</h3><table><tr><th>ID</th><th>Enabled</th><th>Status</th></tr>${providers}</table></div>
|
|
77
|
+
<div class="card"><h3>Open workspaces</h3><pre>${esc((s.workspaces || []).map((w) => `${w.id} ${w.root}`).join("\n") || "none")}</pre></div>`;
|
|
78
|
+
} catch (err) { el.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async workspaces() {
|
|
82
|
+
const el = $("workspaces");
|
|
83
|
+
try {
|
|
84
|
+
const w = await api("/dashboard/api/workspaces");
|
|
85
|
+
const open = (w.open || []).map((o) => `<tr><td><code>${esc(o.id)}</code></td><td>${esc(o.root)}</td><td>${esc(o.mode)}</td></tr>`).join("") || `<tr><td colspan="3" class="muted">none open</td></tr>`;
|
|
86
|
+
const profiles = (w.profiles || []).map((p) => `<tr><td>${esc(p.path)}</td><td>${esc(p.mode || "inherit")}</td><td>${p.agentsAllowed === false ? "agents off" : "agents on"}</td></tr>`).join("") || `<tr><td colspan="3" class="muted">no profiles</td></tr>`;
|
|
87
|
+
el.innerHTML = `
|
|
88
|
+
<div class="card"><h3>Allowed roots</h3><pre>${esc((w.allowedRoots || []).join("\n"))}</pre></div>
|
|
89
|
+
<div class="card"><h3>Open workspaces</h3><table><tr><th>ID</th><th>Root</th><th>Mode</th></tr>${open}</table></div>
|
|
90
|
+
<div class="card"><h3>Workspace profiles</h3><table><tr><th>Path</th><th>Mode</th><th>Agents</th></tr>${profiles}</table></div>`;
|
|
91
|
+
} catch (err) { el.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async agents() {
|
|
95
|
+
const el = $("agents");
|
|
96
|
+
try {
|
|
97
|
+
const w = await api("/dashboard/api/workspaces");
|
|
98
|
+
if (!w.open.length) { el.innerHTML = `<p class="muted">No open workspaces.</p>`; return; }
|
|
99
|
+
const sel = document.createElement("select");
|
|
100
|
+
w.open.forEach((o) => { const opt = document.createElement("option"); opt.value = o.id; opt.textContent = `${o.id} — ${o.root}`; sel.appendChild(opt); });
|
|
101
|
+
el.innerHTML = `<div class="card"><div class="row"></div><div id="agent-list"></div></div>`;
|
|
102
|
+
el.querySelector(".row").append(sel);
|
|
103
|
+
const load = async () => {
|
|
104
|
+
const data = await api(`/dashboard/api/agents?workspaceId=${encodeURIComponent(sel.value)}`);
|
|
105
|
+
const rows = (data.agents || []).map((a) => `<tr><td><code>${esc(a.id)}</code></td><td>${esc(a.provider)}/${esc(a.profileName)}</td><td>${esc(a.status)}</td><td>${esc((a.latestResponse || a.latestOutput || a.error || "").slice(0, 120))}</td></tr>`).join("") || `<tr><td colspan="4" class="muted">no agents</td></tr>`;
|
|
106
|
+
el.querySelector("#agent-list").innerHTML = `<table><tr><th>ID</th><th>Backend</th><th>Status</th><th>Latest</th></tr>${rows}</table>`;
|
|
107
|
+
};
|
|
108
|
+
sel.addEventListener("change", () => load().catch((e) => { el.querySelector("#agent-list").innerHTML = `<p class="error">${esc(e.message)}</p>`; }));
|
|
109
|
+
await load();
|
|
110
|
+
} catch (err) { el.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async tasks() {
|
|
114
|
+
const el = $("tasks");
|
|
115
|
+
try {
|
|
116
|
+
const data = await api("/dashboard/api/tasks?limit=50");
|
|
117
|
+
el.innerHTML = (data.tasks || []).map((t) => `
|
|
118
|
+
<div class="card"><h3><code>${esc(t.id)}</code> ${pill(t.status)} ${t.completionState ? pill(t.completionState) : ""}</h3>
|
|
119
|
+
<div>${esc(t.goal)}</div>
|
|
120
|
+
${t.error ? `<div class="error">${esc(t.error)}</div>` : ""}
|
|
121
|
+
<pre>${esc((t.evidence || []).slice(-8).map((e) => `- [${e.kind}] ${e.summary}`).join("\n") || "no evidence yet")}</pre>
|
|
122
|
+
</div>`).join("") || `<p class="muted">No tasks yet. Create one from any MCP client with task_create.</p>`;
|
|
123
|
+
} catch (err) { el.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async logs() {
|
|
127
|
+
const el = $("logs");
|
|
128
|
+
try {
|
|
129
|
+
const data = await api("/dashboard/api/logs?limit=100");
|
|
130
|
+
const rows = (data.entries || []).reverse().map((e) => `<tr><td class="muted">${esc(e.ts)}</td><td>${esc(e.level)}</td><td><code>${esc(e.event)}</code></td><td>${esc(e.tool || "")}</td><td>${esc(e.workspaceId || "")}</td></tr>`).join("");
|
|
131
|
+
el.innerHTML = `<div class="card"><table><tr><th>Time</th><th>Level</th><th>Event</th><th>Tool</th><th>Workspace</th></tr>${rows}</table></div>`;
|
|
132
|
+
} catch (err) { el.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async config() {
|
|
136
|
+
const el = $("config");
|
|
137
|
+
try {
|
|
138
|
+
const c = await api("/dashboard/api/config");
|
|
139
|
+
const opt = (v, cur) => `<option value="${v}"${v === cur ? " selected" : ""}>${v}</option>`;
|
|
140
|
+
el.innerHTML = `
|
|
141
|
+
<div class="card"><h3>Execution</h3>
|
|
142
|
+
<div class="row"><label>Mode <select id="cfg-mode">${opt("readonly", c.execution.mode)}${opt("supervised", c.execution.mode)}${opt("autonomous", c.execution.mode)}</select></label>
|
|
143
|
+
<label>Sandbox <select id="cfg-sandbox">${opt("auto", c.execution.sandbox)}${opt("none", c.execution.sandbox)}</select></label></div>
|
|
144
|
+
<div class="row"><label><input type="checkbox" id="cfg-reqsandbox" ${c.execution.requireSandboxForAutonomous ? "checked" : ""}> require sandbox for autonomous</label>
|
|
145
|
+
<button class="primary" id="cfg-save-exec">Save</button></div>
|
|
146
|
+
<p class="muted">Saving restarts nothing: restart <code>hearth serve</code> to apply.</p></div>
|
|
147
|
+
<div class="card"><h3>Fleet lanes (JSON)</h3>
|
|
148
|
+
<textarea id="cfg-lanes" rows="8" cols="60">${esc(JSON.stringify(c.fleet?.lanes ?? {}, null, 2))}</textarea>
|
|
149
|
+
<div class="row"><button class="primary" id="cfg-save-lanes">Save lanes</button></div></div>
|
|
150
|
+
<div class="card"><h3>Full safe config</h3><pre>${esc(JSON.stringify(c, null, 2))}</pre></div>
|
|
151
|
+
<p id="cfg-msg"></p>`;
|
|
152
|
+
const msg = (t, bad = false) => { const m = $("cfg-msg"); m.textContent = t; m.className = bad ? "error" : "good"; };
|
|
153
|
+
$("cfg-save-exec").addEventListener("click", async () => {
|
|
154
|
+
try {
|
|
155
|
+
await api("/dashboard/api/config", { method: "PUT", body: JSON.stringify({ path: ["execution", "mode"], value: $("cfg-mode").value }) });
|
|
156
|
+
await api("/dashboard/api/config", { method: "PUT", body: JSON.stringify({ path: ["execution", "sandbox"], value: $("cfg-sandbox").value }) });
|
|
157
|
+
await api("/dashboard/api/config", { method: "PUT", body: JSON.stringify({ path: ["execution", "requireSandboxForAutonomous"], value: $("cfg-reqsandbox").checked }) });
|
|
158
|
+
msg("Saved. Restart the server to apply.");
|
|
159
|
+
} catch (e) { msg(e.message, true); }
|
|
160
|
+
});
|
|
161
|
+
$("cfg-save-lanes").addEventListener("click", async () => {
|
|
162
|
+
try {
|
|
163
|
+
const lanes = JSON.parse($("cfg-lanes").value);
|
|
164
|
+
await api("/dashboard/api/config", { method: "PUT", body: JSON.stringify({ path: ["fleet"], value: { lanes } }) });
|
|
165
|
+
msg("Lanes saved. Restart the server to apply.");
|
|
166
|
+
} catch (e) { msg(e.message, true); }
|
|
167
|
+
});
|
|
168
|
+
} catch (err) { el.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
async setup() {
|
|
172
|
+
const el = $("setup");
|
|
173
|
+
try {
|
|
174
|
+
const s = await api("/dashboard/api/setup");
|
|
175
|
+
const providers = (s.providers || []).map((p) => `<tr><td>${esc(p.id)}</td><td>${p.enabled ? "yes" : "no"}</td><td>${p.available ? "<span class='good'>ready</span>" : "<span class='muted'>missing</span>"}</td></tr>`).join("");
|
|
176
|
+
el.innerHTML = `
|
|
177
|
+
<div class="card"><h3>Installation checklist</h3><table>
|
|
178
|
+
<tr><td>This machine</td><td><code>${esc(s.machineId || "unknown")}</code> <span class="muted">(<code>hearth id</code>)</span></td></tr>
|
|
179
|
+
<tr><td>Allowed roots configured</td><td>${s.hasAllowedRoots ? "<span class='good'>yes</span>" : "<span class='error'>no — run hearth init or add roots</span>"}</td></tr>
|
|
180
|
+
<tr><td>Public base URL</td><td><code>${esc(s.publicBaseUrl)}</code></td></tr>
|
|
181
|
+
<tr><td>Public MCP URL</td><td><code>${esc(s.publicMcpUrl || `${s.publicBaseUrl}/mcp`)}</code></td></tr>
|
|
182
|
+
<tr><td>Tool mode</td><td><code>${esc(s.toolMode || "")}</code></td></tr>
|
|
183
|
+
<tr><td>Redirect hosts</td><td><code>${esc((s.redirectHosts || []).join(", "))}</code></td></tr>
|
|
184
|
+
<tr><td>Subagents enabled</td><td>${s.subagentsEnabled ? "yes" : "no"}</td></tr>
|
|
185
|
+
<tr><td>MCP endpoint</td><td><code>${esc(s.mcpEndpoint)}</code></td></tr>
|
|
186
|
+
<tr><td>Health endpoint</td><td><code>${esc(s.healthEndpoint)}</code></td></tr>
|
|
187
|
+
</table></div>
|
|
188
|
+
<div class="card"><h3>Agent backends</h3><table><tr><th>ID</th><th>Enabled</th><th>Status</th></tr>${providers}</table>
|
|
189
|
+
<p class="muted">Missing CLIs are optional — delegation to them fails loud with install hints. opencode needs a model allow-list from you (see its skill docs) so you never get a surprise bill.</p></div>
|
|
190
|
+
<div class="card"><h3>Fleet lanes</h3><pre>${esc((s.fleetLanes || []).join("\n") || "none configured — pass target directly or add fleet.lanes in config")}</pre></div>`;
|
|
191
|
+
} catch (err) { el.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async connect() {
|
|
195
|
+
const el = $("connect");
|
|
196
|
+
try {
|
|
197
|
+
const s = await api("/dashboard/api/setup");
|
|
198
|
+
const mcpUrl = s.publicMcpUrl || `${s.publicBaseUrl}/mcp`;
|
|
199
|
+
el.innerHTML = `
|
|
200
|
+
<div class="card"><h3>This PC</h3>
|
|
201
|
+
<div>Machine <code>${esc(s.machineId || "unknown")}</code> (diagnostic label — keep one public URL per PC; the label does not prevent cloning).</div>
|
|
202
|
+
<div>Public MCP URL: <code>${esc(mcpUrl)}</code></div>
|
|
203
|
+
<div class="muted">Local: <code>${esc(s.mcpEndpoint)}</code> · health: <code>${esc(s.healthEndpoint)}</code> · tool mode: <code>${esc(s.toolMode || "")}</code></div></div>
|
|
204
|
+
<div class="card"><h3>ChatGPT</h3>
|
|
205
|
+
<ol><li>Keep <code>hearth serve</code> running; tunnel must proxy the whole origin, not only <code>/mcp</code>.</li>
|
|
206
|
+
<li>Add connector URL <code>${esc(mcpUrl)}</code>.</li>
|
|
207
|
+
<li>Approve with the Owner password (<code>~/.hearth/auth.json</code>).</li></ol></div>
|
|
208
|
+
<div class="card"><h3>Claude</h3>
|
|
209
|
+
<ol><li>Redirect hosts now: <code>${esc((s.redirectHosts || []).join(", "))}</code> (needs <code>claude.ai</code>).</li>
|
|
210
|
+
<li>Add MCP server <code>${esc(mcpUrl)}</code>, approve with Owner password.</li>
|
|
211
|
+
<li>Local-only alternative: <code>hearth token create claude-local</code> + <code>hearth mcp</code> as stdio.</li></ol></div>
|
|
212
|
+
<div class="card"><h3>Any MCP client</h3>
|
|
213
|
+
<div>Remote OAuth: <code>${esc(mcpUrl)}</code></div>
|
|
214
|
+
<div>Local stdio: <code>hearth mcp</code> (device token via <code>hearth token create <name></code> for bearer).</div>
|
|
215
|
+
<p class="muted">CLI equivalent: <code>hearth connect [chatgpt|claude|generic]</code>.</p></div>`;
|
|
216
|
+
} catch (err) { el.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
(async () => {
|
|
221
|
+
try {
|
|
222
|
+
const me = await api("/dashboard/api/me");
|
|
223
|
+
if (me.authenticated) showApp();
|
|
224
|
+
else showLogin();
|
|
225
|
+
} catch { showLogin(); }
|
|
226
|
+
})();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Hearth Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="/dashboard/styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<h1>Hearth</h1>
|
|
12
|
+
<nav id="tabs" hidden>
|
|
13
|
+
<button data-tab="overview" class="active">Overview</button>
|
|
14
|
+
<button data-tab="connect">Connect</button>
|
|
15
|
+
<button data-tab="workspaces">Workspaces</button>
|
|
16
|
+
<button data-tab="agents">Agents</button>
|
|
17
|
+
<button data-tab="tasks">Tasks</button>
|
|
18
|
+
<button data-tab="logs">Logs</button>
|
|
19
|
+
<button data-tab="config">Config</button>
|
|
20
|
+
<button data-tab="setup">Setup</button>
|
|
21
|
+
</nav>
|
|
22
|
+
<button id="logout" hidden>Log out</button>
|
|
23
|
+
</header>
|
|
24
|
+
<main>
|
|
25
|
+
<section id="login">
|
|
26
|
+
<h2>Sign in</h2>
|
|
27
|
+
<p>Enter the Owner password printed by <code>hearth init</code>.</p>
|
|
28
|
+
<form id="login-form">
|
|
29
|
+
<input id="owner-token" type="password" autocomplete="current-password" placeholder="Owner password" required>
|
|
30
|
+
<button type="submit">Sign in</button>
|
|
31
|
+
</form>
|
|
32
|
+
<p id="login-error" class="error" hidden></p>
|
|
33
|
+
</section>
|
|
34
|
+
<section id="overview" class="tab" hidden></section>
|
|
35
|
+
<section id="connect" class="tab" hidden></section>
|
|
36
|
+
<section id="workspaces" class="tab" hidden></section>
|
|
37
|
+
<section id="agents" class="tab" hidden></section>
|
|
38
|
+
<section id="tasks" class="tab" hidden></section>
|
|
39
|
+
<section id="logs" class="tab" hidden></section>
|
|
40
|
+
<section id="config" class="tab" hidden></section>
|
|
41
|
+
<section id="setup" class="tab" hidden></section>
|
|
42
|
+
</main>
|
|
43
|
+
<script src="/dashboard/app.js"></script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Hearth — local AI engineering runtime</title>
|
|
7
|
+
<link rel="stylesheet" href="/dashboard/styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Hearth</h1>
|
|
12
|
+
<p>Your computer as a local-first AI software engineering runtime, exposed
|
|
13
|
+
to ChatGPT, Claude, and other MCP-compatible clients. You run the server,
|
|
14
|
+
the execution environment, and the workspace authority.</p>
|
|
15
|
+
|
|
16
|
+
<div class="card">
|
|
17
|
+
<h3>Install</h3>
|
|
18
|
+
<p>Install once, then <code>hearth</code> works like <code>opencode</code>:</p>
|
|
19
|
+
<pre>npm install -g @sunshinelife83/hearth
|
|
20
|
+
hearth init
|
|
21
|
+
hearth serve</pre>
|
|
22
|
+
<p class="muted">Then <code>hearth connect</code> prints per-host steps, and <code>hearth doctor</code> checks Node, Git, Bash, URLs, and hosts.</p>
|
|
23
|
+
<p class="muted">From a source checkout: <code>./install.sh</code>, then <code>hearth init</code>. Requires Node 22.19+.</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="card">
|
|
27
|
+
<h3>Use it</h3>
|
|
28
|
+
<ol>
|
|
29
|
+
<li>Run <code>hearth serve</code> (or <code>hearth mcp</code> for local stdio).</li>
|
|
30
|
+
<li>Open the <a href="/dashboard">local dashboard</a> and sign in with your Owner password.</li>
|
|
31
|
+
<li>Run <code>hearth connect</code> and paste the public <code>/mcp</code> URL into your MCP client (via your own tunnel for remote clients).</li>
|
|
32
|
+
<li>Ask it to open a project, plan a task, delegate to a coding agent, verify, land.</li>
|
|
33
|
+
</ol>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="card">
|
|
37
|
+
<h3>Connect</h3>
|
|
38
|
+
<p>Each PC has a stable <code>hearth-xxxx</code> diagnostic label (<code>hearth id</code>). Keep one public URL per PC — OAuth tokens are bound to that machine's resource URL. The label is not a security boundary: copying the state directory copies the identity.</p>
|
|
39
|
+
<ul>
|
|
40
|
+
<li><strong>ChatGPT:</strong> add the public <code>/mcp</code> URL as a connector, approve with the Owner password.</li>
|
|
41
|
+
<li><strong>Claude:</strong> same public <code>/mcp</code> URL; ensure <code>oauth.allowedRedirectHosts</code> includes <code>claude.ai</code>. Or <code>hearth token create claude-local</code> + <code>hearth mcp</code> for local stdio.</li>
|
|
42
|
+
<li><strong>Any MCP client:</strong> remote OAuth via public <code>/mcp</code>, or local <code>hearth mcp</code> with a device token bearer.</li>
|
|
43
|
+
</ul>
|
|
44
|
+
<p class="muted">Tip: <code>hearth connect chatgpt|claude|generic</code> prints the exact snippet for this machine. <a href="/healthz">/healthz</a> reports this machine's id and MCP URL.</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="card">
|
|
48
|
+
<h3>Endpoints</h3>
|
|
49
|
+
<table>
|
|
50
|
+
<tr><td><a href="/dashboard">/dashboard</a></td><td>Local ops console (Owner sign-in)</td></tr>
|
|
51
|
+
<tr><td><code>/mcp</code></td><td>MCP endpoint (OAuth bearer)</td></tr>
|
|
52
|
+
<tr><td><a href="/healthz">/healthz</a></td><td>Liveness probe</td></tr>
|
|
53
|
+
</table>
|
|
54
|
+
</div>
|
|
55
|
+
</main>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
:root { color-scheme: dark; --bg: #14171c; --panel: #1d2129; --text: #e6e9ee; --muted: #9aa3b2; --accent: #6aa5ff; --bad: #ff7a7a; --good: #7ddf9a; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.5 system-ui, sans-serif; }
|
|
4
|
+
header { display: flex; align-items: center; gap: 1rem; padding: 0.6rem 1rem; background: var(--panel); position: sticky; top: 0; }
|
|
5
|
+
header h1 { font-size: 1.1rem; margin: 0; }
|
|
6
|
+
nav { display: flex; gap: 0.25rem; flex-wrap: wrap; }
|
|
7
|
+
nav button, #logout { background: none; border: 1px solid transparent; color: var(--text); padding: 0.35rem 0.7rem; border-radius: 6px; cursor: pointer; }
|
|
8
|
+
nav button.active { border-color: var(--accent); }
|
|
9
|
+
nav button:hover, #logout:hover { background: #2a303b; }
|
|
10
|
+
#logout { margin-left: auto; }
|
|
11
|
+
main { padding: 1rem; max-width: 1100px; margin: 0 auto; }
|
|
12
|
+
.card { background: var(--panel); border-radius: 8px; padding: 0.8rem 1rem; margin-bottom: 0.8rem; }
|
|
13
|
+
.muted { color: var(--muted); }
|
|
14
|
+
.error { color: var(--bad); }
|
|
15
|
+
.good { color: var(--good); }
|
|
16
|
+
table { border-collapse: collapse; width: 100%; }
|
|
17
|
+
th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid #2a303b; vertical-align: top; }
|
|
18
|
+
code, pre { font-family: ui-monospace, monospace; }
|
|
19
|
+
pre { white-space: pre-wrap; word-break: break-word; background: #10131a; padding: 0.6rem; border-radius: 6px; max-height: 320px; overflow: auto; }
|
|
20
|
+
input, select, textarea { background: #10131a; color: var(--text); border: 1px solid #2a303b; border-radius: 6px; padding: 0.4rem 0.6rem; }
|
|
21
|
+
button.primary { background: var(--accent); border: none; color: #0b0e13; padding: 0.4rem 0.9rem; border-radius: 6px; cursor: pointer; }
|
|
22
|
+
.row { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.6rem; }
|
|
23
|
+
.pill { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px; background: #2a303b; font-size: 0.8rem; }
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual, createHmac } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import * as z from "zod/v4";
|
|
6
|
+
import { createLocalAgentClient } from "./local-agent-client.js";
|
|
7
|
+
import { listSnapshots } from "./snapshot-manager.js";
|
|
8
|
+
import { probeSandboxAdapter } from "./policy/sandbox.js";
|
|
9
|
+
import { hearthVersion } from "./app-version.js";
|
|
10
|
+
import { recentLogEntries, logEvent } from "./logger.js";
|
|
11
|
+
import { setHearthConfigValue } from "./user-config.js";
|
|
12
|
+
import { RateLimiter, ipRateLimitMiddleware } from "./rate-limit.js";
|
|
13
|
+
import { fleetConfigSchema } from "./orchestration/lanes.js";
|
|
14
|
+
import { loadMachineIdentity } from "./machine-id.js";
|
|
15
|
+
const SESSION_TTL_MS = 12 * 3600 * 1000;
|
|
16
|
+
const SESSION_COOKIE = "hearth_session";
|
|
17
|
+
function safeEqual(a, b) {
|
|
18
|
+
const aBuf = Buffer.from(a, "utf8");
|
|
19
|
+
const bBuf = Buffer.from(b, "utf8");
|
|
20
|
+
if (aBuf.length !== bBuf.length)
|
|
21
|
+
return false;
|
|
22
|
+
return timingSafeEqual(aBuf, bBuf);
|
|
23
|
+
}
|
|
24
|
+
export function dashboardDirectory() {
|
|
25
|
+
const dist = fileURLToPath(new URL("../dist/dashboard", import.meta.url));
|
|
26
|
+
if (existsSync(dist))
|
|
27
|
+
return dist;
|
|
28
|
+
return fileURLToPath(new URL("./dashboard", import.meta.url));
|
|
29
|
+
}
|
|
30
|
+
const configEditSchema = z.object({
|
|
31
|
+
path: z.array(z.union([z.string(), z.number()])).min(1).max(4),
|
|
32
|
+
value: z.unknown(),
|
|
33
|
+
});
|
|
34
|
+
const ALLOWED_CONFIG_PATHS = [
|
|
35
|
+
{ path: ["execution", "mode"], validate: (v) => z.enum(["readonly", "supervised", "autonomous"]).parse(v) },
|
|
36
|
+
{ path: ["execution", "sandbox"], validate: (v) => z.enum(["auto", "none"]).parse(v) },
|
|
37
|
+
{ path: ["execution", "sandboxNetwork"], validate: (v) => z.enum(["allow", "deny"]).parse(v) },
|
|
38
|
+
{ path: ["execution", "requireSandboxForAutonomous"], validate: (v) => z.boolean().parse(v) },
|
|
39
|
+
{ path: ["fleet"], validate: (v) => fleetConfigSchema.parse(v) },
|
|
40
|
+
{ path: ["workspaces", "profiles"], validate: (v) => z.array(z.record(z.string(), z.unknown())).parse(v) },
|
|
41
|
+
];
|
|
42
|
+
export function registerDashboard(app, context) {
|
|
43
|
+
const { config, workspaces, taskStore } = context;
|
|
44
|
+
const sessionSecret = randomBytes(32);
|
|
45
|
+
const sessions = new Map();
|
|
46
|
+
const loginLimiter = new RateLimiter({
|
|
47
|
+
keyPrefix: "dashboard-login",
|
|
48
|
+
rule: { limit: 10, windowMs: 15 * 60 * 1000 },
|
|
49
|
+
});
|
|
50
|
+
const signSession = (id) => `${id}.${createHmac("sha256", sessionSecret).update(id).digest("hex")}`;
|
|
51
|
+
const readSession = (req) => {
|
|
52
|
+
const cookie = req.headers.cookie
|
|
53
|
+
?.split(";")
|
|
54
|
+
.map((part) => part.trim())
|
|
55
|
+
.find((part) => part.startsWith(`${SESSION_COOKIE}=`))
|
|
56
|
+
?.slice(SESSION_COOKIE.length + 1);
|
|
57
|
+
if (!cookie)
|
|
58
|
+
return undefined;
|
|
59
|
+
const dot = cookie.lastIndexOf(".");
|
|
60
|
+
if (dot <= 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
const id = cookie.slice(0, dot);
|
|
63
|
+
const signature = cookie.slice(dot + 1);
|
|
64
|
+
const expected = createHmac("sha256", sessionSecret).update(id).digest("hex");
|
|
65
|
+
if (!safeEqual(signature, expected))
|
|
66
|
+
return undefined;
|
|
67
|
+
const expires = sessions.get(id);
|
|
68
|
+
if (!expires || expires < Date.now()) {
|
|
69
|
+
sessions.delete(id);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return id;
|
|
73
|
+
};
|
|
74
|
+
const requireSession = (req, res, next) => {
|
|
75
|
+
if (!readSession(req)) {
|
|
76
|
+
res.status(401).json({ error: "Dashboard session required. POST /dashboard/api/login first." });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
next();
|
|
80
|
+
};
|
|
81
|
+
const requireJson = (req, res, next) => {
|
|
82
|
+
if (!req.is("application/json")) {
|
|
83
|
+
res.status(415).json({ error: "JSON content type required." });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
next();
|
|
87
|
+
};
|
|
88
|
+
const configEnv = { ...process.env, HEARTH_CONFIG_DIR: config.configDir };
|
|
89
|
+
app.post("/dashboard/api/login", ipRateLimitMiddleware(loginLimiter, config.oauth.trustProxy), express.json({ limit: "16kb" }), (req, res) => {
|
|
90
|
+
const parsed = z.object({ ownerToken: z.string().min(1).max(500) }).safeParse(req.body);
|
|
91
|
+
if (!parsed.success || !safeEqual(parsed.data.ownerToken, config.oauth.ownerToken)) {
|
|
92
|
+
logEvent(config.logging, "warn", "dashboard_login_denied", {});
|
|
93
|
+
res.status(401).json({ error: "Invalid owner password." });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const id = randomBytes(16).toString("hex");
|
|
97
|
+
sessions.set(id, Date.now() + SESSION_TTL_MS);
|
|
98
|
+
res.cookie(SESSION_COOKIE, signSession(id), {
|
|
99
|
+
httpOnly: true,
|
|
100
|
+
sameSite: "lax",
|
|
101
|
+
path: "/",
|
|
102
|
+
maxAge: SESSION_TTL_MS,
|
|
103
|
+
});
|
|
104
|
+
logEvent(config.logging, "info", "dashboard_login", {});
|
|
105
|
+
res.json({ ok: true });
|
|
106
|
+
});
|
|
107
|
+
app.post("/dashboard/api/logout", express.json({ limit: "1kb" }), (req, res) => {
|
|
108
|
+
const id = readSession(req);
|
|
109
|
+
if (id)
|
|
110
|
+
sessions.delete(id);
|
|
111
|
+
res.clearCookie(SESSION_COOKIE, { path: "/" });
|
|
112
|
+
res.json({ ok: true });
|
|
113
|
+
});
|
|
114
|
+
app.get("/dashboard/api/me", (req, res) => {
|
|
115
|
+
res.json({ authenticated: readSession(req) !== undefined });
|
|
116
|
+
});
|
|
117
|
+
app.get("/dashboard/api/status", requireSession, (_req, res) => {
|
|
118
|
+
const tasks = taskStore?.list() ?? [];
|
|
119
|
+
const byStatus = {};
|
|
120
|
+
for (const task of tasks)
|
|
121
|
+
byStatus[task.status] = (byStatus[task.status] ?? 0) + 1;
|
|
122
|
+
res.json({
|
|
123
|
+
version: hearthVersion(),
|
|
124
|
+
mode: config.execution.mode,
|
|
125
|
+
sandbox: config.execution.sandbox,
|
|
126
|
+
sandboxAdapter: probeSandboxAdapter(),
|
|
127
|
+
requireSandboxForAutonomous: config.execution.requireSandboxForAutonomous,
|
|
128
|
+
toolMode: config.toolMode,
|
|
129
|
+
publicBaseUrl: config.publicBaseUrl,
|
|
130
|
+
providers: context.resolveLocalAgentProviders(),
|
|
131
|
+
workspaces: workspaces.listWorkspaces(),
|
|
132
|
+
tasks: byStatus,
|
|
133
|
+
lanes: config.fleet.lanes,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
app.get("/dashboard/api/workspaces", requireSession, (_req, res) => {
|
|
137
|
+
res.json({
|
|
138
|
+
open: workspaces.listWorkspaces(),
|
|
139
|
+
allowedRoots: config.allowedRoots,
|
|
140
|
+
profiles: config.workspaceProfiles,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
app.get("/dashboard/api/tasks", requireSession, (req, res) => {
|
|
144
|
+
const limit = Math.min(Math.max(1, Number(req.query.limit ?? 50)), 200);
|
|
145
|
+
res.json({ tasks: (taskStore?.list() ?? []).slice(0, limit) });
|
|
146
|
+
});
|
|
147
|
+
app.get("/dashboard/api/agents", requireSession, async (req, res) => {
|
|
148
|
+
const workspaceId = String(req.query.workspaceId ?? "");
|
|
149
|
+
if (!workspaceId) {
|
|
150
|
+
res.status(400).json({ error: "workspaceId query parameter required." });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
155
|
+
const client = createLocalAgentClient(config);
|
|
156
|
+
const result = await client.list({ workspaceId, workspaceRoot: workspace.root });
|
|
157
|
+
if (result.isErr()) {
|
|
158
|
+
res.status(502).json({ error: `${result.error.code}: ${result.error.message}` });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
res.json({ agents: result.value });
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
res.status(404).json({ error: error instanceof Error ? error.message : String(error) });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
app.get("/dashboard/api/snapshots", requireSession, async (req, res) => {
|
|
168
|
+
const workspaceId = String(req.query.workspaceId ?? "");
|
|
169
|
+
if (!workspaceId) {
|
|
170
|
+
res.status(400).json({ error: "workspaceId query parameter required." });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
175
|
+
res.json({ snapshots: await listSnapshots({ root: workspace.root, workspaceId }) });
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
res.status(404).json({ error: error instanceof Error ? error.message : String(error) });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
app.get("/dashboard/api/logs", requireSession, (req, res) => {
|
|
182
|
+
const limit = Math.min(Math.max(1, Number(req.query.limit ?? 100)), 200);
|
|
183
|
+
res.json({ entries: recentLogEntries(limit) });
|
|
184
|
+
});
|
|
185
|
+
app.get("/dashboard/api/setup", requireSession, (_req, res) => {
|
|
186
|
+
const providers = context.resolveLocalAgentProviders();
|
|
187
|
+
let machineId;
|
|
188
|
+
try {
|
|
189
|
+
machineId = loadMachineIdentity(config.stateDir).id;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
machineId = undefined;
|
|
193
|
+
}
|
|
194
|
+
res.json({
|
|
195
|
+
machineId,
|
|
196
|
+
allowedRoots: config.allowedRoots,
|
|
197
|
+
hasAllowedRoots: config.allowedRoots.length > 0,
|
|
198
|
+
publicBaseUrl: config.publicBaseUrl,
|
|
199
|
+
publicMcpUrl: new URL("/mcp", config.publicBaseUrl).toString(),
|
|
200
|
+
toolMode: config.toolMode,
|
|
201
|
+
redirectHosts: config.oauth.allowedRedirectHosts,
|
|
202
|
+
subagentsEnabled: config.subagents.enabled,
|
|
203
|
+
providers: providers.map((provider) => ({
|
|
204
|
+
id: provider.id,
|
|
205
|
+
enabled: provider.enabled,
|
|
206
|
+
available: provider.available,
|
|
207
|
+
})),
|
|
208
|
+
fleetLanes: Object.keys(config.fleet.lanes),
|
|
209
|
+
healthEndpoint: "/healthz",
|
|
210
|
+
mcpEndpoint: "/mcp",
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
app.get("/dashboard/api/config", requireSession, (_req, res) => {
|
|
214
|
+
res.json({
|
|
215
|
+
execution: {
|
|
216
|
+
mode: config.execution.mode,
|
|
217
|
+
sandbox: config.execution.sandbox,
|
|
218
|
+
sandboxNetwork: config.execution.sandboxNetwork,
|
|
219
|
+
requireSandboxForAutonomous: config.execution.requireSandboxForAutonomous,
|
|
220
|
+
},
|
|
221
|
+
toolMode: config.toolMode,
|
|
222
|
+
publicBaseUrl: config.publicBaseUrl,
|
|
223
|
+
allowedRoots: config.allowedRoots,
|
|
224
|
+
fleet: config.fleet,
|
|
225
|
+
subagents: {
|
|
226
|
+
enabled: config.subagents.enabled,
|
|
227
|
+
providers: config.subagents.providers.map((provider) => ({
|
|
228
|
+
id: provider.id,
|
|
229
|
+
enabled: provider.enabled,
|
|
230
|
+
...(provider.model ? { model: provider.model } : {}),
|
|
231
|
+
...(provider.effort ? { effort: provider.effort } : {}),
|
|
232
|
+
})),
|
|
233
|
+
},
|
|
234
|
+
workspaceProfiles: config.workspaceProfiles,
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
app.put("/dashboard/api/config", requireSession, requireJson, express.json({ limit: "64kb" }), (req, res) => {
|
|
238
|
+
const parsed = configEditSchema.safeParse(req.body);
|
|
239
|
+
if (!parsed.success) {
|
|
240
|
+
res.status(400).json({ error: "Body must be { path: [...], value }." });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const allowed = ALLOWED_CONFIG_PATHS.find((entry) => entry.path.length === parsed.data.path.length
|
|
244
|
+
&& entry.path.every((segment, index) => segment === parsed.data.path[index]));
|
|
245
|
+
if (!allowed) {
|
|
246
|
+
res.status(403).json({ error: "Config path is not editable from the dashboard." });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
let value;
|
|
250
|
+
try {
|
|
251
|
+
value = allowed.validate(parsed.data.value);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
setHearthConfigValue(allowed.path, value, configEnv);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
logEvent(config.logging, "info", "dashboard_config_edit", { path: allowed.path.join(".") });
|
|
265
|
+
res.json({ ok: true, restartRequired: true });
|
|
266
|
+
});
|
|
267
|
+
app.get("/dashboard", (_req, res) => {
|
|
268
|
+
res.sendFile("index.html", { root: dashboardDirectory() });
|
|
269
|
+
});
|
|
270
|
+
app.use("/dashboard", express.static(dashboardDirectory(), { fallthrough: false }));
|
|
271
|
+
}
|