@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/server.js
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { access, realpath } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
7
|
+
import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
8
|
+
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
9
|
+
import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
|
|
10
|
+
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
11
|
+
import { toNodeHandler } from "@modelcontextprotocol/node";
|
|
12
|
+
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
|
|
13
|
+
import express from "express";
|
|
14
|
+
import * as z from "zod/v4";
|
|
15
|
+
import { isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
|
|
16
|
+
import { registerAgentTools } from "./agent-tools.js";
|
|
17
|
+
import { registerDashboard, dashboardDirectory } from "./dashboard.js";
|
|
18
|
+
import { loadMachineIdentity } from "./machine-id.js";
|
|
19
|
+
import { expandHomePath } from "./roots.js";
|
|
20
|
+
import { registerTaskTools } from "./task-tools.js";
|
|
21
|
+
import { registerContextTools } from "./context/context-tools.js";
|
|
22
|
+
import { TaskStore } from "./task-store.js";
|
|
23
|
+
import { probeSandboxAdapter, wrapCommandWithSandbox } from "./policy/sandbox.js";
|
|
24
|
+
import { resolveExecutionForWorkspace } from "./policy/workspace-profiles.js";
|
|
25
|
+
import { loadConfig } from "./config.js";
|
|
26
|
+
import { createOpenAIIncomingArtifactAdapter, } from "./incoming-artifacts.js";
|
|
27
|
+
import { logEvent, requestIp, requestPath, } from "./logger.js";
|
|
28
|
+
import { readFileTool } from "./pi-tools.js";
|
|
29
|
+
import { RateLimiter, createAuthRateLimiters } from "./rate-limit.js";
|
|
30
|
+
import { SingleUserOAuthProvider } from "./oauth-provider.js";
|
|
31
|
+
import { compileMcpRegistrationSurface, createModernMcpServerAdapter, modernMcpAdapterErrorLogFields, } from "./mcp-modern-server.js";
|
|
32
|
+
import { ProcessSessionManager } from "./process-sessions.js";
|
|
33
|
+
import { createReviewCheckpointManager } from "./review-checkpoints.js";
|
|
34
|
+
import { createSnapshot, listSnapshots, rollbackSnapshot, } from "./snapshot-manager.js";
|
|
35
|
+
import { decideExecution } from "./policy/command-policy.js";
|
|
36
|
+
import { conversationScopeIdFromRequestMeta } from "./request-meta.js";
|
|
37
|
+
import { shutdownHttpServer } from "./server-shutdown.js";
|
|
38
|
+
import { formatPathForPrompt } from "./skills.js";
|
|
39
|
+
import { createWorkspaceStore } from "./workspace-store.js";
|
|
40
|
+
import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
|
|
41
|
+
import { getLocalAgentProviderAvailabilitySnapshot, } from "./local-agent-availability.js";
|
|
42
|
+
import { buildLocalAgentCatalog, buildLocalAgentProviderStatuses, formatLocalAgentProviderStatusSummary, } from "./local-agent-catalog.js";
|
|
43
|
+
import { getToolSurface } from "./tool-surfaces/index.js";
|
|
44
|
+
import { contentText, logFailedToolResponse, logToolCall, resultOutputSchema, textBlock, workspaceAppDescriptorMeta, } from "./tool-surfaces/shared.js";
|
|
45
|
+
import { WORKSPACE_APP_URI, toolNames, workspaceIdDescription, } from "./tool-surfaces/types.js";
|
|
46
|
+
const WORKSPACE_APP_MANIFEST_ENTRY = "workspace-app.html";
|
|
47
|
+
const SHUTDOWN_DRAIN_TIMEOUT_MS = 15_000;
|
|
48
|
+
function mcpServerInfo() {
|
|
49
|
+
return {
|
|
50
|
+
name: "hearth",
|
|
51
|
+
title: "Hearth",
|
|
52
|
+
version: "0.1.0",
|
|
53
|
+
description: "Coding tools for project workspaces. Open each project or worktree once, then reuse its workspaceId.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
class ToolActivityTracker {
|
|
57
|
+
active = new Set();
|
|
58
|
+
track = (operation) => {
|
|
59
|
+
const promise = operation();
|
|
60
|
+
this.active.add(promise);
|
|
61
|
+
const remove = () => this.active.delete(promise);
|
|
62
|
+
void promise.then(remove, remove);
|
|
63
|
+
return promise;
|
|
64
|
+
};
|
|
65
|
+
activeCount() {
|
|
66
|
+
return this.active.size;
|
|
67
|
+
}
|
|
68
|
+
async waitForIdle(timeoutMs) {
|
|
69
|
+
if (timeoutMs === undefined) {
|
|
70
|
+
while (this.active.size > 0) {
|
|
71
|
+
await Promise.allSettled(Array.from(this.active));
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const deadline = new Promise((resolve) => {
|
|
76
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
77
|
+
timer.unref();
|
|
78
|
+
});
|
|
79
|
+
while (this.active.size > 0) {
|
|
80
|
+
await Promise.race([Promise.allSettled(Array.from(this.active)), deadline]);
|
|
81
|
+
if (this.active.size > 0)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function serverInstructions(config, toolSurface) {
|
|
87
|
+
const artifactInstruction = config.artifactsEnabled && isArtifactDownloadSupportedPlatform()
|
|
88
|
+
? " When the user supplies or generates a file that is not present on the Hearth host, use download_artifact with its native file value, the existing workspace ID, and a suitable relative destination path chosen from the user's request and project structure. The tool refuses to overwrite an existing destination and returns the normalized workspace-relative path. Use normal workspace tools when explicit inspection, replacement, movement, renaming, or deletion is needed. Do not recreate binary files with write/edit calls or place signed URLs, native file objects, base64 content, or invented host paths in shell commands or logs."
|
|
89
|
+
: "";
|
|
90
|
+
const showChangesInstruction = " If the turn successfully modifies files by creating, editing, overwriting, deleting, moving, or applying patches, call show_changes exactly once for that workspace after the final related file change and before your final response so the user can inspect the aggregate diff for that turn. Do not call it after every individual file change.";
|
|
91
|
+
const skills = config.skillsEnabled
|
|
92
|
+
? `When ${toolNames.openWorkspace} returns available skills and a task matches a skill, use ${toolNames.read} to read that skill's path before proceeding. Skill paths may be outside the workspace, and ${toolNames.read} permits files within advertised skill directories. `
|
|
93
|
+
: "";
|
|
94
|
+
const agents = `Follow instructions returned by ${toolNames.openWorkspace}. Before working under a path listed in availableAgentsFiles, use ${toolNames.read} to inspect that instruction file and follow it. `;
|
|
95
|
+
const common = `Use Hearth for coding work. Call ${toolNames.openWorkspace} once for each project folder or isolated worktree, then keep using its workspaceId. During continued work in the same project or worktree, do not call ${toolNames.openWorkspace} again. Open another workspace only when changing projects, switching checkout/worktree mode, creating another isolated worktree, or when the current workspaceId is rejected.`;
|
|
96
|
+
return `${common} ${toolSurface.instructions({ agents, skills })}${artifactInstruction}${showChangesInstruction}`;
|
|
97
|
+
}
|
|
98
|
+
function formatVisibleAgent(agent) {
|
|
99
|
+
const model = agent.model ? `, model ${agent.model}` : "";
|
|
100
|
+
const effort = agent.effort ? `, effort ${agent.effort}` : "";
|
|
101
|
+
return `${agent.name} (${agent.provider}${model}${effort})`;
|
|
102
|
+
}
|
|
103
|
+
function formatAvailableAgentProvider(provider) {
|
|
104
|
+
const details = [
|
|
105
|
+
provider.model ? `model ${provider.model}` : undefined,
|
|
106
|
+
provider.effort ? `effort ${provider.effort}` : undefined,
|
|
107
|
+
provider.note,
|
|
108
|
+
].filter(Boolean).join(", ");
|
|
109
|
+
return `${provider.id}${details ? ` (${details})` : ""}`;
|
|
110
|
+
}
|
|
111
|
+
const workspaceSkillOutputSchema = z.object({
|
|
112
|
+
name: z.string(),
|
|
113
|
+
description: z.string(),
|
|
114
|
+
path: z.string(),
|
|
115
|
+
});
|
|
116
|
+
const workspaceAgentsFileOutputSchema = z.object({
|
|
117
|
+
path: z.string(),
|
|
118
|
+
content: z.string(),
|
|
119
|
+
});
|
|
120
|
+
const workspaceLocalAgentOutputSchema = z.object({
|
|
121
|
+
name: z.string(),
|
|
122
|
+
description: z.string(),
|
|
123
|
+
provider: z.string(),
|
|
124
|
+
model: z.string().optional(),
|
|
125
|
+
effort: z.string().optional(),
|
|
126
|
+
});
|
|
127
|
+
const workspaceLocalAgentProviderOutputSchema = z.object({
|
|
128
|
+
id: z.string(),
|
|
129
|
+
model: z.string().optional(),
|
|
130
|
+
effort: z.string().optional(),
|
|
131
|
+
note: z.string().optional(),
|
|
132
|
+
});
|
|
133
|
+
const workspaceAvailableAgentsFileOutputSchema = z.object({
|
|
134
|
+
path: z.string(),
|
|
135
|
+
});
|
|
136
|
+
function sendJsonRpcError(res, status, code, message) {
|
|
137
|
+
res.status(status).json({
|
|
138
|
+
jsonrpc: "2.0",
|
|
139
|
+
error: { code, message },
|
|
140
|
+
id: null,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function requestLogFields(req, config) {
|
|
144
|
+
return {
|
|
145
|
+
ip: requestIp(req, config.logging.trustProxy),
|
|
146
|
+
host: req.header("host"),
|
|
147
|
+
userAgent: req.header("user-agent"),
|
|
148
|
+
origin: req.header("origin"),
|
|
149
|
+
referer: req.header("referer"),
|
|
150
|
+
contentLength: req.header("content-length"),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function assetBaseUrl(config) {
|
|
154
|
+
return `${config.publicBaseUrl.replace(/\/+$/, "")}/mcp-app-assets`;
|
|
155
|
+
}
|
|
156
|
+
function uiManifestUrl() {
|
|
157
|
+
return new URL("../dist/ui/.vite/manifest.json", import.meta.url);
|
|
158
|
+
}
|
|
159
|
+
function readWorkspaceAppManifest() {
|
|
160
|
+
return JSON.parse(readFileSync(uiManifestUrl(), "utf8"));
|
|
161
|
+
}
|
|
162
|
+
function getWorkspaceAppManifestEntry() {
|
|
163
|
+
const manifest = readWorkspaceAppManifest();
|
|
164
|
+
const entry = manifest[WORKSPACE_APP_MANIFEST_ENTRY];
|
|
165
|
+
if (!entry?.file) {
|
|
166
|
+
throw new Error(`Missing ${WORKSPACE_APP_MANIFEST_ENTRY} in UI manifest.`);
|
|
167
|
+
}
|
|
168
|
+
return entry;
|
|
169
|
+
}
|
|
170
|
+
function assetUrl(baseUrl, assetPath) {
|
|
171
|
+
return `${baseUrl}/${assetPath.replace(/^\/+/, "")}`;
|
|
172
|
+
}
|
|
173
|
+
function workspaceAppHtml(config) {
|
|
174
|
+
const baseUrl = assetBaseUrl(config);
|
|
175
|
+
const entry = getWorkspaceAppManifestEntry();
|
|
176
|
+
const stylesheets = (entry.css ?? [])
|
|
177
|
+
.map((stylesheet) => ` <link rel="stylesheet" crossorigin href="${assetUrl(baseUrl, stylesheet)}" />`)
|
|
178
|
+
.join("\n");
|
|
179
|
+
return `<!doctype html>
|
|
180
|
+
<html lang="en">
|
|
181
|
+
<head>
|
|
182
|
+
<meta charset="UTF-8" />
|
|
183
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
184
|
+
<title>Hearth Workspace</title>
|
|
185
|
+
<script type="module" crossorigin src="${assetUrl(baseUrl, entry.file)}"></script>
|
|
186
|
+
${stylesheets}
|
|
187
|
+
</head>
|
|
188
|
+
<body>
|
|
189
|
+
<main id="app" class="shell">
|
|
190
|
+
<section class="empty">Waiting for a tool result.</section>
|
|
191
|
+
</main>
|
|
192
|
+
</body>
|
|
193
|
+
</html>`;
|
|
194
|
+
}
|
|
195
|
+
function appCsp(config) {
|
|
196
|
+
const publicBaseUrl = config.publicBaseUrl.replace(/\/+$/, "");
|
|
197
|
+
return {
|
|
198
|
+
resourceDomains: [publicBaseUrl],
|
|
199
|
+
connectDomains: [publicBaseUrl],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function uiBuildDirectory() {
|
|
203
|
+
return fileURLToPath(new URL("../dist/ui", import.meta.url));
|
|
204
|
+
}
|
|
205
|
+
function setAssetHeaders(res) {
|
|
206
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
207
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
|
208
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Range");
|
|
209
|
+
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
210
|
+
}
|
|
211
|
+
async function assertWorkspaceAppAssets() {
|
|
212
|
+
const entry = getWorkspaceAppManifestEntry();
|
|
213
|
+
const candidates = [entry.file, ...(entry.css ?? [])].map((assetPath) => new URL(`../dist/ui/${assetPath}`, import.meta.url));
|
|
214
|
+
for (const candidate of candidates) {
|
|
215
|
+
await access(candidate);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, resolveLocalAgentProviders, incomingArtifactAdapters, trackToolActivity, taskStore) {
|
|
219
|
+
const toolSurface = getToolSurface(config.toolMode);
|
|
220
|
+
const server = new McpServer(mcpServerInfo(), {
|
|
221
|
+
instructions: serverInstructions(config, toolSurface),
|
|
222
|
+
});
|
|
223
|
+
registerMcpSurface(server, config, workspaces, reviewCheckpoints, processSessions, resolveLocalAgentProviders, incomingArtifactAdapters, trackToolActivity, taskStore);
|
|
224
|
+
return server;
|
|
225
|
+
}
|
|
226
|
+
function registerMcpSurface(server, config, workspaces, reviewCheckpoints, processSessions, resolveLocalAgentProviders, incomingArtifactAdapters, trackToolActivity, taskStore) {
|
|
227
|
+
const registrationTarget = trackToolActivity
|
|
228
|
+
? withTrackedToolHandlers(server, trackToolActivity)
|
|
229
|
+
: server;
|
|
230
|
+
const toolSurface = getToolSurface(config.toolMode);
|
|
231
|
+
registerAppResource(registrationTarget, "Hearth Diff Card", WORKSPACE_APP_URI, {
|
|
232
|
+
description: "Interactive card for viewing Hearth file diffs.",
|
|
233
|
+
_meta: {
|
|
234
|
+
ui: {
|
|
235
|
+
csp: appCsp(config),
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
}, async () => {
|
|
239
|
+
await assertWorkspaceAppAssets();
|
|
240
|
+
return {
|
|
241
|
+
contents: [
|
|
242
|
+
{
|
|
243
|
+
uri: WORKSPACE_APP_URI,
|
|
244
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
245
|
+
text: workspaceAppHtml(config),
|
|
246
|
+
_meta: {
|
|
247
|
+
ui: {
|
|
248
|
+
csp: appCsp(config),
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
registerAppTool(registrationTarget, "open_workspace", {
|
|
256
|
+
title: "Open workspace",
|
|
257
|
+
description: "Start work in a project directory or isolated worktree when no usable workspaceId exists for it. During continued work, reuse the existing workspaceId instead of calling this tool again. By default this uses the actual checkout; set mode=\"worktree\" for isolated or parallel work.",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
path: z
|
|
260
|
+
.string()
|
|
261
|
+
.describe("Absolute path, or a leading-tilde home path such as ~/project, to a project directory inside an allowed root."),
|
|
262
|
+
mode: z
|
|
263
|
+
.enum(["checkout", "worktree"])
|
|
264
|
+
.optional()
|
|
265
|
+
.describe("Defaults to checkout, which works in the actual directory. Use worktree for isolated or parallel Git work."),
|
|
266
|
+
baseRef: z
|
|
267
|
+
.string()
|
|
268
|
+
.optional()
|
|
269
|
+
.describe("Git ref to base a worktree on. Only used with mode=\"worktree\". Defaults to HEAD."),
|
|
270
|
+
},
|
|
271
|
+
outputSchema: {
|
|
272
|
+
workspaceId: z.string(),
|
|
273
|
+
root: z.string(),
|
|
274
|
+
mode: z.enum(["checkout", "worktree"]),
|
|
275
|
+
sourceRoot: z.string().optional(),
|
|
276
|
+
worktree: z
|
|
277
|
+
.object({
|
|
278
|
+
path: z.string(),
|
|
279
|
+
baseRef: z.string(),
|
|
280
|
+
baseSha: z.string(),
|
|
281
|
+
dirtySource: z.boolean(),
|
|
282
|
+
detached: z.boolean(),
|
|
283
|
+
managed: z.boolean(),
|
|
284
|
+
})
|
|
285
|
+
.optional(),
|
|
286
|
+
agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
|
|
287
|
+
availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
|
|
288
|
+
skills: z.array(workspaceSkillOutputSchema).optional(),
|
|
289
|
+
agentProviders: z.array(workspaceLocalAgentProviderOutputSchema).optional(),
|
|
290
|
+
agents: z.array(workspaceLocalAgentOutputSchema).optional(),
|
|
291
|
+
skillDiagnostics: z.array(z.unknown()).optional(),
|
|
292
|
+
review: z.discriminatedUnion("available", [
|
|
293
|
+
z.object({ available: z.literal(true) }),
|
|
294
|
+
z.object({
|
|
295
|
+
available: z.literal(false),
|
|
296
|
+
reason: z.string(),
|
|
297
|
+
}),
|
|
298
|
+
]),
|
|
299
|
+
instruction: z.string(),
|
|
300
|
+
},
|
|
301
|
+
...workspaceAppDescriptorMeta(config),
|
|
302
|
+
annotations: { readOnlyHint: true },
|
|
303
|
+
}, async ({ path, mode, baseRef }, { _meta }) => {
|
|
304
|
+
const startedAt = performance.now();
|
|
305
|
+
const { workspace, agentsFiles, availableAgentsFiles, workspaceReused, includeBootstrapContext, } = await workspaces.openWorkspace({ path, mode, baseRef }, { conversationScopeId: conversationScopeIdFromRequestMeta(_meta) });
|
|
306
|
+
const review = await reviewCheckpoints.initializeWorkspace({
|
|
307
|
+
workspaceId: workspace.id,
|
|
308
|
+
root: workspace.root,
|
|
309
|
+
});
|
|
310
|
+
const cardSkills = workspace.skills
|
|
311
|
+
.filter((skill) => !skill.disableModelInvocation)
|
|
312
|
+
.map((skill) => ({
|
|
313
|
+
name: skill.name,
|
|
314
|
+
description: skill.description,
|
|
315
|
+
path: formatPathForPrompt(skill.filePath),
|
|
316
|
+
}));
|
|
317
|
+
const agentCatalog = buildLocalAgentCatalog(config.subagents, workspace.agentProfiles, resolveLocalAgentProviders());
|
|
318
|
+
const cardAgentProviders = agentCatalog.providers
|
|
319
|
+
.filter((provider) => provider.usable)
|
|
320
|
+
.map((provider) => ({
|
|
321
|
+
id: provider.id,
|
|
322
|
+
model: provider.model,
|
|
323
|
+
effort: provider.effort,
|
|
324
|
+
note: provider.note,
|
|
325
|
+
}));
|
|
326
|
+
const cardAgents = agentCatalog.profiles;
|
|
327
|
+
const cardAgentsFiles = agentsFiles.map((file) => ({
|
|
328
|
+
path: formatAgentsPath(file.path, workspace.root),
|
|
329
|
+
content: file.content,
|
|
330
|
+
}));
|
|
331
|
+
const cardAvailableAgentsFiles = availableAgentsFiles.map((file) => ({
|
|
332
|
+
path: formatAgentsPath(file.path, workspace.root),
|
|
333
|
+
}));
|
|
334
|
+
const visibleSkills = includeBootstrapContext ? cardSkills : [];
|
|
335
|
+
const visibleAgentProviders = includeBootstrapContext ? cardAgentProviders : [];
|
|
336
|
+
const visibleAgents = includeBootstrapContext ? cardAgents : [];
|
|
337
|
+
const loadedAgentsFiles = includeBootstrapContext ? cardAgentsFiles : [];
|
|
338
|
+
const availableAgentsFileOutputs = includeBootstrapContext ? cardAvailableAgentsFiles : [];
|
|
339
|
+
const cardInstruction = config.skillsEnabled
|
|
340
|
+
? "Use this workspaceId for subsequent work in this project. Keep reusing it while working in this project. Follow loaded agentsFiles instructions. Before working under a path listed in availableAgentsFiles, read that instruction file. When a task matches an available skill in skills, read its path before proceeding."
|
|
341
|
+
: "Use this workspaceId for subsequent work in this project. Keep reusing it while working in this project. Follow loaded agentsFiles instructions. Before working under a path listed in availableAgentsFiles, read that instruction file.";
|
|
342
|
+
const instruction = workspaceReused
|
|
343
|
+
? [
|
|
344
|
+
`Workspace already open as ${workspace.id}.`,
|
|
345
|
+
"Continue with this workspaceId.",
|
|
346
|
+
"Keep following the project instructions, nested instruction files, skills, agent profiles, and diagnostics already provided for this workspace.",
|
|
347
|
+
].join("\n\n")
|
|
348
|
+
: workspace.mode === "worktree"
|
|
349
|
+
? "Use this workspaceId for subsequent work in this isolated worktree. Keep reusing it while working in this worktree. Follow the project instructions, nested instruction files, skills, agent profiles, and diagnostics returned for it."
|
|
350
|
+
: cardInstruction;
|
|
351
|
+
const resultContent = [
|
|
352
|
+
{
|
|
353
|
+
type: "text",
|
|
354
|
+
text: [
|
|
355
|
+
workspaceReused
|
|
356
|
+
? `Workspace already open as ${workspace.id}.`
|
|
357
|
+
: workspace.mode === "worktree"
|
|
358
|
+
? `Opened isolated worktree workspace ${workspace.id}.`
|
|
359
|
+
: `Opened workspace ${workspace.id}.`,
|
|
360
|
+
`Root: ${workspace.root}`,
|
|
361
|
+
`Mode: ${workspace.mode}`,
|
|
362
|
+
loadedAgentsFiles.length > 0
|
|
363
|
+
? `Loaded project instructions: ${loadedAgentsFiles.map((file) => file.path).join(", ")}`
|
|
364
|
+
: undefined,
|
|
365
|
+
availableAgentsFileOutputs.length > 0
|
|
366
|
+
? `Available nested instructions: ${availableAgentsFileOutputs.map((file) => file.path).join(", ")}`
|
|
367
|
+
: undefined,
|
|
368
|
+
visibleSkills.length > 0
|
|
369
|
+
? `Available skills: ${visibleSkills.map((skill) => skill.name).join(", ")}`
|
|
370
|
+
: undefined,
|
|
371
|
+
visibleAgentProviders.length > 0
|
|
372
|
+
? `Available subagent providers: ${visibleAgentProviders.map(formatAvailableAgentProvider).join(", ")}`
|
|
373
|
+
: undefined,
|
|
374
|
+
visibleAgents.length > 0
|
|
375
|
+
? `Available subagent profiles: ${visibleAgents.map(formatVisibleAgent).join(", ")}`
|
|
376
|
+
: undefined,
|
|
377
|
+
instruction,
|
|
378
|
+
].filter(Boolean).join("\n"),
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
logToolCall(config, {
|
|
382
|
+
tool: "open_workspace",
|
|
383
|
+
workspaceId: workspace.id,
|
|
384
|
+
path: workspace.root,
|
|
385
|
+
success: true,
|
|
386
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
content: resultContent,
|
|
390
|
+
_meta: {
|
|
391
|
+
card: {
|
|
392
|
+
workspaceId: workspace.id,
|
|
393
|
+
root: workspace.root,
|
|
394
|
+
path: workspace.root,
|
|
395
|
+
mode: workspace.mode,
|
|
396
|
+
workspaceReused,
|
|
397
|
+
includeBootstrapContext,
|
|
398
|
+
sourceRoot: workspace.sourceRoot,
|
|
399
|
+
worktree: workspace.worktree,
|
|
400
|
+
agentsFiles: cardAgentsFiles,
|
|
401
|
+
availableAgentsFiles: cardAvailableAgentsFiles,
|
|
402
|
+
skills: cardSkills,
|
|
403
|
+
agentProviders: cardAgentProviders,
|
|
404
|
+
agents: cardAgents,
|
|
405
|
+
review,
|
|
406
|
+
instruction: cardInstruction,
|
|
407
|
+
summary: {
|
|
408
|
+
mode: workspace.mode,
|
|
409
|
+
agentsFiles: cardAgentsFiles.length,
|
|
410
|
+
availableAgentsFiles: cardAvailableAgentsFiles.length,
|
|
411
|
+
skills: cardSkills.length,
|
|
412
|
+
agentProviders: cardAgentProviders.length,
|
|
413
|
+
agents: cardAgents.length,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
structuredContent: {
|
|
418
|
+
workspaceId: workspace.id,
|
|
419
|
+
root: workspace.root,
|
|
420
|
+
mode: workspace.mode,
|
|
421
|
+
sourceRoot: workspace.sourceRoot,
|
|
422
|
+
worktree: workspace.worktree,
|
|
423
|
+
review,
|
|
424
|
+
...(includeBootstrapContext
|
|
425
|
+
? {
|
|
426
|
+
agentsFiles: loadedAgentsFiles,
|
|
427
|
+
availableAgentsFiles: availableAgentsFileOutputs,
|
|
428
|
+
skills: visibleSkills,
|
|
429
|
+
agentProviders: visibleAgentProviders,
|
|
430
|
+
agents: visibleAgents,
|
|
431
|
+
skillDiagnostics: workspace.skillDiagnostics,
|
|
432
|
+
}
|
|
433
|
+
: {}),
|
|
434
|
+
instruction,
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
registrationTarget.registerTool(toolNames.read, {
|
|
439
|
+
title: "Read file",
|
|
440
|
+
description: [
|
|
441
|
+
"Read a file in a workspace. Use this for file inspection instead of shell commands like cat or sed.",
|
|
442
|
+
"Use this tool to inspect relevant AGENTS.md or CLAUDE.md files listed by open_workspace before working in nested directories.",
|
|
443
|
+
config.skillsEnabled
|
|
444
|
+
? "If available skills were returned and a task matches one, read that skill's path before proceeding. Skill paths may be outside the workspace; files within advertised skill directories are readable."
|
|
445
|
+
: "",
|
|
446
|
+
]
|
|
447
|
+
.filter(Boolean)
|
|
448
|
+
.join(" "),
|
|
449
|
+
inputSchema: {
|
|
450
|
+
workspaceId: z
|
|
451
|
+
.string()
|
|
452
|
+
.describe(workspaceIdDescription),
|
|
453
|
+
path: z
|
|
454
|
+
.string()
|
|
455
|
+
.describe(config.skillsEnabled
|
|
456
|
+
? "File path to read, relative to the workspace root. May also be an advertised skill path from open_workspace skills."
|
|
457
|
+
: "File path to read, relative to the workspace root."),
|
|
458
|
+
offset: z
|
|
459
|
+
.number()
|
|
460
|
+
.int()
|
|
461
|
+
.positive()
|
|
462
|
+
.optional()
|
|
463
|
+
.describe("1-indexed line number to start reading from."),
|
|
464
|
+
limit: z
|
|
465
|
+
.number()
|
|
466
|
+
.int()
|
|
467
|
+
.positive()
|
|
468
|
+
.optional()
|
|
469
|
+
.describe("Maximum number of lines to read."),
|
|
470
|
+
},
|
|
471
|
+
outputSchema: resultOutputSchema(),
|
|
472
|
+
annotations: { readOnlyHint: true },
|
|
473
|
+
}, async ({ workspaceId, ...input }) => {
|
|
474
|
+
const startedAt = performance.now();
|
|
475
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
476
|
+
const readPath = workspaces.resolveReadPath(workspace, input.path);
|
|
477
|
+
const response = await readFileTool({ ...input, path: readPath.absolutePath }, {
|
|
478
|
+
cwd: workspace.root,
|
|
479
|
+
root: workspace.root,
|
|
480
|
+
readRoots: readPath.readRoots,
|
|
481
|
+
});
|
|
482
|
+
if (response.isError) {
|
|
483
|
+
logFailedToolResponse(config, {
|
|
484
|
+
tool: toolNames.read,
|
|
485
|
+
workspaceId,
|
|
486
|
+
path: input.path,
|
|
487
|
+
}, response.content, startedAt);
|
|
488
|
+
return response;
|
|
489
|
+
}
|
|
490
|
+
logToolCall(config, {
|
|
491
|
+
tool: toolNames.read,
|
|
492
|
+
workspaceId,
|
|
493
|
+
path: input.path,
|
|
494
|
+
success: true,
|
|
495
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
...response,
|
|
499
|
+
structuredContent: {
|
|
500
|
+
result: contentText(response.content),
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
toolSurface.register({
|
|
505
|
+
server: registrationTarget,
|
|
506
|
+
config,
|
|
507
|
+
workspaces,
|
|
508
|
+
processSessions,
|
|
509
|
+
});
|
|
510
|
+
registerAppTool(registrationTarget, "show_changes", {
|
|
511
|
+
title: "Show changes",
|
|
512
|
+
description: "Show the changes made in this turn for an open workspace. Call this once after the final related file change and before your final response so the user can review the combined diff. Do not call it after each individual file change.",
|
|
513
|
+
inputSchema: {
|
|
514
|
+
workspaceId: z.string().describe(workspaceIdDescription),
|
|
515
|
+
},
|
|
516
|
+
outputSchema: resultOutputSchema({
|
|
517
|
+
workspaceId: z.string(),
|
|
518
|
+
reviewRef: z.string().regex(/^[0-9a-f]{40,64}$/),
|
|
519
|
+
}),
|
|
520
|
+
...workspaceAppDescriptorMeta(config),
|
|
521
|
+
annotations: { readOnlyHint: true },
|
|
522
|
+
}, async ({ workspaceId }, { _meta }) => {
|
|
523
|
+
const startedAt = performance.now();
|
|
524
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
525
|
+
const reviewRef = typeof _meta?.["hearth/reviewRef"] === "string"
|
|
526
|
+
? _meta["hearth/reviewRef"]
|
|
527
|
+
: undefined;
|
|
528
|
+
const review = reviewRef
|
|
529
|
+
? await reviewCheckpoints.reviewByRef({
|
|
530
|
+
workspaceId,
|
|
531
|
+
root: workspace.root,
|
|
532
|
+
reviewRef,
|
|
533
|
+
})
|
|
534
|
+
: await reviewCheckpoints.reviewChanges({
|
|
535
|
+
workspaceId,
|
|
536
|
+
root: workspace.root,
|
|
537
|
+
markReviewed: true,
|
|
538
|
+
});
|
|
539
|
+
const content = [textBlock(review.result)];
|
|
540
|
+
logToolCall(config, {
|
|
541
|
+
tool: "show_changes",
|
|
542
|
+
workspaceId,
|
|
543
|
+
success: true,
|
|
544
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
545
|
+
});
|
|
546
|
+
return {
|
|
547
|
+
content,
|
|
548
|
+
_meta: {
|
|
549
|
+
card: {
|
|
550
|
+
workspaceId,
|
|
551
|
+
summary: review.summary,
|
|
552
|
+
files: review.files,
|
|
553
|
+
payload: {
|
|
554
|
+
patch: review.patch,
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
structuredContent: {
|
|
559
|
+
workspaceId,
|
|
560
|
+
reviewRef: review.reviewRef,
|
|
561
|
+
result: contentText(content),
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
registerAgentTools(registrationTarget, { config, workspaces, taskStore });
|
|
566
|
+
registerContextTools(registrationTarget, { config, workspaces });
|
|
567
|
+
if (taskStore) {
|
|
568
|
+
registerTaskTools(registrationTarget, { config, workspaces, processSessions, taskStore });
|
|
569
|
+
}
|
|
570
|
+
if (config.artifactsEnabled && isArtifactDownloadSupportedPlatform()) {
|
|
571
|
+
registerArtifactTools(registrationTarget, {
|
|
572
|
+
config,
|
|
573
|
+
workspaces,
|
|
574
|
+
incomingArtifactAdapters,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
registrationTarget.registerTool("create_snapshot", {
|
|
578
|
+
title: "Create snapshot",
|
|
579
|
+
description: "Capture a restorable snapshot of the workspace working tree (tracked and untracked files, not git-ignored ones) as a git ref. Use before risky multi-step changes so the work can be restored later.",
|
|
580
|
+
inputSchema: {
|
|
581
|
+
workspaceId: z.string().describe(workspaceIdDescription),
|
|
582
|
+
label: z
|
|
583
|
+
.string()
|
|
584
|
+
.trim()
|
|
585
|
+
.min(1)
|
|
586
|
+
.max(120)
|
|
587
|
+
.optional()
|
|
588
|
+
.describe("Short label describing the upcoming change."),
|
|
589
|
+
},
|
|
590
|
+
outputSchema: resultOutputSchema({
|
|
591
|
+
snapshotId: z.string(),
|
|
592
|
+
label: z.string(),
|
|
593
|
+
}),
|
|
594
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
595
|
+
}, async ({ workspaceId, label }) => {
|
|
596
|
+
const startedAt = performance.now();
|
|
597
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
598
|
+
try {
|
|
599
|
+
const snapshot = await createSnapshot({
|
|
600
|
+
root: workspace.root,
|
|
601
|
+
workspaceId,
|
|
602
|
+
label: label ?? "manual",
|
|
603
|
+
});
|
|
604
|
+
const result = `Snapshot created: ${snapshot.snapshotId}`;
|
|
605
|
+
logToolCall(config, {
|
|
606
|
+
tool: "create_snapshot",
|
|
607
|
+
workspaceId,
|
|
608
|
+
success: true,
|
|
609
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
610
|
+
});
|
|
611
|
+
return {
|
|
612
|
+
content: [textBlock(result)],
|
|
613
|
+
structuredContent: { result, snapshotId: snapshot.snapshotId, label: snapshot.label },
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
logFailedToolResponse(config, {
|
|
618
|
+
tool: "create_snapshot",
|
|
619
|
+
workspaceId,
|
|
620
|
+
}, [textBlock(error instanceof Error ? error.message : String(error))], startedAt);
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
registrationTarget.registerTool("list_snapshots", {
|
|
625
|
+
title: "List snapshots",
|
|
626
|
+
description: "List restorable snapshots previously captured for this workspace.",
|
|
627
|
+
inputSchema: {
|
|
628
|
+
workspaceId: z.string().describe(workspaceIdDescription),
|
|
629
|
+
},
|
|
630
|
+
outputSchema: resultOutputSchema(),
|
|
631
|
+
annotations: { readOnlyHint: true },
|
|
632
|
+
}, async ({ workspaceId }) => {
|
|
633
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
634
|
+
const records = await listSnapshots({ root: workspace.root, workspaceId });
|
|
635
|
+
const lines = records.length > 0
|
|
636
|
+
? records.map((record) => `${record.snapshotId} ${record.createdAt} ${record.label}`).join("\n")
|
|
637
|
+
: "No snapshots for this workspace yet.";
|
|
638
|
+
return {
|
|
639
|
+
content: [textBlock(lines)],
|
|
640
|
+
structuredContent: {
|
|
641
|
+
result: lines,
|
|
642
|
+
snapshots: records.map((record) => ({
|
|
643
|
+
snapshotId: record.snapshotId,
|
|
644
|
+
label: record.label,
|
|
645
|
+
createdAt: record.createdAt,
|
|
646
|
+
})),
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
});
|
|
650
|
+
registrationTarget.registerTool("rollback_snapshot", {
|
|
651
|
+
title: "Rollback to snapshot",
|
|
652
|
+
description: "Restore the workspace working tree to a previously captured snapshot. Files changed since the snapshot are reverted (including deletions and creations); git-ignored files and the repository HEAD are untouched. Tier-2 policy applies like shell commands.",
|
|
653
|
+
inputSchema: {
|
|
654
|
+
workspaceId: z.string().describe(workspaceIdDescription),
|
|
655
|
+
snapshotId: z
|
|
656
|
+
.string()
|
|
657
|
+
.regex(/^[0-9a-f]{40,64}$/)
|
|
658
|
+
.describe("Snapshot ID returned by create_snapshot or list_snapshots."),
|
|
659
|
+
approvedByUser: z
|
|
660
|
+
.boolean()
|
|
661
|
+
.optional()
|
|
662
|
+
.describe("Set to true only after the user explicitly approved this rollback in the conversation. Required in supervised mode."),
|
|
663
|
+
},
|
|
664
|
+
outputSchema: resultOutputSchema(),
|
|
665
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
666
|
+
}, async ({ workspaceId, snapshotId, approvedByUser }) => {
|
|
667
|
+
const startedAt = performance.now();
|
|
668
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
669
|
+
const decision = decideExecution({
|
|
670
|
+
mode: config.execution.mode,
|
|
671
|
+
tier: 2,
|
|
672
|
+
approvedByUser,
|
|
673
|
+
});
|
|
674
|
+
logEvent(config.logging, decision.decision === "allow" ? "info" : "warn", "policy_decision", {
|
|
675
|
+
tool: "rollback_snapshot",
|
|
676
|
+
workspaceId,
|
|
677
|
+
tier: 2,
|
|
678
|
+
decision: decision.decision,
|
|
679
|
+
mode: config.execution.mode,
|
|
680
|
+
approvalClaimed: decision.approvalClaimed ?? false,
|
|
681
|
+
});
|
|
682
|
+
if (decision.decision !== "allow") {
|
|
683
|
+
return {
|
|
684
|
+
content: [textBlock(decision.reason)],
|
|
685
|
+
isError: true,
|
|
686
|
+
structuredContent: { result: decision.reason, policy: { decision: decision.decision, tier: 2 } },
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const rollback = await rollbackSnapshot({
|
|
690
|
+
root: workspace.root,
|
|
691
|
+
workspaceId,
|
|
692
|
+
snapshotId,
|
|
693
|
+
});
|
|
694
|
+
const result = rollback.files > 0
|
|
695
|
+
? `Rolled back ${rollback.files} file(s) to snapshot ${snapshotId}.`
|
|
696
|
+
: `Workspace already matches snapshot ${snapshotId}.`;
|
|
697
|
+
logToolCall(config, {
|
|
698
|
+
tool: "rollback_snapshot",
|
|
699
|
+
workspaceId,
|
|
700
|
+
success: true,
|
|
701
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
702
|
+
});
|
|
703
|
+
return {
|
|
704
|
+
content: [textBlock(result)],
|
|
705
|
+
structuredContent: { result, files: rollback.files },
|
|
706
|
+
};
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
function withTrackedToolHandlers(server, trackToolActivity) {
|
|
710
|
+
return {
|
|
711
|
+
registerTool: ((...args) => {
|
|
712
|
+
const handler = args.at(-1);
|
|
713
|
+
return server.registerTool(...args.slice(0, -1), (...handlerArgs) => trackToolActivity(() => Promise.resolve(handler(...handlerArgs))));
|
|
714
|
+
}),
|
|
715
|
+
registerResource: server.registerResource.bind(server),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
export function createServer(config = loadConfig(), options = {}) {
|
|
719
|
+
const incomingArtifactAdapters = options.incomingArtifactAdapters
|
|
720
|
+
?? [createOpenAIIncomingArtifactAdapter()];
|
|
721
|
+
const allowedHosts = config.allowedHosts.includes("*")
|
|
722
|
+
? undefined
|
|
723
|
+
: Array.from(new Set([config.host, ...config.allowedHosts]));
|
|
724
|
+
const app = createMcpExpressApp({
|
|
725
|
+
host: config.host,
|
|
726
|
+
...(allowedHosts ? { allowedHosts } : {}),
|
|
727
|
+
});
|
|
728
|
+
const mcpUrl = new URL("/mcp", config.publicBaseUrl);
|
|
729
|
+
const resourceServerUrl = resourceUrlFromServerUrl(mcpUrl);
|
|
730
|
+
const rateLimiters = createAuthRateLimiters(config.oauth.trustProxy);
|
|
731
|
+
const ownerTokenFailureLimiter = new RateLimiter({
|
|
732
|
+
keyPrefix: "owner-token-failure",
|
|
733
|
+
rule: { limit: 8, windowMs: 60 * 60 * 1000 },
|
|
734
|
+
});
|
|
735
|
+
const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl, config.stateDir, ownerTokenFailureLimiter);
|
|
736
|
+
const bearerAuth = requireBearerAuth({
|
|
737
|
+
verifier: oauthProvider,
|
|
738
|
+
requiredScopes: [config.oauth.scopes[0] ?? "hearth"],
|
|
739
|
+
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(resourceServerUrl),
|
|
740
|
+
});
|
|
741
|
+
const workspaceStore = createWorkspaceStore(config.stateDir);
|
|
742
|
+
const workspaces = new WorkspaceRegistry(config, workspaceStore);
|
|
743
|
+
const taskStore = new TaskStore(config.stateDir);
|
|
744
|
+
taskStore.reconcileOnBoot();
|
|
745
|
+
const reviewCheckpoints = createReviewCheckpointManager();
|
|
746
|
+
const processJournalDir = `${config.stateDir}/process-journal`;
|
|
747
|
+
void import("./process-journal.js").then(({ reapProcessJournal }) => reapProcessJournal(processJournalDir, (event, details) => logEvent(config.logging, "warn", event, details)).then((report) => {
|
|
748
|
+
if (report.reaped > 0 || report.errors.length > 0) {
|
|
749
|
+
logEvent(config.logging, "warn", "orphan_reap_boot", {
|
|
750
|
+
reaped: report.reaped,
|
|
751
|
+
skipped: report.skipped,
|
|
752
|
+
errors: report.errors.slice(0, 5),
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}).catch(() => undefined)).catch(() => undefined);
|
|
756
|
+
const processSessions = new ProcessSessionManager({
|
|
757
|
+
environment: {
|
|
758
|
+
allowAll: config.execution.envAllowAll,
|
|
759
|
+
extraAllowlist: config.execution.envAllowlist,
|
|
760
|
+
},
|
|
761
|
+
journalDir: processJournalDir,
|
|
762
|
+
...(config.execution.sandbox === "none" && config.workspaceProfiles.every((profile) => profile.sandbox !== "auto")
|
|
763
|
+
? {}
|
|
764
|
+
: {
|
|
765
|
+
commandWrapper: (shell, ctx) => {
|
|
766
|
+
const resolved = resolveExecutionForWorkspace(config, ctx.workspaceRoot);
|
|
767
|
+
return wrapCommandWithSandbox(shell, probeSandboxAdapter(), {
|
|
768
|
+
workspaceRoot: ctx.workspaceRoot,
|
|
769
|
+
allowNetwork: (resolved.sandboxNetwork ?? config.execution.sandboxNetwork) === "allow",
|
|
770
|
+
});
|
|
771
|
+
},
|
|
772
|
+
}),
|
|
773
|
+
});
|
|
774
|
+
const toolActivities = new ToolActivityTracker();
|
|
775
|
+
const localAgentProviders = buildLocalAgentProviderStatuses(config.subagents, getLocalAgentProviderAvailabilitySnapshot());
|
|
776
|
+
const resolveLocalAgentProviders = () => buildLocalAgentProviderStatuses(config.subagents, getLocalAgentProviderAvailabilitySnapshot());
|
|
777
|
+
const modernToolSurface = getToolSurface(config.toolMode);
|
|
778
|
+
const bindModernMcpSurface = compileMcpRegistrationSurface((target) => {
|
|
779
|
+
registerMcpSurface(target, config, workspaces, reviewCheckpoints, processSessions, resolveLocalAgentProviders, incomingArtifactAdapters, toolActivities.track, taskStore);
|
|
780
|
+
});
|
|
781
|
+
const logMcpHandlerError = (error) => logEvent(config.logging, "error", "mcp_handler_error", modernMcpAdapterErrorLogFields(error));
|
|
782
|
+
const mcpHandler = createMcpHandler(() => {
|
|
783
|
+
const adapter = createModernMcpServerAdapter(mcpServerInfo(), { instructions: serverInstructions(config, modernToolSurface) });
|
|
784
|
+
bindModernMcpSurface(adapter.registrationTarget);
|
|
785
|
+
return adapter.server;
|
|
786
|
+
}, {
|
|
787
|
+
legacy: "stateless",
|
|
788
|
+
onerror: logMcpHandlerError,
|
|
789
|
+
});
|
|
790
|
+
const mcpNodeHandler = toNodeHandler(mcpHandler, {
|
|
791
|
+
onerror: logMcpHandlerError,
|
|
792
|
+
});
|
|
793
|
+
if (config.logging.trustProxy) {
|
|
794
|
+
app.set("trust proxy", true);
|
|
795
|
+
}
|
|
796
|
+
app.use((req, res, next) => {
|
|
797
|
+
const requestId = randomUUID();
|
|
798
|
+
const startedAt = performance.now();
|
|
799
|
+
res.locals.requestId = requestId;
|
|
800
|
+
res.on("finish", () => {
|
|
801
|
+
const path = requestPath(req);
|
|
802
|
+
if (!config.logging.requests)
|
|
803
|
+
return;
|
|
804
|
+
if (!config.logging.assets && path.startsWith("/mcp-app-assets"))
|
|
805
|
+
return;
|
|
806
|
+
logEvent(config.logging, "info", "http_request", {
|
|
807
|
+
requestId,
|
|
808
|
+
method: req.method,
|
|
809
|
+
path,
|
|
810
|
+
status: res.statusCode,
|
|
811
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
812
|
+
...requestLogFields(req, config),
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
next();
|
|
816
|
+
});
|
|
817
|
+
app.use("/authorize", rateLimiters.authorizeIpMiddleware);
|
|
818
|
+
app.use("/register", rateLimiters.registerIpMiddleware);
|
|
819
|
+
app.use("/token", rateLimiters.tokenIpMiddleware);
|
|
820
|
+
app.use(mcpAuthRouter({
|
|
821
|
+
provider: oauthProvider,
|
|
822
|
+
issuerUrl: new URL(config.oauthIssuerUrl),
|
|
823
|
+
baseUrl: new URL(config.oauthIssuerUrl),
|
|
824
|
+
resourceServerUrl,
|
|
825
|
+
scopesSupported: config.oauth.scopes,
|
|
826
|
+
resourceName: "Hearth",
|
|
827
|
+
}));
|
|
828
|
+
app.options("/mcp-app-assets/{*asset}", (_req, res) => {
|
|
829
|
+
setAssetHeaders(res);
|
|
830
|
+
res.sendStatus(204);
|
|
831
|
+
});
|
|
832
|
+
app.use("/mcp-app-assets", express.static(uiBuildDirectory(), {
|
|
833
|
+
immutable: true,
|
|
834
|
+
maxAge: "1y",
|
|
835
|
+
fallthrough: false,
|
|
836
|
+
setHeaders: setAssetHeaders,
|
|
837
|
+
}));
|
|
838
|
+
app.get("/healthz", (_req, res) => {
|
|
839
|
+
let machineId;
|
|
840
|
+
try {
|
|
841
|
+
machineId = loadMachineIdentity(config.stateDir).id;
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
machineId = undefined;
|
|
845
|
+
}
|
|
846
|
+
res.json({
|
|
847
|
+
ok: true,
|
|
848
|
+
name: "hearth",
|
|
849
|
+
...(machineId ? { machineId } : {}),
|
|
850
|
+
mcp: new URL("/mcp", config.publicBaseUrl).toString(),
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
// ACME HTTP-01 challenge passthrough for relay-free TLS: serve only
|
|
854
|
+
// /.well-known/acme-challenge/* from the configured directory so certbot
|
|
855
|
+
// webroot mode works while the server runs. Nothing else is exposed here.
|
|
856
|
+
if (config.tls.acmeDir) {
|
|
857
|
+
const acmeDir = expandHomePath(config.tls.acmeDir);
|
|
858
|
+
app.use("/.well-known/acme-challenge", express.static(acmeDir, { fallthrough: false, maxAge: 0 }));
|
|
859
|
+
}
|
|
860
|
+
app.get("/", (_req, res) => {
|
|
861
|
+
res.sendFile("landing.html", { root: dashboardDirectory() });
|
|
862
|
+
});
|
|
863
|
+
registerDashboard(app, {
|
|
864
|
+
config,
|
|
865
|
+
workspaces,
|
|
866
|
+
taskStore,
|
|
867
|
+
resolveLocalAgentProviders,
|
|
868
|
+
});
|
|
869
|
+
app.all("/mcp", async (req, res) => {
|
|
870
|
+
const requestId = res.locals.requestId;
|
|
871
|
+
await new Promise((resolve, reject) => {
|
|
872
|
+
bearerAuth(req, res, (error) => {
|
|
873
|
+
if (error)
|
|
874
|
+
reject(error);
|
|
875
|
+
else
|
|
876
|
+
resolve();
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
if (res.headersSent)
|
|
880
|
+
return;
|
|
881
|
+
if (!req.auth?.resource || !checkResourceAllowed({ requestedResource: req.auth.resource, configuredResource: resourceServerUrl })) {
|
|
882
|
+
logEvent(config.logging, "warn", "auth_denied", {
|
|
883
|
+
requestId,
|
|
884
|
+
method: req.method,
|
|
885
|
+
path: requestPath(req),
|
|
886
|
+
reason: "invalid_oauth_resource",
|
|
887
|
+
...requestLogFields(req, config),
|
|
888
|
+
});
|
|
889
|
+
sendJsonRpcError(res, 401, -32001, "Unauthorized");
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
logEvent(config.logging, "debug", "mcp_request", {
|
|
893
|
+
requestId,
|
|
894
|
+
method: req.method,
|
|
895
|
+
});
|
|
896
|
+
try {
|
|
897
|
+
await mcpNodeHandler(req, res, req.body);
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
logEvent(config.logging, "error", "mcp_request_error", {
|
|
901
|
+
requestId,
|
|
902
|
+
error: error instanceof Error ? error.message : String(error),
|
|
903
|
+
});
|
|
904
|
+
if (!res.headersSent) {
|
|
905
|
+
sendJsonRpcError(res, 500, -32603, "Internal server error");
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
let closePromise;
|
|
910
|
+
return {
|
|
911
|
+
app,
|
|
912
|
+
config,
|
|
913
|
+
localAgentProviders,
|
|
914
|
+
close: () => {
|
|
915
|
+
closePromise ??= (async () => {
|
|
916
|
+
try {
|
|
917
|
+
await mcpHandler.close();
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
logEvent(config.logging, "warn", "mcp_handler_close_failed", {
|
|
921
|
+
error: error instanceof Error ? error.message : String(error),
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
await toolActivities.waitForIdle(SHUTDOWN_DRAIN_TIMEOUT_MS);
|
|
925
|
+
if (toolActivities.activeCount() > 0) {
|
|
926
|
+
logEvent(config.logging, "warn", "shutdown_drain_timeout", {
|
|
927
|
+
activeToolCalls: toolActivities.activeCount(),
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
processSessions.shutdown();
|
|
931
|
+
oauthProvider.close();
|
|
932
|
+
workspaceStore.close?.();
|
|
933
|
+
taskStore.close();
|
|
934
|
+
})();
|
|
935
|
+
return closePromise;
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
async function isMainModule() {
|
|
940
|
+
if (!process.argv[1])
|
|
941
|
+
return false;
|
|
942
|
+
const modulePath = await realpath(fileURLToPath(import.meta.url));
|
|
943
|
+
const entrypointPath = await realpath(process.argv[1]);
|
|
944
|
+
return modulePath === entrypointPath;
|
|
945
|
+
}
|
|
946
|
+
if (await isMainModule()) {
|
|
947
|
+
const { app, config, close, localAgentProviders } = createServer();
|
|
948
|
+
const httpServer = app.listen(config.port, config.host, () => {
|
|
949
|
+
console.log(`hearth listening on http://${config.host}:${config.port}/mcp`);
|
|
950
|
+
console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
|
|
951
|
+
console.log("auth: oauth owner-token flow required");
|
|
952
|
+
console.log(`logging: ${config.logging.level} ${config.logging.format}`);
|
|
953
|
+
console.log(`request logging: ${config.logging.requests ? "enabled" : "disabled"}`);
|
|
954
|
+
console.log(`asset logging: ${config.logging.assets ? "enabled" : "disabled"}`);
|
|
955
|
+
console.log(`trust proxy: ${config.logging.trustProxy ? "enabled" : "disabled"}`);
|
|
956
|
+
const artifactDownloadStatus = !config.artifactsEnabled
|
|
957
|
+
? "disabled"
|
|
958
|
+
: isArtifactDownloadSupportedPlatform()
|
|
959
|
+
? "enabled"
|
|
960
|
+
: `unsupported on ${process.platform}`;
|
|
961
|
+
console.log(`native artifact download: ${artifactDownloadStatus}`);
|
|
962
|
+
console.log(`subagent providers: ${formatLocalAgentProviderStatusSummary(localAgentProviders)}`);
|
|
963
|
+
});
|
|
964
|
+
let shuttingDown = false;
|
|
965
|
+
const shutdown = async () => {
|
|
966
|
+
if (shuttingDown)
|
|
967
|
+
return;
|
|
968
|
+
shuttingDown = true;
|
|
969
|
+
await shutdownHttpServer(httpServer, close);
|
|
970
|
+
process.exit(0);
|
|
971
|
+
};
|
|
972
|
+
const handleShutdown = () => {
|
|
973
|
+
void shutdown().catch((error) => {
|
|
974
|
+
console.error("hearth shutdown failed", error);
|
|
975
|
+
process.exit(1);
|
|
976
|
+
});
|
|
977
|
+
};
|
|
978
|
+
process.once("SIGINT", handleShutdown);
|
|
979
|
+
process.once("SIGTERM", handleShutdown);
|
|
980
|
+
}
|