@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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import * as prompts from "@clack/prompts";
|
|
6
|
+
import { getShellConfig } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { satisfies } from "semver";
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
9
|
+
import { resolveCliWorkspaceContext } from "./cli-workspace.js";
|
|
10
|
+
import { getLocalAgentProviderAvailabilitySnapshot, } from "./local-agent-availability.js";
|
|
11
|
+
import { buildLocalAgentCatalog, buildLocalAgentProviderStatuses, formatLocalAgentProviderStatusSummary, } from "./local-agent-catalog.js";
|
|
12
|
+
import { loadLocalAgentProfiles } from "./local-agent-profiles.js";
|
|
13
|
+
import { parseLocalAgentContinueArgs, parseLocalAgentRunArgs, } from "./local-agent-targets.js";
|
|
14
|
+
import { createLocalAgentClient } from "./local-agent-client.js";
|
|
15
|
+
import { toAgentErrorPayload } from "./local-agent-errors.js";
|
|
16
|
+
import { formatAgentObservation, formatAgentReceipt, formatAgentSummary, formatAgentTargetCatalog, presentAgentObservation, presentAgentReceipt, presentAgentSummary, presentAgentTargetCatalog, } from "./local-agent-presentation.js";
|
|
17
|
+
import { SUBAGENT_SKILL_INSTALL_COMMAND, resolveOnboardingUsage, updateOnboardingSubagentsConfig, usesChatGpt, usesCodingAgents, } from "./onboarding.js";
|
|
18
|
+
import { generateOwnerToken, loadHearthFiles, setHearthConfigValue, setHearthConfigValues, writeHearthAuth, } from "./user-config.js";
|
|
19
|
+
import { expandHomePath } from "./roots.js";
|
|
20
|
+
import { readReviewRef } from "./review-checkpoints.js";
|
|
21
|
+
import { shutdownHttpServer } from "./server-shutdown.js";
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
// Keep in sync with "engines.node" in package.json and the documented range.
|
|
24
|
+
const SUPPORTED_NODE_RANGE = ">=22.19 <27";
|
|
25
|
+
async function main(argv) {
|
|
26
|
+
assertSupportedNode();
|
|
27
|
+
const [rawCommand, ...args] = argv;
|
|
28
|
+
const command = normalizeCommand(rawCommand);
|
|
29
|
+
switch (command) {
|
|
30
|
+
case "serve":
|
|
31
|
+
await ensureConfigured();
|
|
32
|
+
await serve();
|
|
33
|
+
return;
|
|
34
|
+
case "mcp":
|
|
35
|
+
await runMcp();
|
|
36
|
+
return;
|
|
37
|
+
case "token":
|
|
38
|
+
await runTokenCommand(args);
|
|
39
|
+
return;
|
|
40
|
+
case "init":
|
|
41
|
+
await runInit(parseInitArgs(args));
|
|
42
|
+
return;
|
|
43
|
+
case "doctor":
|
|
44
|
+
await runDoctor(parseDoctorArgs(args));
|
|
45
|
+
return;
|
|
46
|
+
case "config":
|
|
47
|
+
runConfigCommand(args);
|
|
48
|
+
return;
|
|
49
|
+
case "agents":
|
|
50
|
+
await runAgentsCommand(args);
|
|
51
|
+
return;
|
|
52
|
+
case "expose":
|
|
53
|
+
await runExpose();
|
|
54
|
+
return;
|
|
55
|
+
case "id":
|
|
56
|
+
runMachineId();
|
|
57
|
+
return;
|
|
58
|
+
case "connect":
|
|
59
|
+
await runConnect(args);
|
|
60
|
+
return;
|
|
61
|
+
case "show-changes":
|
|
62
|
+
await runShowChanges(args);
|
|
63
|
+
return;
|
|
64
|
+
case "help":
|
|
65
|
+
printHelp();
|
|
66
|
+
return;
|
|
67
|
+
case "version":
|
|
68
|
+
printVersion();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function normalizeCommand(command) {
|
|
73
|
+
if (!command || command === "serve" || command === "start")
|
|
74
|
+
return "serve";
|
|
75
|
+
if (command === "init"
|
|
76
|
+
|| command === "mcp"
|
|
77
|
+
|| command === "token"
|
|
78
|
+
|| command === "doctor"
|
|
79
|
+
|| command === "config"
|
|
80
|
+
|| command === "agents"
|
|
81
|
+
|| command === "show-changes"
|
|
82
|
+
|| command === "expose"
|
|
83
|
+
|| command === "connect"
|
|
84
|
+
|| command === "id")
|
|
85
|
+
return command;
|
|
86
|
+
if (command === "help" || command === "--help" || command === "-h")
|
|
87
|
+
return "help";
|
|
88
|
+
if (command === "version" || command === "--version" || command === "-v")
|
|
89
|
+
return "version";
|
|
90
|
+
throw new Error(`Unknown command: ${command}`);
|
|
91
|
+
}
|
|
92
|
+
async function ensureConfigured() {
|
|
93
|
+
const files = loadHearthFiles();
|
|
94
|
+
if (files.migratedLegacyConfig) {
|
|
95
|
+
console.log(`Migrated legacy configuration to ${files.configPath}`);
|
|
96
|
+
}
|
|
97
|
+
if (files.migratedFromDevspace) {
|
|
98
|
+
console.log(`Adopted your pre-rename DevSpace install into ${files.dir} (originals kept).`);
|
|
99
|
+
}
|
|
100
|
+
if (files.configExists && files.authExists)
|
|
101
|
+
return;
|
|
102
|
+
if (process.env.HEARTH_OAUTH_OWNER_TOKEN)
|
|
103
|
+
return;
|
|
104
|
+
if (!input.isTTY || !output.isTTY) {
|
|
105
|
+
throw new Error([
|
|
106
|
+
"Hearth is not configured and this terminal is non-interactive.",
|
|
107
|
+
"",
|
|
108
|
+
"Run:",
|
|
109
|
+
" hearth init",
|
|
110
|
+
"",
|
|
111
|
+
"Or provide HEARTH_OAUTH_OWNER_TOKEN.",
|
|
112
|
+
].join("\n"));
|
|
113
|
+
}
|
|
114
|
+
await runInit({ force: false });
|
|
115
|
+
}
|
|
116
|
+
function parseInitArgs(args) {
|
|
117
|
+
const options = { force: false, yes: false };
|
|
118
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
119
|
+
const arg = args[index];
|
|
120
|
+
if (arg === "--force")
|
|
121
|
+
options.force = true;
|
|
122
|
+
else if (arg === "--yes" || arg === "-y")
|
|
123
|
+
options.yes = true;
|
|
124
|
+
else if (arg === "--roots")
|
|
125
|
+
options.roots = args[++index];
|
|
126
|
+
else if (arg.startsWith("--roots="))
|
|
127
|
+
options.roots = arg.slice("--roots=".length);
|
|
128
|
+
else if (arg === "--public-url")
|
|
129
|
+
options.publicUrl = args[++index];
|
|
130
|
+
else if (arg.startsWith("--public-url="))
|
|
131
|
+
options.publicUrl = arg.slice("--public-url=".length);
|
|
132
|
+
else if (arg === "--use")
|
|
133
|
+
options.use = args[++index];
|
|
134
|
+
else if (arg.startsWith("--use="))
|
|
135
|
+
options.use = arg.slice("--use=".length);
|
|
136
|
+
else if (arg === "--providers")
|
|
137
|
+
options.providers = args[++index];
|
|
138
|
+
else if (arg.startsWith("--providers="))
|
|
139
|
+
options.providers = arg.slice("--providers=".length);
|
|
140
|
+
else
|
|
141
|
+
throw new Error(`Unknown init option: ${arg}. Usage: hearth init [--force] [--yes] [--roots <csv>] [--public-url <url>] [--use chatgpt|coding-agents|both] [--providers <csv>]`);
|
|
142
|
+
}
|
|
143
|
+
return options;
|
|
144
|
+
}
|
|
145
|
+
function parseDoctorArgs(args) {
|
|
146
|
+
for (const arg of args) {
|
|
147
|
+
if (arg === "--fix")
|
|
148
|
+
return { fix: true };
|
|
149
|
+
if (arg === "--help" || arg === "-h") {
|
|
150
|
+
console.log("Usage:\n hearth doctor [--fix]\n\n--fix rewrites a trailing /mcp in server.publicBaseUrl to its origin.");
|
|
151
|
+
return { fix: false };
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`Unknown doctor option: ${arg}. Usage: hearth doctor [--fix]`);
|
|
154
|
+
}
|
|
155
|
+
return { fix: false };
|
|
156
|
+
}
|
|
157
|
+
function parseInitDestinations(value, fallback) {
|
|
158
|
+
if (!value)
|
|
159
|
+
return fallback;
|
|
160
|
+
const normalized = value.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
161
|
+
const destinations = [];
|
|
162
|
+
for (const part of normalized) {
|
|
163
|
+
if (part === "both")
|
|
164
|
+
return ["chatgpt", "coding-agents"];
|
|
165
|
+
if (part === "chatgpt" || part === "coding-agents") {
|
|
166
|
+
if (!destinations.includes(part))
|
|
167
|
+
destinations.push(part);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`Unknown --use value: ${value}. Use chatgpt, coding-agents, or both.`);
|
|
171
|
+
}
|
|
172
|
+
if (destinations.length === 0)
|
|
173
|
+
throw new Error(`Unknown --use value: ${value}. Use chatgpt, coding-agents, or both.`);
|
|
174
|
+
return destinations;
|
|
175
|
+
}
|
|
176
|
+
async function runInit({ force, yes, roots, publicUrl, use, providers }) {
|
|
177
|
+
const files = loadHearthFiles();
|
|
178
|
+
if (!force && files.configExists && files.authExists) {
|
|
179
|
+
prompts.log.info(`Hearth is already configured at ${files.dir}`);
|
|
180
|
+
prompts.log.info("Run `hearth init --force` to update it.");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
if (yes && !use) {
|
|
185
|
+
throw new Error("hearth init --yes requires --use chatgpt|coding-agents|both.");
|
|
186
|
+
}
|
|
187
|
+
const nonInteractive = yes || roots !== undefined || publicUrl !== undefined || use !== undefined || providers !== undefined;
|
|
188
|
+
if (nonInteractive && (!input.isTTY || !output.isTTY)) {
|
|
189
|
+
// Allow fully-flagged runs in non-TTY; fall through to flagged path.
|
|
190
|
+
}
|
|
191
|
+
if (nonInteractive && use === undefined && !yes) {
|
|
192
|
+
// Partial flags still need a destination; default preserves old behavior.
|
|
193
|
+
}
|
|
194
|
+
const fallbackDestinations = files.config.server.publicBaseUrl ? ["chatgpt"] : ["coding-agents"];
|
|
195
|
+
let destinations;
|
|
196
|
+
let rootsInput = roots;
|
|
197
|
+
let publicUrlInput = publicUrl;
|
|
198
|
+
let providersInput = providers;
|
|
199
|
+
if (nonInteractive) {
|
|
200
|
+
destinations = parseInitDestinations(use, fallbackDestinations);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
prompts.intro("Hearth setup");
|
|
204
|
+
const destinationAnswer = await prompts.multiselect({
|
|
205
|
+
message: "Where will you use Hearth?",
|
|
206
|
+
options: [
|
|
207
|
+
{
|
|
208
|
+
value: "chatgpt",
|
|
209
|
+
label: "ChatGPT",
|
|
210
|
+
hint: "Connect ChatGPT to projects on this computer.",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
value: "coding-agents",
|
|
214
|
+
label: "Coding Agents",
|
|
215
|
+
hint: "Use Hearth from Codex, Claude Code, OpenCode, Pi, and similar tools.",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
initialValues: files.config.server.publicBaseUrl ? ["chatgpt"] : ["coding-agents"],
|
|
219
|
+
required: true,
|
|
220
|
+
});
|
|
221
|
+
if (prompts.isCancel(destinationAnswer))
|
|
222
|
+
throw new SetupCancelledError();
|
|
223
|
+
destinations = destinationAnswer;
|
|
224
|
+
}
|
|
225
|
+
const usage = resolveOnboardingUsage(destinations);
|
|
226
|
+
const useChatGpt = usesChatGpt(usage);
|
|
227
|
+
const useCodingAgents = usesCodingAgents(usage);
|
|
228
|
+
let allowedRoots;
|
|
229
|
+
if (useChatGpt) {
|
|
230
|
+
const defaultRoots = files.config.workspaces.allowedRoots.join(", ") || process.cwd();
|
|
231
|
+
let rootsAnswer;
|
|
232
|
+
if (rootsInput !== undefined) {
|
|
233
|
+
rootsAnswer = rootsInput.trim() || defaultRoots;
|
|
234
|
+
if (!rootsAnswer.trim())
|
|
235
|
+
throw new Error("Enter at least one project root via --roots.");
|
|
236
|
+
}
|
|
237
|
+
else if (nonInteractive) {
|
|
238
|
+
rootsAnswer = defaultRoots;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
rootsAnswer = await textPrompt({
|
|
242
|
+
message: `Which project folders can Hearth access? Press Enter to use ${defaultRoots}`,
|
|
243
|
+
placeholder: defaultRoots,
|
|
244
|
+
defaultValue: defaultRoots,
|
|
245
|
+
validate: (value) => value?.trim() ? undefined : "Enter at least one project root.",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
allowedRoots = rootsAnswer
|
|
249
|
+
.split(",")
|
|
250
|
+
.map((root) => resolve(expandHomePath(root.trim())))
|
|
251
|
+
.filter(Boolean);
|
|
252
|
+
if (allowedRoots.length === 0)
|
|
253
|
+
throw new Error("Enter at least one project root.");
|
|
254
|
+
}
|
|
255
|
+
const port = files.config.server.port;
|
|
256
|
+
let publicBaseUrl = null;
|
|
257
|
+
if (useChatGpt) {
|
|
258
|
+
if (publicUrlInput !== undefined) {
|
|
259
|
+
const trimmed = publicUrlInput.trim() || files.config.server.publicBaseUrl || "";
|
|
260
|
+
const validation = validateRequiredPublicBaseUrl(trimmed || undefined);
|
|
261
|
+
if (validation)
|
|
262
|
+
throw new Error(validation);
|
|
263
|
+
publicBaseUrl = normalizePublicBaseUrl(trimmed);
|
|
264
|
+
}
|
|
265
|
+
else if (nonInteractive) {
|
|
266
|
+
if (!files.config.server.publicBaseUrl) {
|
|
267
|
+
throw new Error("hearth init --yes for ChatGPT requires --public-url https://your-tunnel-host.example.com (origin only, without /mcp).");
|
|
268
|
+
}
|
|
269
|
+
publicBaseUrl = normalizePublicBaseUrl(files.config.server.publicBaseUrl);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
prompts.note([
|
|
273
|
+
`Point your HTTPS tunnel or reverse proxy to http://127.0.0.1:${port}.`,
|
|
274
|
+
"Paste its public URL below.",
|
|
275
|
+
"",
|
|
276
|
+
"Example: https://your-tunnel-host.example.com",
|
|
277
|
+
].join("\n"), "Connect ChatGPT");
|
|
278
|
+
publicBaseUrl = normalizePublicBaseUrl(await textPrompt({
|
|
279
|
+
message: files.config.server.publicBaseUrl
|
|
280
|
+
? `What public URL will ChatGPT connect to? Press Enter to keep ${files.config.server.publicBaseUrl}`
|
|
281
|
+
: "What public URL will ChatGPT connect to?",
|
|
282
|
+
placeholder: files.config.server.publicBaseUrl ?? "https://your-tunnel-host.example.com",
|
|
283
|
+
defaultValue: files.config.server.publicBaseUrl ?? "",
|
|
284
|
+
validate: validateRequiredPublicBaseUrl,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const currentSubagents = files.config.subagents;
|
|
289
|
+
const availability = getLocalAgentProviderAvailabilitySnapshot();
|
|
290
|
+
const configuredProviders = currentSubagents.providers
|
|
291
|
+
.filter((provider) => provider.enabled)
|
|
292
|
+
.map((provider) => provider.id);
|
|
293
|
+
const initialValues = configuredProviders.length > 0
|
|
294
|
+
? configuredProviders
|
|
295
|
+
: availability
|
|
296
|
+
.filter((provider) => provider.available)
|
|
297
|
+
.map((provider) => provider.name);
|
|
298
|
+
let selectedProviders;
|
|
299
|
+
if (providersInput !== undefined) {
|
|
300
|
+
const wanted = providersInput.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
301
|
+
const known = new Set(availability.map((provider) => provider.name));
|
|
302
|
+
for (const name of wanted) {
|
|
303
|
+
if (!known.has(name))
|
|
304
|
+
throw new Error(`Unknown provider: ${name}.`);
|
|
305
|
+
}
|
|
306
|
+
selectedProviders = wanted;
|
|
307
|
+
}
|
|
308
|
+
else if (nonInteractive) {
|
|
309
|
+
selectedProviders = initialValues;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const providerAnswer = await prompts.multiselect({
|
|
313
|
+
message: "Which Coding Agents should be available?",
|
|
314
|
+
options: availability.map((provider) => ({
|
|
315
|
+
value: provider.name,
|
|
316
|
+
label: provider.name,
|
|
317
|
+
hint: provider.available
|
|
318
|
+
? provider.note ?? "available"
|
|
319
|
+
: `unavailable: ${provider.reason ?? "provider preflight failed"}`,
|
|
320
|
+
})),
|
|
321
|
+
initialValues,
|
|
322
|
+
required: true,
|
|
323
|
+
});
|
|
324
|
+
if (prompts.isCancel(providerAnswer))
|
|
325
|
+
throw new SetupCancelledError();
|
|
326
|
+
selectedProviders = providerAnswer;
|
|
327
|
+
}
|
|
328
|
+
const subagents = updateOnboardingSubagentsConfig(currentSubagents, selectedProviders);
|
|
329
|
+
// `init --force` rotates the owner password so "password not accepted"
|
|
330
|
+
// recovery never keeps a credential the user considers lost.
|
|
331
|
+
const auth = {
|
|
332
|
+
ownerToken: force || !files.auth.ownerToken
|
|
333
|
+
? generateOwnerToken()
|
|
334
|
+
: files.auth.ownerToken,
|
|
335
|
+
};
|
|
336
|
+
setHearthConfigValues([
|
|
337
|
+
{ path: ["server", "port"], value: port },
|
|
338
|
+
...(useChatGpt
|
|
339
|
+
? [{ path: ["server", "publicBaseUrl"], value: publicBaseUrl }]
|
|
340
|
+
: []),
|
|
341
|
+
...(allowedRoots
|
|
342
|
+
? [{ path: ["workspaces", "allowedRoots"], value: allowedRoots }]
|
|
343
|
+
: []),
|
|
344
|
+
{ path: ["subagents"], value: subagents },
|
|
345
|
+
]);
|
|
346
|
+
writeHearthAuth(auth);
|
|
347
|
+
const lines = [
|
|
348
|
+
...(allowedRoots ? [`Project folders: ${allowedRoots.join(", ")}`] : []),
|
|
349
|
+
`Coding Agents: ${selectedProviders.join(", ")}`,
|
|
350
|
+
...(publicBaseUrl ? [`ChatGPT connection URL: ${publicBaseUrl}/mcp`] : []),
|
|
351
|
+
];
|
|
352
|
+
prompts.note(lines.join("\n"), "Hearth is ready");
|
|
353
|
+
if (useChatGpt) {
|
|
354
|
+
prompts.note([
|
|
355
|
+
`Owner password: ${auth.ownerToken}`,
|
|
356
|
+
"Use this when ChatGPT asks you to approve Hearth access.",
|
|
357
|
+
].join("\n"), "Owner password");
|
|
358
|
+
}
|
|
359
|
+
if (useCodingAgents) {
|
|
360
|
+
prompts.note([
|
|
361
|
+
SUBAGENT_SKILL_INSTALL_COMMAND,
|
|
362
|
+
"",
|
|
363
|
+
"The Skills CLI will let you choose which Coding Agents receive it.",
|
|
364
|
+
].join("\n"), "Install the Subagents skill");
|
|
365
|
+
}
|
|
366
|
+
const nextSteps = [
|
|
367
|
+
useChatGpt ? "Run `hearth serve`, then `hearth connect` for per-host steps." : undefined,
|
|
368
|
+
useCodingAgents ? "Run the skill command above before delegating from your Coding Agents." : undefined,
|
|
369
|
+
].filter(Boolean).join(" ");
|
|
370
|
+
if (nonInteractive) {
|
|
371
|
+
console.log(["Hearth is ready", ...lines].join("\n"));
|
|
372
|
+
if (useChatGpt)
|
|
373
|
+
console.log(`Owner password: ${auth.ownerToken}`);
|
|
374
|
+
console.log(nextSteps);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
prompts.outro(nextSteps);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
if (error instanceof SetupCancelledError) {
|
|
382
|
+
prompts.cancel("Setup cancelled");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function runTokenCommand(args) {
|
|
389
|
+
const { loadConfig } = await import("./config.js");
|
|
390
|
+
const { DeviceTokenStore } = await import("./device-tokens.js");
|
|
391
|
+
const [sub, ...rest] = args;
|
|
392
|
+
const store = new DeviceTokenStore(loadConfig().stateDir);
|
|
393
|
+
try {
|
|
394
|
+
if (sub === "create") {
|
|
395
|
+
const name = rest[0]?.trim();
|
|
396
|
+
if (!name)
|
|
397
|
+
throw new Error("Usage: hearth token create <name>");
|
|
398
|
+
const { token, record } = store.create(name);
|
|
399
|
+
console.log(`Device token created: ${record.createdAt}`);
|
|
400
|
+
console.log(token);
|
|
401
|
+
console.log("Store it now — it is shown only once and cannot be recovered.");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (sub === "list" || sub === undefined) {
|
|
405
|
+
const tokens = store.list();
|
|
406
|
+
if (tokens.length === 0) {
|
|
407
|
+
console.log("No device tokens. Create one: hearth token create <name>");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
for (const token of tokens) {
|
|
411
|
+
console.log(`${token.name} created ${token.createdAt}${token.lastUsedAt ? ` last used ${token.lastUsedAt}` : " never used"}`);
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (sub === "revoke") {
|
|
416
|
+
const name = rest[0]?.trim();
|
|
417
|
+
if (!name)
|
|
418
|
+
throw new Error("Usage: hearth token revoke <name>");
|
|
419
|
+
if (store.revoke(name))
|
|
420
|
+
console.log(`Revoked device token: ${name}`);
|
|
421
|
+
else
|
|
422
|
+
console.log(`No device token named ${name}.`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
throw new Error("Usage: hearth token create <name> | list | revoke <name>");
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
store.close();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function runMcp() {
|
|
432
|
+
const files = loadHearthFiles();
|
|
433
|
+
if (!files.configExists || (!files.authExists && !process.env.HEARTH_OAUTH_OWNER_TOKEN)) {
|
|
434
|
+
throw new Error([
|
|
435
|
+
"Hearth is not configured.",
|
|
436
|
+
"",
|
|
437
|
+
"Run `hearth init` once, then point your local MCP client at:",
|
|
438
|
+
" hearth mcp",
|
|
439
|
+
].join("\n"));
|
|
440
|
+
}
|
|
441
|
+
const { loadConfig } = await import("./config.js");
|
|
442
|
+
const { runStdioServer } = await import("./stdio-server.js");
|
|
443
|
+
await runStdioServer(loadConfig());
|
|
444
|
+
}
|
|
445
|
+
function logServeBanner(config, localAgentProviders, scheme) {
|
|
446
|
+
let machineId = "unknown";
|
|
447
|
+
try {
|
|
448
|
+
const { loadMachineIdentity } = require("./machine-id.js");
|
|
449
|
+
machineId = loadMachineIdentity(config.stateDir).id;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// Banner must never fail because identity storage is unavailable.
|
|
453
|
+
}
|
|
454
|
+
const publicMcpUrl = new URL("/mcp", config.publicBaseUrl).toString();
|
|
455
|
+
console.log(`hearth listening on ${scheme}://${config.host}:${config.port}/mcp`);
|
|
456
|
+
console.log(`public MCP URL: ${publicMcpUrl}`);
|
|
457
|
+
console.log(`machine: ${machineId} (hearth id, diagnostic label only — not a security boundary)`);
|
|
458
|
+
console.log(`dashboard: ${scheme}://${config.host}:${config.port}/dashboard`);
|
|
459
|
+
console.log(`public base url: ${config.publicBaseUrl}`);
|
|
460
|
+
console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
|
|
461
|
+
console.log(`allowed hosts: ${config.allowedHosts.join(", ")}`);
|
|
462
|
+
if (config.allowedHosts.includes("*")) {
|
|
463
|
+
console.warn("warning: Host header allowlist is disabled because server.allowedHosts contains '*'");
|
|
464
|
+
}
|
|
465
|
+
if (scheme === "https")
|
|
466
|
+
console.log("tls: native termination from tls.certFile/tls.keyFile");
|
|
467
|
+
console.log("auth: Owner password approval required");
|
|
468
|
+
console.log(`logging: ${config.logging.level} ${config.logging.format}`);
|
|
469
|
+
console.log(`subagent providers: ${formatLocalAgentProviderStatusSummary(localAgentProviders)}`);
|
|
470
|
+
console.log("next: run `hearth connect` for ChatGPT / Claude / generic MCP steps");
|
|
471
|
+
}
|
|
472
|
+
async function serve() {
|
|
473
|
+
const sqliteStatus = checkSqliteNative();
|
|
474
|
+
if (sqliteStatus !== "ok") {
|
|
475
|
+
throw new Error([
|
|
476
|
+
"better-sqlite3 could not load for this Node runtime.",
|
|
477
|
+
sqliteStatus,
|
|
478
|
+
"",
|
|
479
|
+
"Try reinstalling or rebuilding dependencies under the active Node version:",
|
|
480
|
+
" npm rebuild better-sqlite3",
|
|
481
|
+
].join("\n"));
|
|
482
|
+
}
|
|
483
|
+
const { createServer } = await import("./server.js");
|
|
484
|
+
const config = loadConfig();
|
|
485
|
+
const { app, close, localAgentProviders } = createServer(config);
|
|
486
|
+
const tlsOn = Boolean(config.tls.certFile && config.tls.keyFile);
|
|
487
|
+
if (Boolean(config.tls.certFile) !== Boolean(config.tls.keyFile)) {
|
|
488
|
+
throw new Error("tls.certFile and tls.keyFile must be set together (or both left null).");
|
|
489
|
+
}
|
|
490
|
+
let httpServer;
|
|
491
|
+
if (tlsOn) {
|
|
492
|
+
const { readFileSync } = await import("node:fs");
|
|
493
|
+
const { createServer: createHttpsServer } = await import("node:https");
|
|
494
|
+
const { expandHomePath } = await import("./roots.js");
|
|
495
|
+
httpServer = createHttpsServer({
|
|
496
|
+
key: readFileSync(expandHomePath(config.tls.keyFile)),
|
|
497
|
+
cert: readFileSync(expandHomePath(config.tls.certFile)),
|
|
498
|
+
}, app).listen(config.port, config.host, () => {
|
|
499
|
+
logServeBanner(config, localAgentProviders, "https");
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
httpServer = app.listen(config.port, config.host, () => {
|
|
504
|
+
logServeBanner(config, localAgentProviders, "http");
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
let shuttingDown = false;
|
|
508
|
+
const shutdown = async () => {
|
|
509
|
+
if (shuttingDown)
|
|
510
|
+
return;
|
|
511
|
+
shuttingDown = true;
|
|
512
|
+
await shutdownHttpServer(httpServer, close);
|
|
513
|
+
process.exit(0);
|
|
514
|
+
};
|
|
515
|
+
const handleShutdown = () => {
|
|
516
|
+
void shutdown().catch((error) => {
|
|
517
|
+
console.error("hearth shutdown failed", error);
|
|
518
|
+
process.exit(1);
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
process.once("SIGINT", handleShutdown);
|
|
522
|
+
process.once("SIGTERM", handleShutdown);
|
|
523
|
+
}
|
|
524
|
+
async function runDoctor({ fix }) {
|
|
525
|
+
const files = loadHearthFiles();
|
|
526
|
+
console.log(`Config dir: ${files.dir}`);
|
|
527
|
+
console.log(`Config file: ${files.configExists ? files.configPath : "missing"}`);
|
|
528
|
+
console.log(`Auth file: ${files.authExists ? files.authPath : "missing"}`);
|
|
529
|
+
console.log(`Node: ${process.version} (${nodeVersionStatus()})`);
|
|
530
|
+
console.log(`Node ABI: ${process.versions.modules}`);
|
|
531
|
+
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
532
|
+
console.log(`Git: ${checkGitAvailable()}`);
|
|
533
|
+
console.log(`Bash shell: ${checkBashShell()}`);
|
|
534
|
+
console.log(`SQLite native dependency: ${checkSqliteNative()}`);
|
|
535
|
+
try {
|
|
536
|
+
if (fix) {
|
|
537
|
+
const raw = String(files.config.server.publicBaseUrl ?? "");
|
|
538
|
+
if (/\/mcp\/?$/i.test(raw.trim())) {
|
|
539
|
+
const origin = new URL(raw).origin;
|
|
540
|
+
setHearthConfigValue(["server", "publicBaseUrl"], origin);
|
|
541
|
+
console.log(`Fixed publicBaseUrl: stripped trailing /mcp -> ${origin}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const config = loadConfig();
|
|
545
|
+
let machineId = "unknown";
|
|
546
|
+
try {
|
|
547
|
+
const { loadMachineIdentity } = await import("./machine-id.js");
|
|
548
|
+
machineId = loadMachineIdentity(config.stateDir).id;
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
// Identity is advisory in doctor output.
|
|
552
|
+
}
|
|
553
|
+
console.log(`Machine: ${machineId} (hearth id)`);
|
|
554
|
+
console.log(`Local MCP URL: http://${config.host}:${config.port}/mcp`);
|
|
555
|
+
console.log(`Public MCP URL: ${new URL("/mcp", config.publicBaseUrl).toString()}`);
|
|
556
|
+
console.log(`Dashboard: http://${config.host}:${config.port}/dashboard`);
|
|
557
|
+
console.log(`Allowed roots: ${config.allowedRoots.join(", ") || "(none — MCP open_workspace will reject every path)"}`);
|
|
558
|
+
console.log(`Allowed hosts: ${config.allowedHosts.join(", ")}`);
|
|
559
|
+
console.log(`Redirect hosts: ${config.oauth.allowedRedirectHosts.join(", ")}`);
|
|
560
|
+
const providers = buildLocalAgentProviderStatuses(config.subagents, getLocalAgentProviderAvailabilitySnapshot());
|
|
561
|
+
console.log(`Subagents: ${config.subagents.enabled ? "enabled" : "disabled"}`);
|
|
562
|
+
console.log(`Subagent providers: ${formatLocalAgentProviderStatusSummary(providers)}`);
|
|
563
|
+
console.log(`Tool mode: ${config.toolMode}`);
|
|
564
|
+
const warnings = [];
|
|
565
|
+
if (config.allowedHosts.includes("*"))
|
|
566
|
+
warnings.push("server.allowedHosts contains '*': Host header checks are disabled (local debugging only).");
|
|
567
|
+
if (config.allowedRoots.length === 0)
|
|
568
|
+
warnings.push("workspaces.allowedRoots is empty: MCP file tools fall back to cwd. Run hearth init to set narrow roots.");
|
|
569
|
+
if (!config.publicBaseUrl.startsWith("https://") && config.publicBaseUrl !== `http://${config.host}:${config.port}`) {
|
|
570
|
+
warnings.push("server.publicBaseUrl is http: remote MCP clients require public https. Use your tunnel's https origin.");
|
|
571
|
+
}
|
|
572
|
+
for (const host of ["chatgpt.com", "claude.ai"]) {
|
|
573
|
+
if (!config.oauth.allowedRedirectHosts.some((allowed) => host === allowed || host.endsWith(`.${allowed}`) || allowed.endsWith(host))) {
|
|
574
|
+
warnings.push(`oauth.allowedRedirectHosts is missing ${host}: that host's OAuth redirect will be rejected.`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (process.platform === "win32")
|
|
578
|
+
warnings.push("Windows: shell tools require Git Bash, WSL, MSYS2, or Cygwin Bash (PowerShell/cmd not supported).");
|
|
579
|
+
for (const warning of warnings)
|
|
580
|
+
console.log(`warning: ${warning}`);
|
|
581
|
+
if (!fix && /\/mcp\/?$/i.test(String(files.config.server.publicBaseUrl ?? ""))) {
|
|
582
|
+
console.log("hint: run `hearth doctor --fix` to strip trailing /mcp from publicBaseUrl.");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
console.log(`Config status: ${error instanceof Error ? error.message : String(error)}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function runConnect(args) {
|
|
590
|
+
const host = args.find((arg) => !arg.startsWith("-"))?.toLowerCase();
|
|
591
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
592
|
+
console.log(["Usage:", " hearth connect [chatgpt|claude|generic]", "", "Prints copy-paste MCP connection steps for this PC."].join("\n"));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (host && !["chatgpt", "claude", "generic", "all"].includes(host)) {
|
|
596
|
+
throw new Error(`Unknown connect target: ${host}. Use chatgpt, claude, or generic.`);
|
|
597
|
+
}
|
|
598
|
+
const config = loadConfig();
|
|
599
|
+
const { loadMachineIdentity } = await import("./machine-id.js");
|
|
600
|
+
const identity = loadMachineIdentity(config.stateDir);
|
|
601
|
+
const publicMcpUrl = new URL("/mcp", config.publicBaseUrl).toString();
|
|
602
|
+
const localMcpUrl = `http://${config.host}:${config.port}/mcp`;
|
|
603
|
+
const wanted = host ?? "all";
|
|
604
|
+
const lines = [
|
|
605
|
+
`machine: ${identity.id} (${identity.hostname})`,
|
|
606
|
+
`public MCP URL (use this in remote clients): ${publicMcpUrl}`,
|
|
607
|
+
`local MCP URL: ${localMcpUrl}`,
|
|
608
|
+
`dashboard: http://${config.host}:${config.port}/dashboard`,
|
|
609
|
+
`tool mode: ${config.toolMode} (ChatGPT works with either; Claude Desktop prefers tools.mode claude)`,
|
|
610
|
+
"",
|
|
611
|
+
];
|
|
612
|
+
if (wanted === "all" || wanted === "chatgpt") {
|
|
613
|
+
lines.push("ChatGPT:", ` 1. hearth serve (keep running) + tunnel pointing at http://${config.host}:${config.port} (proxy the whole origin, not only /mcp).`, ` 2. Add connector URL: ${publicMcpUrl}`, " 3. Approve with the Owner password from ~/.hearth/auth.json (hearth init prints it).", "");
|
|
614
|
+
}
|
|
615
|
+
if (wanted === "all" || wanted === "claude") {
|
|
616
|
+
lines.push("Claude (Desktop / Code with remote MCP):", ` 1. Ensure oauth.allowedRedirectHosts includes claude.ai (current: ${config.oauth.allowedRedirectHosts.join(", ")}).`, ` 2. Add MCP server URL: ${publicMcpUrl} and approve with the Owner password.`, " 3. For local-only use without OAuth: hearth token create claude-local, then `hearth mcp` as a stdio server with that bearer.", "");
|
|
617
|
+
}
|
|
618
|
+
if (wanted === "all" || wanted === "generic") {
|
|
619
|
+
lines.push("Any MCP client (generic):", ` Remote (OAuth): ${publicMcpUrl}`, ` Local stdio (no OAuth browser round-trip): hearth mcp`, " Local with device token: hearth token create <name>, then use it as the MCP Authorization bearer.", "");
|
|
620
|
+
}
|
|
621
|
+
lines.push("Do not reuse this public URL on another PC: OAuth tokens are bound to this machine's resource URL and owner password.");
|
|
622
|
+
lines.push("The machine id is a diagnostic label, not a security boundary: copying stateDir copies the identity, so it cannot prove which PC answered or prevent cloning.");
|
|
623
|
+
console.log(lines.join("\n"));
|
|
624
|
+
}
|
|
625
|
+
function runConfigCommand(args) {
|
|
626
|
+
const [subcommand, key, ...rest] = args;
|
|
627
|
+
const files = loadHearthFiles();
|
|
628
|
+
if (!subcommand || subcommand === "get") {
|
|
629
|
+
console.log(JSON.stringify(files.config, null, 2));
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (subcommand !== "set") {
|
|
633
|
+
throw new Error(`Unknown config command: ${subcommand}`);
|
|
634
|
+
}
|
|
635
|
+
if (key !== "publicBaseUrl") {
|
|
636
|
+
throw new Error("Only `hearth config set publicBaseUrl <url|null>` is supported right now.");
|
|
637
|
+
}
|
|
638
|
+
const value = rest.join(" ").trim();
|
|
639
|
+
if (!value) {
|
|
640
|
+
throw new Error("Missing publicBaseUrl value.");
|
|
641
|
+
}
|
|
642
|
+
setHearthConfigValue(["server", "publicBaseUrl"], normalizeOptionalPublicBaseUrl(value));
|
|
643
|
+
console.log(`Updated ${files.configPath}`);
|
|
644
|
+
}
|
|
645
|
+
function runMachineId() {
|
|
646
|
+
const config = loadConfig();
|
|
647
|
+
void import("./machine-id.js").then(({ loadMachineIdentity }) => {
|
|
648
|
+
const identity = loadMachineIdentity(config.stateDir);
|
|
649
|
+
console.log(JSON.stringify(identity, null, 2));
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Relay-free exposure report: tells the owner exactly what stands between
|
|
654
|
+
* this PC and a direct public URL, without routing through any tunnel
|
|
655
|
+
* service. Honest by design: a public URL needs (1) an inbound route to this
|
|
656
|
+
* machine and (2) a domain with a valid certificate. Hearth automates
|
|
657
|
+
* everything after those two owner-provided prerequisites.
|
|
658
|
+
*/
|
|
659
|
+
async function runExpose() {
|
|
660
|
+
const config = loadConfig();
|
|
661
|
+
const { loadMachineIdentity } = await import("./machine-id.js");
|
|
662
|
+
const identity = loadMachineIdentity(config.stateDir);
|
|
663
|
+
const { execFileSync } = await import("node:child_process");
|
|
664
|
+
const { existsSync } = await import("node:fs");
|
|
665
|
+
const { expandHomePath } = await import("./roots.js");
|
|
666
|
+
let publicIp;
|
|
667
|
+
try {
|
|
668
|
+
const out = execFileSync("dig", ["+short", "+time=5", "myip.opendns.com", "@resolver1.opendns.com"], {
|
|
669
|
+
encoding: "utf8",
|
|
670
|
+
timeout: 10_000,
|
|
671
|
+
}).trim().split("\n").pop()?.trim();
|
|
672
|
+
if (out && /^[0-9a-fA-F.:]+$/.test(out))
|
|
673
|
+
publicIp = out;
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
// dig absent or blocked: report without a public IP guess.
|
|
677
|
+
}
|
|
678
|
+
const certSet = Boolean(config.tls.certFile && config.tls.keyFile);
|
|
679
|
+
const certPresent = certSet
|
|
680
|
+
&& existsSync(expandHomePath(config.tls.certFile))
|
|
681
|
+
&& existsSync(expandHomePath(config.tls.keyFile));
|
|
682
|
+
const publicUrl = new URL(config.publicBaseUrl);
|
|
683
|
+
const directHttps = publicUrl.protocol === "https:" && certSet;
|
|
684
|
+
const lines = [
|
|
685
|
+
`machine: ${identity.id} (${identity.hostname}, ${identity.platform}/${identity.arch})`,
|
|
686
|
+
`bind: ${config.host}:${config.port} (hearth serve)`,
|
|
687
|
+
`public base url: ${config.publicBaseUrl}`,
|
|
688
|
+
`public MCP URL (keep one URL per PC — reusing it elsewhere splits approvals and audit): ${new URL("/mcp", config.publicBaseUrl).toString()}`,
|
|
689
|
+
`public IP seen from here: ${publicIp ?? "unknown (dig unavailable or blocked)"}`,
|
|
690
|
+
`native TLS: ${!certSet ? "off (tls.certFile/tls.keyFile unset)" : certPresent ? "cert + key present" : "CONFIGURED BUT FILES MISSING"}`,
|
|
691
|
+
`ACME webroot: ${config.tls.acmeDir ?? "unset"}`,
|
|
692
|
+
"",
|
|
693
|
+
"OAuth tokens are bound to this machine's resource URL and Owner password.",
|
|
694
|
+
"Run `hearth connect` for ChatGPT / Claude / generic MCP steps.",
|
|
695
|
+
"",
|
|
696
|
+
directHttps && certPresent
|
|
697
|
+
? "status: DIRECT — publicBaseUrl is https and TLS material is present. Forward TCP 443 to this machine, point your domain's A/AAAA record at the public IP, restart serve."
|
|
698
|
+
: "status: NOT directly reachable — to drop the relay you need:",
|
|
699
|
+
...(!directHttps || !certPresent ? [
|
|
700
|
+
" 1. A domain whose A/AAAA record points at this machine (public IP above).",
|
|
701
|
+
" 2. TCP 443 (and TCP 80 for issuance) forwarded to this machine.",
|
|
702
|
+
" 3. Certificates, e.g.: certbot certonly --webroot -w <dir> -d <domain>",
|
|
703
|
+
" with tls.acmeDir set to <dir> so Hearth serves the HTTP-01 challenge.",
|
|
704
|
+
" 4. tls.certFile/tls.keyFile set to the issued files,",
|
|
705
|
+
" hearth config set publicBaseUrl https://<domain>, then restart serve.",
|
|
706
|
+
] : []),
|
|
707
|
+
"",
|
|
708
|
+
"Without an inbound route + domain, some relay must carry the traffic;",
|
|
709
|
+
"that relay can be your own VPS — never a third party you don't control.",
|
|
710
|
+
];
|
|
711
|
+
console.log(lines.join("\n"));
|
|
712
|
+
}
|
|
713
|
+
function printHelp() {
|
|
714
|
+
console.log([
|
|
715
|
+
"Hearth",
|
|
716
|
+
"",
|
|
717
|
+
"Usage:",
|
|
718
|
+
" hearth Run first-time setup if needed, then start the server",
|
|
719
|
+
" hearth serve Start the server",
|
|
720
|
+
" hearth init [--force] [--yes --use chatgpt|coding-agents|both --roots <csv> --public-url <url> --providers <csv>]",
|
|
721
|
+
" hearth doctor [--fix] Show config, runtime, and native dependency status",
|
|
722
|
+
" hearth connect [chatgpt|claude|generic] Print copy-paste MCP connection steps for this PC",
|
|
723
|
+
" hearth config get Print persisted config",
|
|
724
|
+
" hearth config set publicBaseUrl <url|null> (origin only, without /mcp)",
|
|
725
|
+
" hearth show-changes <review-ref> [--json]",
|
|
726
|
+
" hearth agents ls List subagent sessions",
|
|
727
|
+
" hearth agents run <profile-or-provider> [--model <model>] [--effort <level>] <prompt>",
|
|
728
|
+
" hearth agents continue <id> [--model <model>] [--effort <level>] <prompt>",
|
|
729
|
+
" hearth agents show <id>",
|
|
730
|
+
" hearth agents daemon <status|stop|logs>",
|
|
731
|
+
" hearth expose Relay-free exposure report for this PC (own domain + TLS)",
|
|
732
|
+
" hearth id Print this machine's stable Hearth identity",
|
|
733
|
+
" hearth -v, --version Print the installed version",
|
|
734
|
+
"",
|
|
735
|
+
"Examples:",
|
|
736
|
+
" npm install -g @sunshinelife83/hearth",
|
|
737
|
+
" hearth init",
|
|
738
|
+
" hearth serve",
|
|
739
|
+
" hearth connect",
|
|
740
|
+
"",
|
|
741
|
+
"For temporary tunnels:",
|
|
742
|
+
" hearth config set publicBaseUrl https://your-tunnel-host.example.com",
|
|
743
|
+
" hearth serve",
|
|
744
|
+
].join("\n"));
|
|
745
|
+
}
|
|
746
|
+
async function runShowChanges(args) {
|
|
747
|
+
const { args: commandArgs, json } = extractJsonOption(args);
|
|
748
|
+
const [reviewRef, ...extra] = commandArgs;
|
|
749
|
+
if (!reviewRef || extra.length > 0) {
|
|
750
|
+
throw new Error("Usage: hearth show-changes <review-ref> [--json]");
|
|
751
|
+
}
|
|
752
|
+
const config = loadConfig();
|
|
753
|
+
const scope = resolveCliWorkspaceContext(config.allowedRoots);
|
|
754
|
+
const review = await readReviewRef(scope.workspaceRoot, reviewRef);
|
|
755
|
+
if (json) {
|
|
756
|
+
printJson(review);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
console.log(review.patch || review.result);
|
|
760
|
+
}
|
|
761
|
+
async function runAgentsCommand(args) {
|
|
762
|
+
const [subcommand, ...rest] = args;
|
|
763
|
+
const { args: commandArgs, json } = extractJsonOption(rest);
|
|
764
|
+
switch (subcommand) {
|
|
765
|
+
case "ls":
|
|
766
|
+
case "list":
|
|
767
|
+
await runAgentsList(commandArgs, json);
|
|
768
|
+
return;
|
|
769
|
+
case "run":
|
|
770
|
+
await runAgentsRun(commandArgs, json);
|
|
771
|
+
return;
|
|
772
|
+
case "continue":
|
|
773
|
+
await runAgentsContinue(commandArgs, json);
|
|
774
|
+
return;
|
|
775
|
+
case "show":
|
|
776
|
+
await runAgentsShow(commandArgs, json);
|
|
777
|
+
return;
|
|
778
|
+
case "targets":
|
|
779
|
+
await runAgentsTargets(commandArgs, json);
|
|
780
|
+
return;
|
|
781
|
+
case "daemon":
|
|
782
|
+
await runAgentsDaemon(commandArgs, json);
|
|
783
|
+
return;
|
|
784
|
+
case undefined:
|
|
785
|
+
case "help":
|
|
786
|
+
case "--help":
|
|
787
|
+
case "-h":
|
|
788
|
+
printAgentsHelp();
|
|
789
|
+
return;
|
|
790
|
+
default:
|
|
791
|
+
throw new Error(`Unknown agents command: ${subcommand}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async function runAgentsTargets(args, json) {
|
|
795
|
+
if (args.length > 0)
|
|
796
|
+
throw new Error("Usage: hearth agents targets [--json]");
|
|
797
|
+
const config = loadConfig();
|
|
798
|
+
const scope = resolveCliWorkspaceContext(config.allowedRoots);
|
|
799
|
+
const profiles = await loadLocalAgentProfiles(config, scope.workspaceRoot);
|
|
800
|
+
const providers = buildLocalAgentProviderStatuses(config.subagents, getLocalAgentProviderAvailabilitySnapshot());
|
|
801
|
+
const catalog = buildLocalAgentCatalog(config.subagents, profiles, providers);
|
|
802
|
+
const output = presentAgentTargetCatalog(catalog);
|
|
803
|
+
if (json)
|
|
804
|
+
printJson(output);
|
|
805
|
+
else
|
|
806
|
+
console.log(formatAgentTargetCatalog(output));
|
|
807
|
+
}
|
|
808
|
+
async function runAgentsList(args, json) {
|
|
809
|
+
if (args.length > 0)
|
|
810
|
+
throw new Error("Usage: hearth agents ls [--json]");
|
|
811
|
+
const config = loadConfig();
|
|
812
|
+
const client = createLocalAgentClient(config);
|
|
813
|
+
const result = await client.list(resolveCliWorkspaceContext(config.allowedRoots));
|
|
814
|
+
const agents = presentAgentResult(result, json);
|
|
815
|
+
if (!agents)
|
|
816
|
+
return;
|
|
817
|
+
const summaries = agents.map(presentAgentSummary);
|
|
818
|
+
if (json) {
|
|
819
|
+
printJson(summaries);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (agents.length === 0) {
|
|
823
|
+
console.log("No subagent sessions found for this workspace.");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
for (const summary of summaries) {
|
|
827
|
+
console.log(formatAgentSummary(summary));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function runAgentsRun(args, json) {
|
|
831
|
+
const parsed = parseLocalAgentRunArgs(args);
|
|
832
|
+
const config = loadConfig();
|
|
833
|
+
const scope = resolveCliWorkspaceContext(config.allowedRoots);
|
|
834
|
+
const client = createLocalAgentClient(config);
|
|
835
|
+
const result = await client.start({
|
|
836
|
+
target: parsed.target,
|
|
837
|
+
prompt: parsed.prompt,
|
|
838
|
+
workspaceRoot: scope.workspaceRoot,
|
|
839
|
+
workspaceId: scope.workspaceId,
|
|
840
|
+
model: parsed.model,
|
|
841
|
+
effort: parsed.effort,
|
|
842
|
+
});
|
|
843
|
+
const record = presentAgentResult(result, json);
|
|
844
|
+
if (!record)
|
|
845
|
+
return;
|
|
846
|
+
const receipt = presentAgentReceipt(record);
|
|
847
|
+
if (json) {
|
|
848
|
+
printJson(receipt);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
console.log(formatAgentReceipt(receipt));
|
|
852
|
+
}
|
|
853
|
+
async function runAgentsContinue(args, json) {
|
|
854
|
+
const parsed = parseLocalAgentContinueArgs(args);
|
|
855
|
+
const config = loadConfig();
|
|
856
|
+
const client = createLocalAgentClient(config);
|
|
857
|
+
const scope = resolveCliWorkspaceContext(config.allowedRoots);
|
|
858
|
+
const result = await client.continue(parsed.agentId, parsed.prompt, {
|
|
859
|
+
model: parsed.model,
|
|
860
|
+
effort: parsed.effort,
|
|
861
|
+
}, scope);
|
|
862
|
+
const record = presentAgentResult(result, json);
|
|
863
|
+
if (!record)
|
|
864
|
+
return;
|
|
865
|
+
const receipt = presentAgentReceipt(record);
|
|
866
|
+
if (json) {
|
|
867
|
+
printJson(receipt);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
console.log(formatAgentReceipt(receipt));
|
|
871
|
+
}
|
|
872
|
+
async function runAgentsShow(args, json) {
|
|
873
|
+
const [id, ...extra] = args;
|
|
874
|
+
if (!id || extra.length > 0)
|
|
875
|
+
throw new Error("Usage: hearth agents show <id> [--json]");
|
|
876
|
+
const config = loadConfig();
|
|
877
|
+
const client = createLocalAgentClient(config);
|
|
878
|
+
const scope = resolveCliWorkspaceContext(config.allowedRoots);
|
|
879
|
+
const initial = await client.get(id, scope);
|
|
880
|
+
let record = presentAgentResult(initial, json);
|
|
881
|
+
if (!record)
|
|
882
|
+
return;
|
|
883
|
+
const deadline = Date.now() + 15_000;
|
|
884
|
+
while ((record.status === "starting" || record.status === "running") && Date.now() < deadline) {
|
|
885
|
+
await sleep(500);
|
|
886
|
+
const refreshed = presentAgentResult(await client.get(id, scope), json);
|
|
887
|
+
if (!refreshed)
|
|
888
|
+
return;
|
|
889
|
+
record = refreshed;
|
|
890
|
+
}
|
|
891
|
+
const observation = presentAgentObservation(record);
|
|
892
|
+
if (json)
|
|
893
|
+
printJson(observation);
|
|
894
|
+
else
|
|
895
|
+
console.log(formatAgentObservation(observation));
|
|
896
|
+
}
|
|
897
|
+
async function runAgentsDaemon(args, json) {
|
|
898
|
+
const [subcommand, ...extra] = args;
|
|
899
|
+
if (extra.length > 0)
|
|
900
|
+
throw new Error("Usage: hearth agents daemon <status|stop|logs> [--json]");
|
|
901
|
+
const config = loadConfig();
|
|
902
|
+
const client = createLocalAgentClient(config);
|
|
903
|
+
switch (subcommand) {
|
|
904
|
+
case "status": {
|
|
905
|
+
const status = presentAgentResult(await client.status(), json);
|
|
906
|
+
if (!status)
|
|
907
|
+
return;
|
|
908
|
+
printJson(status);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
case "stop": {
|
|
912
|
+
const status = presentAgentResult(await client.stop(), json);
|
|
913
|
+
if (!status)
|
|
914
|
+
return;
|
|
915
|
+
if (json)
|
|
916
|
+
printJson(status);
|
|
917
|
+
else
|
|
918
|
+
console.log("Local agent daemon stop requested.");
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
case "logs": {
|
|
922
|
+
const logs = presentAgentResult(await client.logs(), json);
|
|
923
|
+
if (logs === undefined)
|
|
924
|
+
return;
|
|
925
|
+
if (json)
|
|
926
|
+
printJson({ logs });
|
|
927
|
+
else
|
|
928
|
+
console.log(logs || "No local agent daemon logs found.");
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
default:
|
|
932
|
+
throw new Error("Usage: hearth agents daemon <status|stop|logs>");
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function extractJsonOption(args) {
|
|
936
|
+
const commandArgs = [];
|
|
937
|
+
let json = false;
|
|
938
|
+
let optionsEnded = false;
|
|
939
|
+
for (const argument of args) {
|
|
940
|
+
if (!optionsEnded && argument === "--") {
|
|
941
|
+
optionsEnded = true;
|
|
942
|
+
commandArgs.push(argument);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
if (!optionsEnded && argument === "--json") {
|
|
946
|
+
json = true;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
commandArgs.push(argument);
|
|
950
|
+
}
|
|
951
|
+
return { args: commandArgs, json };
|
|
952
|
+
}
|
|
953
|
+
function presentAgentResult(result, json) {
|
|
954
|
+
if (result.isOk())
|
|
955
|
+
return result.value;
|
|
956
|
+
if (json) {
|
|
957
|
+
printJson({ error: toAgentErrorPayload(result.error) });
|
|
958
|
+
process.exitCode = 1;
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
throw new Error(result.error.message);
|
|
962
|
+
}
|
|
963
|
+
function printJson(value) {
|
|
964
|
+
console.log(JSON.stringify(value));
|
|
965
|
+
}
|
|
966
|
+
function sleep(ms) {
|
|
967
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
968
|
+
}
|
|
969
|
+
function printAgentsHelp() {
|
|
970
|
+
console.log([
|
|
971
|
+
"Hearth agents",
|
|
972
|
+
"",
|
|
973
|
+
"Usage:",
|
|
974
|
+
" hearth agents ls [--json]",
|
|
975
|
+
" hearth agents run <profile-or-provider> [--model <model>] [--effort <level>] [--json] <prompt>",
|
|
976
|
+
" hearth agents continue <id> [--model <model>] [--effort <level>] [--json] <prompt>",
|
|
977
|
+
" hearth agents show <id> [--json]",
|
|
978
|
+
" hearth agents targets [--json]",
|
|
979
|
+
" hearth agents daemon <status|stop|logs> [--json]",
|
|
980
|
+
].join("\n"));
|
|
981
|
+
}
|
|
982
|
+
function printVersion() {
|
|
983
|
+
const packageJson = require("../package.json");
|
|
984
|
+
if (typeof packageJson.version !== "string") {
|
|
985
|
+
throw new Error("Unable to read Hearth package version.");
|
|
986
|
+
}
|
|
987
|
+
console.log(packageJson.version);
|
|
988
|
+
}
|
|
989
|
+
function normalizeOptionalPublicBaseUrl(value) {
|
|
990
|
+
const trimmed = value.trim();
|
|
991
|
+
if (!trimmed || trimmed === "null" || trimmed === "none")
|
|
992
|
+
return null;
|
|
993
|
+
return normalizePublicBaseUrl(trimmed);
|
|
994
|
+
}
|
|
995
|
+
function normalizePublicBaseUrl(value) {
|
|
996
|
+
const trimmed = value.trim();
|
|
997
|
+
const parsed = new URL(trimmed);
|
|
998
|
+
parsed.hash = "";
|
|
999
|
+
parsed.search = "";
|
|
1000
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
1001
|
+
if (/(^|\/)mcp$/i.test(parsed.pathname)) {
|
|
1002
|
+
throw new Error(`Use the origin without /mcp (for example https://${parsed.host}). Got: ${trimmed}`);
|
|
1003
|
+
}
|
|
1004
|
+
return parsed.toString().replace(/\/$/, "");
|
|
1005
|
+
}
|
|
1006
|
+
async function textPrompt(options) {
|
|
1007
|
+
const result = await prompts.text({
|
|
1008
|
+
...options,
|
|
1009
|
+
validate: (value) => options.validate?.(value?.trim() ? value : options.defaultValue),
|
|
1010
|
+
});
|
|
1011
|
+
if (prompts.isCancel(result))
|
|
1012
|
+
throw new SetupCancelledError();
|
|
1013
|
+
const value = String(result).trim();
|
|
1014
|
+
return value || options.defaultValue;
|
|
1015
|
+
}
|
|
1016
|
+
function validateRequiredPublicBaseUrl(value) {
|
|
1017
|
+
const trimmed = value?.trim() ?? "";
|
|
1018
|
+
if (!trimmed)
|
|
1019
|
+
return "Enter the public URL from your tunnel or reverse proxy.";
|
|
1020
|
+
if (trimmed.endsWith("/mcp"))
|
|
1021
|
+
return "Enter the base URL only, without /mcp.";
|
|
1022
|
+
return validatePublicBaseUrl(trimmed);
|
|
1023
|
+
}
|
|
1024
|
+
function validatePublicBaseUrl(value) {
|
|
1025
|
+
try {
|
|
1026
|
+
const parsed = new URL(value);
|
|
1027
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:"
|
|
1028
|
+
? undefined
|
|
1029
|
+
: "Use an http or https URL.";
|
|
1030
|
+
}
|
|
1031
|
+
catch {
|
|
1032
|
+
return "Enter a valid URL, for example https://your-tunnel-host.example.com.";
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function assertSupportedNode() {
|
|
1036
|
+
if (satisfies(process.versions.node, SUPPORTED_NODE_RANGE))
|
|
1037
|
+
return;
|
|
1038
|
+
throw new Error([
|
|
1039
|
+
`Hearth requires Node ${SUPPORTED_NODE_RANGE}.`,
|
|
1040
|
+
`Current Node: ${process.version}`,
|
|
1041
|
+
"",
|
|
1042
|
+
"Install Node 22 LTS or use a version manager such as nvm, fnm, or mise.",
|
|
1043
|
+
].join("\n"));
|
|
1044
|
+
}
|
|
1045
|
+
function nodeVersionStatus() {
|
|
1046
|
+
return satisfies(process.versions.node, SUPPORTED_NODE_RANGE)
|
|
1047
|
+
? `supported ${SUPPORTED_NODE_RANGE}`
|
|
1048
|
+
: `unsupported, requires ${SUPPORTED_NODE_RANGE}`;
|
|
1049
|
+
}
|
|
1050
|
+
class SetupCancelledError extends Error {
|
|
1051
|
+
}
|
|
1052
|
+
function checkSqliteNative() {
|
|
1053
|
+
try {
|
|
1054
|
+
const Database = require("better-sqlite3");
|
|
1055
|
+
const db = new Database(":memory:");
|
|
1056
|
+
db.close();
|
|
1057
|
+
return "ok";
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
return error instanceof Error ? error.message : String(error);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function checkGitAvailable() {
|
|
1064
|
+
try {
|
|
1065
|
+
const { execFileSync } = require("node:child_process");
|
|
1066
|
+
return execFileSync("git", ["--version"], { encoding: "utf8" }).trim();
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1070
|
+
return `unavailable (${message})`;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function checkBashShell() {
|
|
1074
|
+
try {
|
|
1075
|
+
const { shell, args } = getShellConfig();
|
|
1076
|
+
return `${shell} ${args.join(" ")}`;
|
|
1077
|
+
}
|
|
1078
|
+
catch (error) {
|
|
1079
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1080
|
+
return `unavailable (${message})`;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
1084
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1085
|
+
process.exitCode = 1;
|
|
1086
|
+
});
|