@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.
Files changed (436) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -0
  3. package/bin/hearth-agentd.js +4 -0
  4. package/bin/hearth.js +4 -0
  5. package/bin/run-entrypoint.js +20 -0
  6. package/dist/agent-tools.js +738 -0
  7. package/dist/app-version.js +16 -0
  8. package/dist/apply-patch.js +402 -0
  9. package/dist/artifact-error.js +8 -0
  10. package/dist/artifact-tools.js +377 -0
  11. package/dist/cli-workspace.js +36 -0
  12. package/dist/cli.js +1086 -0
  13. package/dist/config-migration.js +82 -0
  14. package/dist/config-schema.js +136 -0
  15. package/dist/config.js +115 -0
  16. package/dist/context/context-tools.js +150 -0
  17. package/dist/context/repo-map.js +102 -0
  18. package/dist/dashboard/app.js +226 -0
  19. package/dist/dashboard/index.html +45 -0
  20. package/dist/dashboard/landing.html +57 -0
  21. package/dist/dashboard/styles.css +23 -0
  22. package/dist/dashboard.js +271 -0
  23. package/dist/db/client.js +29 -0
  24. package/dist/db/migrations.js +262 -0
  25. package/dist/db/schema.js +85 -0
  26. package/dist/device-tokens.js +61 -0
  27. package/dist/git-worktrees.js +134 -0
  28. package/dist/git.js +41 -0
  29. package/dist/incoming-artifacts.js +378 -0
  30. package/dist/local-agent-acp.js +768 -0
  31. package/dist/local-agent-adapters.js +20 -0
  32. package/dist/local-agent-availability.js +92 -0
  33. package/dist/local-agent-catalog.js +51 -0
  34. package/dist/local-agent-claude.js +334 -0
  35. package/dist/local-agent-client.js +519 -0
  36. package/dist/local-agent-codex.js +532 -0
  37. package/dist/local-agent-config.js +34 -0
  38. package/dist/local-agent-daemon-lifecycle.js +195 -0
  39. package/dist/local-agent-daemon-main.js +86 -0
  40. package/dist/local-agent-daemon-protocol.js +302 -0
  41. package/dist/local-agent-daemon.js +407 -0
  42. package/dist/local-agent-errors.js +378 -0
  43. package/dist/local-agent-grok.js +202 -0
  44. package/dist/local-agent-manager.js +715 -0
  45. package/dist/local-agent-opencode.js +463 -0
  46. package/dist/local-agent-path.js +24 -0
  47. package/dist/local-agent-pi-sandbox.js +472 -0
  48. package/dist/local-agent-pi.js +298 -0
  49. package/dist/local-agent-presentation.js +104 -0
  50. package/dist/local-agent-profiles.js +127 -0
  51. package/dist/local-agent-runtime-pool.js +481 -0
  52. package/dist/local-agent-runtime.js +1 -0
  53. package/dist/local-agent-store.js +193 -0
  54. package/dist/local-agent-targets.js +97 -0
  55. package/dist/logger.js +97 -0
  56. package/dist/machine-id.js +35 -0
  57. package/dist/mcp-modern-server.js +65 -0
  58. package/dist/oauth-provider.js +331 -0
  59. package/dist/oauth-store.js +144 -0
  60. package/dist/onboarding.js +35 -0
  61. package/dist/orchestration/brief.js +70 -0
  62. package/dist/orchestration/lanes.js +145 -0
  63. package/dist/orchestration/result.js +46 -0
  64. package/dist/orchestration/scope.js +61 -0
  65. package/dist/pi-tools.js +58 -0
  66. package/dist/policy/command-policy.js +104 -0
  67. package/dist/policy/env-allowlist.js +96 -0
  68. package/dist/policy/sandbox.js +68 -0
  69. package/dist/policy/workspace-profiles.js +47 -0
  70. package/dist/process-journal.js +116 -0
  71. package/dist/process-platform.js +49 -0
  72. package/dist/process-sessions.js +388 -0
  73. package/dist/rate-limit.js +77 -0
  74. package/dist/request-meta.js +9 -0
  75. package/dist/review-checkpoints.js +349 -0
  76. package/dist/roots.js +37 -0
  77. package/dist/server-shutdown.js +12 -0
  78. package/dist/server.js +980 -0
  79. package/dist/skills.js +84 -0
  80. package/dist/snapshot-manager.js +123 -0
  81. package/dist/stdio-server.js +80 -0
  82. package/dist/task-store.js +203 -0
  83. package/dist/task-tools.js +455 -0
  84. package/dist/tool-surfaces/claude.js +220 -0
  85. package/dist/tool-surfaces/codex.js +255 -0
  86. package/dist/tool-surfaces/index.js +15 -0
  87. package/dist/tool-surfaces/shared.js +228 -0
  88. package/dist/tool-surfaces/types.js +27 -0
  89. package/dist/ui/.vite/manifest.json +2544 -0
  90. package/dist/ui/assets/abap-CLvhMVsD.js +1 -0
  91. package/dist/ui/assets/actionscript-3--17pq3dv.js +1 -0
  92. package/dist/ui/assets/ada-C5qYipkI.js +1 -0
  93. package/dist/ui/assets/andromeeda-vGVdxbeo.js +1 -0
  94. package/dist/ui/assets/angular-html-DrbO3_Xe.js +1 -0
  95. package/dist/ui/assets/angular-ts-CmTVzHJx.js +1 -0
  96. package/dist/ui/assets/apache-U0d_L8uA.js +1 -0
  97. package/dist/ui/assets/apex-CGTLDQj6.js +1 -0
  98. package/dist/ui/assets/apl-BOpCpqXf.js +1 -0
  99. package/dist/ui/assets/applescript-CCn79oCD.js +1 -0
  100. package/dist/ui/assets/ara-4CJ0cIlV.js +1 -0
  101. package/dist/ui/assets/asciidoc-DE70LPWp.js +1 -0
  102. package/dist/ui/assets/asm-Cmm7eHzH.js +1 -0
  103. package/dist/ui/assets/astro-CxPeBwei.js +1 -0
  104. package/dist/ui/assets/aurora-x-CDeNXAV0.js +1 -0
  105. package/dist/ui/assets/awk-BWXHIvNe.js +1 -0
  106. package/dist/ui/assets/ayu-dark-DluEY0Gj.js +1 -0
  107. package/dist/ui/assets/ayu-light-C3h-C4tm.js +1 -0
  108. package/dist/ui/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  109. package/dist/ui/assets/ballerina-B7ZEbQpA.js +1 -0
  110. package/dist/ui/assets/bat-Bo4NYOV-.js +1 -0
  111. package/dist/ui/assets/beancount-D-usSTwE.js +1 -0
  112. package/dist/ui/assets/berry-DKpUyyne.js +1 -0
  113. package/dist/ui/assets/bibtex-Ci_nEsc7.js +1 -0
  114. package/dist/ui/assets/bicep-CUHmPFLl.js +1 -0
  115. package/dist/ui/assets/bird2-C2hNVINV.js +1 -0
  116. package/dist/ui/assets/blade-DtsAAG4P.js +1 -0
  117. package/dist/ui/assets/bsl-BkkzgIyY.js +1 -0
  118. package/dist/ui/assets/c-D9hLjNUM.js +1 -0
  119. package/dist/ui/assets/c3-CnJL0r0V.js +1 -0
  120. package/dist/ui/assets/cadence-CQ2zXKGN.js +1 -0
  121. package/dist/ui/assets/cairo-DLTphjLi.js +1 -0
  122. package/dist/ui/assets/catppuccin-frappe-3VR1Za6u.js +1 -0
  123. package/dist/ui/assets/catppuccin-latte-DwIHMF0Q.js +1 -0
  124. package/dist/ui/assets/catppuccin-macchiato-DYnBP6_5.js +1 -0
  125. package/dist/ui/assets/catppuccin-mocha-DYhrFGRu.js +1 -0
  126. package/dist/ui/assets/clarity-SemFz856.js +1 -0
  127. package/dist/ui/assets/clojure-DqKBuwfJ.js +1 -0
  128. package/dist/ui/assets/cmake-Bj61d0ZC.js +1 -0
  129. package/dist/ui/assets/cobol-C1SWlvZm.js +1 -0
  130. package/dist/ui/assets/codeowners-C8r90Shi.js +1 -0
  131. package/dist/ui/assets/codeql-oeQT6MSM.js +1 -0
  132. package/dist/ui/assets/coffee-B71R3v2U.js +1 -0
  133. package/dist/ui/assets/common-lisp-Cv5bFMCO.js +1 -0
  134. package/dist/ui/assets/coq-BrsZFFmf.js +1 -0
  135. package/dist/ui/assets/cpp--5a4c3v4.js +1 -0
  136. package/dist/ui/assets/crystal-AtqWYtE0.js +1 -0
  137. package/dist/ui/assets/csharp-Ct8U2NOr.js +1 -0
  138. package/dist/ui/assets/css-CbqKSHI-.js +1 -0
  139. package/dist/ui/assets/csv-Dx-8-gkx.js +1 -0
  140. package/dist/ui/assets/cue-CE9AQfxI.js +1 -0
  141. package/dist/ui/assets/cypher-ClKdZ_lG.js +1 -0
  142. package/dist/ui/assets/d-qD-0Kul2.js +1 -0
  143. package/dist/ui/assets/dark-plus-Cs2F2srj.js +1 -0
  144. package/dist/ui/assets/dart-DkHntEIa.js +1 -0
  145. package/dist/ui/assets/dax-BkyTk9wS.js +1 -0
  146. package/dist/ui/assets/desktop-Dlh5hvp9.js +1 -0
  147. package/dist/ui/assets/diff-woXpYk--.js +1 -0
  148. package/dist/ui/assets/docker-IyjqRm3v.js +1 -0
  149. package/dist/ui/assets/dotenv-_5a1GRtc.js +1 -0
  150. package/dist/ui/assets/dracula-BHWKrbxM.js +1 -0
  151. package/dist/ui/assets/dracula-soft-5eyTD99u.js +1 -0
  152. package/dist/ui/assets/dream-maker-DW3nJb8Q.js +1 -0
  153. package/dist/ui/assets/edge-BPwY_RrJ.js +1 -0
  154. package/dist/ui/assets/elixir-DVH2yLJf.js +1 -0
  155. package/dist/ui/assets/elm-BrleA6gS.js +1 -0
  156. package/dist/ui/assets/emacs-lisp-C9PiwqqW.js +1 -0
  157. package/dist/ui/assets/erb-2SHiYe30.js +1 -0
  158. package/dist/ui/assets/erlang-Cphh6RMH.js +1 -0
  159. package/dist/ui/assets/everforest-dark-sB-x3p7T.js +1 -0
  160. package/dist/ui/assets/everforest-light-Df2xbC6M.js +1 -0
  161. package/dist/ui/assets/fennel-DQxkIbk2.js +1 -0
  162. package/dist/ui/assets/fish-BJitypiv.js +1 -0
  163. package/dist/ui/assets/fluent-C03EYrpw.js +1 -0
  164. package/dist/ui/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
  165. package/dist/ui/assets/fortran-free-form-CYNrtFtB.js +1 -0
  166. package/dist/ui/assets/fsharp-D13ZGOAj.js +1 -0
  167. package/dist/ui/assets/gdresource-C0sCabJj.js +1 -0
  168. package/dist/ui/assets/gdscript-Cp2uCuqX.js +1 -0
  169. package/dist/ui/assets/gdshader-CBce3t8t.js +1 -0
  170. package/dist/ui/assets/genie-CV2tkWYe.js +1 -0
  171. package/dist/ui/assets/gherkin-DExj1W_8.js +1 -0
  172. package/dist/ui/assets/git-commit-BSykSTBG.js +1 -0
  173. package/dist/ui/assets/git-rebase-Bc7atMaV.js +1 -0
  174. package/dist/ui/assets/github-dark-C-LZuMrd.js +1 -0
  175. package/dist/ui/assets/github-dark-default-DXG-b-1a.js +1 -0
  176. package/dist/ui/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  177. package/dist/ui/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  178. package/dist/ui/assets/github-light-EUqPIrTm.js +1 -0
  179. package/dist/ui/assets/github-light-default-BXViO-2h.js +1 -0
  180. package/dist/ui/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  181. package/dist/ui/assets/gleam-CSRkHgEL.js +1 -0
  182. package/dist/ui/assets/glimmer-js-DXeTDPtW.js +1 -0
  183. package/dist/ui/assets/glimmer-ts-CEOoXHU4.js +1 -0
  184. package/dist/ui/assets/glsl-5-43-Gsg.js +1 -0
  185. package/dist/ui/assets/gn-ilITqXS6.js +1 -0
  186. package/dist/ui/assets/gnuplot-7GGW24-e.js +1 -0
  187. package/dist/ui/assets/go-BJwz_mda.js +1 -0
  188. package/dist/ui/assets/graphql-Bk7Ah1KA.js +1 -0
  189. package/dist/ui/assets/groovy-CacY0gHj.js +1 -0
  190. package/dist/ui/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  191. package/dist/ui/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  192. package/dist/ui/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  193. package/dist/ui/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  194. package/dist/ui/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  195. package/dist/ui/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  196. package/dist/ui/assets/hack-DUHz4rCJ.js +1 -0
  197. package/dist/ui/assets/haml-BPCuuTPR.js +1 -0
  198. package/dist/ui/assets/handlebars-CHxmUsnU.js +1 -0
  199. package/dist/ui/assets/haskell-D8IpX4py.js +1 -0
  200. package/dist/ui/assets/haxe-OTjmBuCE.js +1 -0
  201. package/dist/ui/assets/hcl-Dh228itO.js +1 -0
  202. package/dist/ui/assets/hjson-CxZEssPk.js +1 -0
  203. package/dist/ui/assets/hlsl-Cvrh5tZx.js +1 -0
  204. package/dist/ui/assets/horizon-CE9ld1lL.js +1 -0
  205. package/dist/ui/assets/horizon-bright-Br1oVSNq.js +1 -0
  206. package/dist/ui/assets/houston-CsvMBhTu.js +1 -0
  207. package/dist/ui/assets/html-BBJqizz0.js +1 -0
  208. package/dist/ui/assets/html-derivative-ShTPCcKN.js +1 -0
  209. package/dist/ui/assets/http-VFTyziwA.js +1 -0
  210. package/dist/ui/assets/hurl-B8yxTa4r.js +1 -0
  211. package/dist/ui/assets/hxml-B0Qn7Nwc.js +1 -0
  212. package/dist/ui/assets/hy-CZbG8q4J.js +1 -0
  213. package/dist/ui/assets/imba-DsUTQ-LC.js +1 -0
  214. package/dist/ui/assets/ini-B5eOa1yu.js +1 -0
  215. package/dist/ui/assets/java-BZ5DAgJZ.js +1 -0
  216. package/dist/ui/assets/javascript-C_zuIEaC.js +1 -0
  217. package/dist/ui/assets/jinja-DWd6xfNr.js +1 -0
  218. package/dist/ui/assets/jison-BbY63d1V.js +1 -0
  219. package/dist/ui/assets/json-C4DnajZs.js +1 -0
  220. package/dist/ui/assets/json5-BR5RXkoi.js +1 -0
  221. package/dist/ui/assets/jsonc-CYpm1nAK.js +1 -0
  222. package/dist/ui/assets/jsonl-CmCQp5Yx.js +1 -0
  223. package/dist/ui/assets/jsonnet-CJTPZ8u_.js +1 -0
  224. package/dist/ui/assets/jssm-DXw9l8Rf.js +1 -0
  225. package/dist/ui/assets/jsx-C4AACieZ.js +1 -0
  226. package/dist/ui/assets/julia-sRu7EFMW.js +1 -0
  227. package/dist/ui/assets/just-DpZ9Sx1v.js +1 -0
  228. package/dist/ui/assets/kanagawa-dragon-CXtmUGW6.js +1 -0
  229. package/dist/ui/assets/kanagawa-lotus-BN08jTvb.js +1 -0
  230. package/dist/ui/assets/kanagawa-wave-CTweb8Dz.js +1 -0
  231. package/dist/ui/assets/kdl-CsD5j6eV.js +1 -0
  232. package/dist/ui/assets/kotlin-DhhofPvG.js +1 -0
  233. package/dist/ui/assets/kusto-BUv0MjJC.js +1 -0
  234. package/dist/ui/assets/laserwave-C_8bwKvT.js +1 -0
  235. package/dist/ui/assets/latex-B6a_uum7.js +1 -0
  236. package/dist/ui/assets/lean-CewbzKMR.js +1 -0
  237. package/dist/ui/assets/less-DVTAwKKz.js +1 -0
  238. package/dist/ui/assets/light-plus-DVQuIRkW.js +1 -0
  239. package/dist/ui/assets/liquid-B2sVpjjr.js +1 -0
  240. package/dist/ui/assets/llvm-Cm23YOpf.js +1 -0
  241. package/dist/ui/assets/log-BNLmms1o.js +1 -0
  242. package/dist/ui/assets/logo-Cluzi2Zq.js +1 -0
  243. package/dist/ui/assets/lua-DxJJll61.js +1 -0
  244. package/dist/ui/assets/luau-CNKltnaQ.js +1 -0
  245. package/dist/ui/assets/make-Dixweg8N.js +1 -0
  246. package/dist/ui/assets/markdown-BYOwaDjH.js +1 -0
  247. package/dist/ui/assets/marko-Dx8mw-Wx.js +1 -0
  248. package/dist/ui/assets/material-theme-Bm3Qr25_.js +1 -0
  249. package/dist/ui/assets/material-theme-darker-2IIEA8gg.js +1 -0
  250. package/dist/ui/assets/material-theme-lighter-uhdI0v04.js +1 -0
  251. package/dist/ui/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  252. package/dist/ui/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  253. package/dist/ui/assets/matlab-D7qyCx1q.js +1 -0
  254. package/dist/ui/assets/mdc-vNKeXEwR.js +1 -0
  255. package/dist/ui/assets/mdx-DQZ5AkYe.js +1 -0
  256. package/dist/ui/assets/mermaid-Bk4SNUv9.js +1 -0
  257. package/dist/ui/assets/min-dark-BSWPekZh.js +1 -0
  258. package/dist/ui/assets/min-light-DDpmG2fV.js +1 -0
  259. package/dist/ui/assets/mipsasm-BMqwQI7S.js +1 -0
  260. package/dist/ui/assets/mojo-BgCJLMeH.js +1 -0
  261. package/dist/ui/assets/monokai-CdkpiU2Y.js +1 -0
  262. package/dist/ui/assets/moonbit-CaWjb8XO.js +1 -0
  263. package/dist/ui/assets/move-B1IS1UjX.js +1 -0
  264. package/dist/ui/assets/narrat-_X_XdTYD.js +1 -0
  265. package/dist/ui/assets/nextflow-Bbiyy34d.js +1 -0
  266. package/dist/ui/assets/nextflow-groovy-Dc_ddanL.js +1 -0
  267. package/dist/ui/assets/nginx-DScQAGLT.js +1 -0
  268. package/dist/ui/assets/night-owl-DhmEMT88.js +1 -0
  269. package/dist/ui/assets/night-owl-light-eJ-hLW7d.js +1 -0
  270. package/dist/ui/assets/nim-lAJch9uT.js +1 -0
  271. package/dist/ui/assets/nix-IvuFDN5E.js +1 -0
  272. package/dist/ui/assets/nord-Cb4Vim4T.js +1 -0
  273. package/dist/ui/assets/nushell-DcLAeLz5.js +1 -0
  274. package/dist/ui/assets/objective-c-D1A_Heim.js +1 -0
  275. package/dist/ui/assets/objective-cpp-BsSzOQcm.js +1 -0
  276. package/dist/ui/assets/ocaml-O90oeIOV.js +1 -0
  277. package/dist/ui/assets/odin-B1RWQWA5.js +1 -0
  278. package/dist/ui/assets/one-dark-pro-CLwyXe_n.js +1 -0
  279. package/dist/ui/assets/one-light-D7Lr4KcI.js +1 -0
  280. package/dist/ui/assets/openscad-BUDT5pXO.js +1 -0
  281. package/dist/ui/assets/pascal-4ZHwLPI5.js +1 -0
  282. package/dist/ui/assets/perl-C_GkVo_E.js +1 -0
  283. package/dist/ui/assets/php-DziV3K6J.js +1 -0
  284. package/dist/ui/assets/pierre-dark-CpLgRqie.js +1 -0
  285. package/dist/ui/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
  286. package/dist/ui/assets/pierre-dark-soft-kZQmAZld.js +1 -0
  287. package/dist/ui/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
  288. package/dist/ui/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
  289. package/dist/ui/assets/pierre-light-CoaEpmwp.js +1 -0
  290. package/dist/ui/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
  291. package/dist/ui/assets/pierre-light-soft-lWLdNTOI.js +1 -0
  292. package/dist/ui/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
  293. package/dist/ui/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
  294. package/dist/ui/assets/pkl-ot-7Btpt.js +1 -0
  295. package/dist/ui/assets/plastic-DQwYfKfQ.js +1 -0
  296. package/dist/ui/assets/plsql-DGHpHOYJ.js +1 -0
  297. package/dist/ui/assets/po-BiJDBrnU.js +1 -0
  298. package/dist/ui/assets/poimandres-DRFjx7u4.js +1 -0
  299. package/dist/ui/assets/polar-C7UOKdEL.js +1 -0
  300. package/dist/ui/assets/postcss-BXeXVLqQ.js +1 -0
  301. package/dist/ui/assets/powerquery-DNMTfnFr.js +1 -0
  302. package/dist/ui/assets/powershell-DshXNtvi.js +1 -0
  303. package/dist/ui/assets/prisma-BsRQq5mF.js +1 -0
  304. package/dist/ui/assets/prolog-iXnhIJG7.js +1 -0
  305. package/dist/ui/assets/proto-DB4EqR-F.js +1 -0
  306. package/dist/ui/assets/pug-z75UIWVK.js +1 -0
  307. package/dist/ui/assets/puppet-CDv2pdJW.js +1 -0
  308. package/dist/ui/assets/purescript-9MfHhQsQ.js +1 -0
  309. package/dist/ui/assets/python-gzcpVVnB.js +1 -0
  310. package/dist/ui/assets/qml-Cmm4coz1.js +1 -0
  311. package/dist/ui/assets/qmldir-DCQb3MpD.js +1 -0
  312. package/dist/ui/assets/qss-Fe1Jh2GI.js +1 -0
  313. package/dist/ui/assets/r-Brd0axBE.js +1 -0
  314. package/dist/ui/assets/racket-DcIDlBhZ.js +1 -0
  315. package/dist/ui/assets/raku-B3gFvitq.js +1 -0
  316. package/dist/ui/assets/razor-DSbwmoiu.js +1 -0
  317. package/dist/ui/assets/red-CJ3rzSJv.js +1 -0
  318. package/dist/ui/assets/reg-CRGYupPL.js +1 -0
  319. package/dist/ui/assets/regexp-Bs3Uk-pY.js +1 -0
  320. package/dist/ui/assets/rel-BtDbiS_P.js +1 -0
  321. package/dist/ui/assets/review-payload-CbwCBKB1.js +1580 -0
  322. package/dist/ui/assets/riscv-Ckw8ddFX.js +1 -0
  323. package/dist/ui/assets/ron-VUp2lXgN.js +1 -0
  324. package/dist/ui/assets/rose-pine-BthvhNj6.js +1 -0
  325. package/dist/ui/assets/rose-pine-dawn-Dg85fqjY.js +1 -0
  326. package/dist/ui/assets/rose-pine-moon-hon4tzzS.js +1 -0
  327. package/dist/ui/assets/rosmsg-CAekHB0j.js +1 -0
  328. package/dist/ui/assets/rst-ChIyKpUC.js +1 -0
  329. package/dist/ui/assets/ruby-BdAmvOMO.js +1 -0
  330. package/dist/ui/assets/rust-Cfkwpbl8.js +1 -0
  331. package/dist/ui/assets/sas-DpXn8hc5.js +1 -0
  332. package/dist/ui/assets/sass-DXrisJhu.js +1 -0
  333. package/dist/ui/assets/scala-DKOlJaKm.js +1 -0
  334. package/dist/ui/assets/scheme-DQCgrYNe.js +1 -0
  335. package/dist/ui/assets/scss-atX1Uuef.js +1 -0
  336. package/dist/ui/assets/sdbl-bTVj8UrX.js +1 -0
  337. package/dist/ui/assets/shaderlab-TOUzSsQk.js +1 -0
  338. package/dist/ui/assets/shellscript-BqgcAeMK.js +1 -0
  339. package/dist/ui/assets/shellsession-DfQSMRN5.js +1 -0
  340. package/dist/ui/assets/slack-dark-DnToyrRv.js +1 -0
  341. package/dist/ui/assets/slack-ochin-B2OO5cIa.js +1 -0
  342. package/dist/ui/assets/smalltalk-B16xEiuN.js +1 -0
  343. package/dist/ui/assets/snazzy-light-4G7pJPwS.js +1 -0
  344. package/dist/ui/assets/solarized-dark-DV17i1UV.js +1 -0
  345. package/dist/ui/assets/solarized-light-DSh2HLQt.js +1 -0
  346. package/dist/ui/assets/solidity-CKzVLygQ.js +1 -0
  347. package/dist/ui/assets/soy-DzP7zZTB.js +1 -0
  348. package/dist/ui/assets/sparql-D_iOobhT.js +1 -0
  349. package/dist/ui/assets/splunk-BC2Px7Mm.js +1 -0
  350. package/dist/ui/assets/sql-gmaLk66z.js +1 -0
  351. package/dist/ui/assets/ssh-config-BgfXC-Er.js +1 -0
  352. package/dist/ui/assets/stata-CJ0YTMcV.js +1 -0
  353. package/dist/ui/assets/stylus-B6D30XZt.js +1 -0
  354. package/dist/ui/assets/surrealql-LH7FciQz.js +1 -0
  355. package/dist/ui/assets/svelte-BME2PU6V.js +1 -0
  356. package/dist/ui/assets/swift-DonLKvLd.js +1 -0
  357. package/dist/ui/assets/synthwave-84-nFMaYfgc.js +1 -0
  358. package/dist/ui/assets/system-verilog-DJ5XKQeo.js +1 -0
  359. package/dist/ui/assets/systemd-BxMlprV5.js +1 -0
  360. package/dist/ui/assets/talonscript-CohzipZa.js +1 -0
  361. package/dist/ui/assets/tasl-DMoTqEGO.js +1 -0
  362. package/dist/ui/assets/tcl-CZd0xW_V.js +1 -0
  363. package/dist/ui/assets/templ-DHBNIfYW.js +1 -0
  364. package/dist/ui/assets/terraform-DswuEJGm.js +1 -0
  365. package/dist/ui/assets/tex-C8-kPEzO.js +1 -0
  366. package/dist/ui/assets/tokyo-night-oM2G3aXe.js +1 -0
  367. package/dist/ui/assets/toml-CcmNWLt0.js +1 -0
  368. package/dist/ui/assets/ts-tags-CiewV0vR.js +1 -0
  369. package/dist/ui/assets/tsv-sltzmVWM.js +1 -0
  370. package/dist/ui/assets/tsx-CUYPqmnq.js +1 -0
  371. package/dist/ui/assets/turtle-ByJddavk.js +1 -0
  372. package/dist/ui/assets/twig-BTkvsl0b.js +1 -0
  373. package/dist/ui/assets/typescript-DDQMaPwj.js +1 -0
  374. package/dist/ui/assets/typespec-B88KGewJ.js +1 -0
  375. package/dist/ui/assets/typst-DI99ib-x.js +1 -0
  376. package/dist/ui/assets/v-DETTlOr0.js +1 -0
  377. package/dist/ui/assets/vala-zf12oZj6.js +1 -0
  378. package/dist/ui/assets/vb-Djn5o6TS.js +1 -0
  379. package/dist/ui/assets/verilog-CiiDBU1e.js +1 -0
  380. package/dist/ui/assets/vesper-D5bVUKB1.js +1 -0
  381. package/dist/ui/assets/vhdl-BroJfC0k.js +1 -0
  382. package/dist/ui/assets/viml-DvXPmvsu.js +1 -0
  383. package/dist/ui/assets/vitesse-black-fwtXNY1n.js +1 -0
  384. package/dist/ui/assets/vitesse-dark-BZCL-v6S.js +1 -0
  385. package/dist/ui/assets/vitesse-light-VbXTXTou.js +1 -0
  386. package/dist/ui/assets/vue-BH5SEWRH.js +1 -0
  387. package/dist/ui/assets/vue-html-BT7yeYez.js +1 -0
  388. package/dist/ui/assets/vue-vine-Cs5IN2rs.js +1 -0
  389. package/dist/ui/assets/vyper-CgoNMtux.js +1 -0
  390. package/dist/ui/assets/wasm-BnjxR4X6.js +1 -0
  391. package/dist/ui/assets/wasm-ByWQv1Qj.js +1 -0
  392. package/dist/ui/assets/wenyan-C8pVoKbM.js +1 -0
  393. package/dist/ui/assets/wgsl-BsKzXJz4.js +1 -0
  394. package/dist/ui/assets/wikitext-ClFFjSW2.js +1 -0
  395. package/dist/ui/assets/wit-DdvCle-K.js +1 -0
  396. package/dist/ui/assets/wolfram-DLL8P-h_.js +1 -0
  397. package/dist/ui/assets/workspace-app-B_BnSEHO.css +1 -0
  398. package/dist/ui/assets/workspace-app-DSFpnrIC.js +115 -0
  399. package/dist/ui/assets/xml-D3_1BEcO.js +1 -0
  400. package/dist/ui/assets/xsl-BBOCDn4Y.js +1 -0
  401. package/dist/ui/assets/yaml-lThBDT45.js +1 -0
  402. package/dist/ui/assets/zenscript-BnlCZFoB.js +1 -0
  403. package/dist/ui/assets/zig-CMLA9XwU.js +1 -0
  404. package/dist/ui/workspace-app.html +15 -0
  405. package/dist/user-config.js +228 -0
  406. package/dist/verification.js +74 -0
  407. package/dist/workspace-store.js +133 -0
  408. package/dist/workspaces.js +433 -0
  409. package/docs/adr/devspace-x-implementation-adrs.md +159 -0
  410. package/docs/agent-profile-schema.md +203 -0
  411. package/docs/artifact-exchange.md +44 -0
  412. package/docs/assets/devspace-logo-light.png +0 -0
  413. package/docs/assets/devspace-screenshot.png +0 -0
  414. package/docs/assets/v11-review-ui-after.png +0 -0
  415. package/docs/assets/v11-review-ui-before.png +0 -0
  416. package/docs/chatgpt-coding-workflow.md +215 -0
  417. package/docs/configuration.md +217 -0
  418. package/docs/gotchas.md +294 -0
  419. package/docs/local-agent-daemon.md +101 -0
  420. package/docs/security.md +197 -0
  421. package/docs/setup.md +174 -0
  422. package/examples/agents/claude-implementer.md +30 -0
  423. package/examples/agents/codex-explorer.md +27 -0
  424. package/examples/agents/codex-qa-tester.md +29 -0
  425. package/examples/agents/codex-worker.md +25 -0
  426. package/examples/agents/copilot-reviewer.md +25 -0
  427. package/examples/agents/cursor-agent-worker.md +26 -0
  428. package/examples/agents/grok-builder.md +10 -0
  429. package/examples/agents/opencode-explorer.md +27 -0
  430. package/examples/agents/pi-reviewer.md +27 -0
  431. package/package.json +102 -0
  432. package/schema/v1/hearth.schema.json +529 -0
  433. package/scripts/copy-dashboard.mjs +9 -0
  434. package/scripts/fix-node-pty-permissions.mjs +22 -0
  435. package/scripts/generate-config-schema.ts +9 -0
  436. package/skills/subagents/SKILL.md +57 -0
@@ -0,0 +1,97 @@
1
+ import { isLocalAgentProvider, } from "./local-agent-profiles.js";
2
+ export function parseLocalAgentRunArgs(args) {
3
+ const parsed = parseAgentPromptArgs(args, 'Usage: hearth agents run <profile-or-provider> [--model <model>] [--effort <level>] "<prompt>"');
4
+ return parsed;
5
+ }
6
+ export function parseLocalAgentContinueArgs(args) {
7
+ const parsed = parseAgentPromptArgs(args, 'Usage: hearth agents continue <id> [--model <model>] [--effort <level>] "<prompt>"');
8
+ return { agentId: parsed.target, prompt: parsed.prompt, model: parsed.model, effort: parsed.effort };
9
+ }
10
+ function parseAgentPromptArgs(args, usage) {
11
+ const [target, ...rest] = args;
12
+ if (!target) {
13
+ throw new Error(usage);
14
+ }
15
+ let model;
16
+ let effort;
17
+ const promptParts = [];
18
+ let optionsEnded = false;
19
+ for (let index = 0; index < rest.length; index += 1) {
20
+ const part = rest[index];
21
+ if (!optionsEnded && part === "--") {
22
+ optionsEnded = true;
23
+ continue;
24
+ }
25
+ if (optionsEnded) {
26
+ promptParts.push(part ?? "");
27
+ continue;
28
+ }
29
+ if (part === "--model") {
30
+ const value = parseOptionValue(rest[index + 1], "--model");
31
+ model = value;
32
+ index += 1;
33
+ continue;
34
+ }
35
+ if (part?.startsWith("--model=")) {
36
+ const value = parseOptionValue(part.slice("--model=".length), "--model");
37
+ model = value;
38
+ continue;
39
+ }
40
+ if (part === "--effort") {
41
+ const value = parseOptionValue(rest[index + 1], "--effort");
42
+ effort = value;
43
+ index += 1;
44
+ continue;
45
+ }
46
+ if (part?.startsWith("--effort=")) {
47
+ const value = parseOptionValue(part.slice("--effort=".length), "--effort");
48
+ effort = value;
49
+ continue;
50
+ }
51
+ if (part?.startsWith("-")) {
52
+ throw unknownOptionError(part);
53
+ }
54
+ promptParts.push(part ?? "");
55
+ }
56
+ const prompt = promptParts.join(" ").trim();
57
+ if (!prompt) {
58
+ throw new Error(usage);
59
+ }
60
+ return { target, prompt, model, effort };
61
+ }
62
+ function parseOptionValue(value, option) {
63
+ const trimmed = value?.trim();
64
+ if (!trimmed)
65
+ throw new Error(`Missing value for ${option}.`);
66
+ if (trimmed.startsWith("-"))
67
+ throw unknownOptionError(trimmed);
68
+ return trimmed;
69
+ }
70
+ function unknownOptionError(option) {
71
+ return new Error(`Unknown option: ${option}. Use -- before prompt text that starts with a dash.`);
72
+ }
73
+ export function resolveLocalAgentTarget(target, profiles, modelOverride, effortOverride, providerConfigs = []) {
74
+ const profile = profiles.find((candidate) => candidate.name === target);
75
+ if (profile) {
76
+ const providerConfig = providerConfigs.find((entry) => entry.id === profile.provider);
77
+ return {
78
+ kind: "profile",
79
+ name: profile.name,
80
+ provider: profile.provider,
81
+ model: modelOverride ?? profile.model ?? providerConfig?.model,
82
+ effort: effortOverride ?? profile.effort ?? providerConfig?.effort,
83
+ profile,
84
+ };
85
+ }
86
+ if (isLocalAgentProvider(target)) {
87
+ const providerConfig = providerConfigs.find((entry) => entry.id === target);
88
+ return {
89
+ kind: "provider",
90
+ name: target,
91
+ provider: target,
92
+ model: modelOverride ?? providerConfig?.model,
93
+ effort: effortOverride ?? providerConfig?.effort,
94
+ };
95
+ }
96
+ return undefined;
97
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,97 @@
1
+ const RECENT_LOG_CAP = 200;
2
+ const recentLog = [];
3
+ const RECENT_SCALAR_KEYS = new Set(["tool", "workspaceId", "status", "method", "path"]);
4
+ const SENSITIVE_KEY_RE = /token|secret|password|credential|apikey|api_key|auth|cookie|session/i;
5
+ function toRecentEntry(entry) {
6
+ if (typeof entry.event !== "string")
7
+ return undefined;
8
+ const out = { ts: entry.ts, level: entry.level, event: entry.event };
9
+ for (const key of RECENT_SCALAR_KEYS) {
10
+ if (SENSITIVE_KEY_RE.test(key))
11
+ continue;
12
+ const value = entry[key];
13
+ if (typeof value === "string" && !SENSITIVE_KEY_RE.test(value)) {
14
+ out[key] = value.slice(0, 200);
15
+ }
16
+ }
17
+ if (typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs)) {
18
+ out.durationMs = Math.round(entry.durationMs);
19
+ }
20
+ return out;
21
+ }
22
+ export function recentLogEntries(limit = 100) {
23
+ const capped = Math.min(Math.max(1, Math.floor(limit)), RECENT_LOG_CAP);
24
+ return recentLog.slice(-capped);
25
+ }
26
+ const LEVEL_WEIGHT = {
27
+ silent: 0,
28
+ error: 1,
29
+ warn: 2,
30
+ info: 3,
31
+ debug: 4,
32
+ };
33
+ export function shouldLog(config, level) {
34
+ return LEVEL_WEIGHT[config.level] >= LEVEL_WEIGHT[level];
35
+ }
36
+ export function logEvent(config, level, event, fields = {}) {
37
+ if (!shouldLog(config, level))
38
+ return;
39
+ const entry = {
40
+ ts: new Date().toISOString(),
41
+ level,
42
+ event,
43
+ ...fields,
44
+ };
45
+ const recent = toRecentEntry(entry);
46
+ if (recent) {
47
+ recentLog.push(recent);
48
+ if (recentLog.length > RECENT_LOG_CAP)
49
+ recentLog.splice(0, recentLog.length - RECENT_LOG_CAP);
50
+ }
51
+ const line = config.format === "pretty" ? formatPretty(entry) : JSON.stringify(entry);
52
+ if (level === "error") {
53
+ console.error(line);
54
+ }
55
+ else if (level === "warn") {
56
+ console.warn(line);
57
+ }
58
+ else {
59
+ console.log(line);
60
+ }
61
+ }
62
+ export function requestIp(req, trustProxy) {
63
+ if (trustProxy) {
64
+ const cfConnectingIp = firstHeaderValue(req.header("cf-connecting-ip"));
65
+ if (cfConnectingIp)
66
+ return cfConnectingIp;
67
+ const forwardedFor = firstHeaderValue(req.header("x-forwarded-for"));
68
+ if (forwardedFor)
69
+ return forwardedFor;
70
+ }
71
+ return req.ip ?? req.socket.remoteAddress;
72
+ }
73
+ export function requestPath(req) {
74
+ return req.path || req.url.split("?")[0] || req.url;
75
+ }
76
+ export function commandPreview(command) {
77
+ const normalized = command.replace(/\s+/g, " ").trim();
78
+ return normalized.length > 120 ? `${normalized.slice(0, 117)}...` : normalized;
79
+ }
80
+ function firstHeaderValue(value) {
81
+ return value?.split(",")[0]?.trim() || undefined;
82
+ }
83
+ function formatPretty(entry) {
84
+ const ts = String(entry.ts);
85
+ const level = String(entry.level).toUpperCase();
86
+ const event = String(entry.event);
87
+ const rest = Object.entries(entry)
88
+ .filter(([key, value]) => !["ts", "level", "event"].includes(key) && value !== undefined)
89
+ .map(([key, value]) => `${key}=${formatPrettyValue(value)}`)
90
+ .join(" ");
91
+ return rest ? `${ts} ${level} ${event} ${rest}` : `${ts} ${level} ${event}`;
92
+ }
93
+ function formatPrettyValue(value) {
94
+ if (typeof value === "string")
95
+ return JSON.stringify(value);
96
+ return JSON.stringify(value);
97
+ }
@@ -0,0 +1,35 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { hostname, platform, arch } from "node:os";
4
+ import { join } from "node:path";
5
+ export function loadMachineIdentity(stateDir) {
6
+ const path = join(stateDir, "machine.json");
7
+ try {
8
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
9
+ if (typeof parsed.id === "string" && /^hearth-[0-9a-f]{12}$/.test(parsed.id)) {
10
+ return {
11
+ id: parsed.id,
12
+ hostname: hostname(),
13
+ platform: platform(),
14
+ arch: arch(),
15
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : new Date(0).toISOString(),
16
+ };
17
+ }
18
+ }
19
+ catch {
20
+ // Missing or corrupt: mint a fresh identity below.
21
+ }
22
+ const identity = {
23
+ id: `hearth-${randomBytes(6).toString("hex")}`,
24
+ hostname: hostname(),
25
+ platform: platform(),
26
+ arch: arch(),
27
+ createdAt: new Date().toISOString(),
28
+ };
29
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
30
+ writeFileSync(path, JSON.stringify(identity, null, 2), { mode: 0o600 });
31
+ return identity;
32
+ }
33
+ export function machineFileExists(stateDir) {
34
+ return existsSync(join(stateDir, "machine.json"));
35
+ }
@@ -0,0 +1,65 @@
1
+ import { McpServer, } from "@modelcontextprotocol/server";
2
+ export function createModernMcpServerAdapter(serverInfo, options) {
3
+ const server = new McpServer(serverInfo, options);
4
+ const registerModernTool = server.registerTool.bind(server);
5
+ const registerModernResource = server.registerResource.bind(server);
6
+ const registrationTarget = {
7
+ registerTool: ((name, definition, handler) => registerModernTool(name, definition, async (input, context) => handler(input, legacyToolHandlerExtra(context)))),
8
+ registerResource: ((...args) => {
9
+ const callback = args.at(-1);
10
+ return registerModernResource(...args.slice(0, -1), (...callbackArgs) => {
11
+ const context = callbackArgs.at(-1);
12
+ return callback(...callbackArgs.slice(0, -1), legacyToolHandlerExtra(context));
13
+ });
14
+ }),
15
+ };
16
+ return {
17
+ server,
18
+ registrationTarget,
19
+ };
20
+ }
21
+ export function compileMcpRegistrationSurface(registerSurface) {
22
+ const registrations = [];
23
+ const recordingTarget = {
24
+ registerTool: ((...args) => {
25
+ registrations.push((target) => {
26
+ target.registerTool(...args);
27
+ });
28
+ }),
29
+ registerResource: ((...args) => {
30
+ registrations.push((target) => {
31
+ target.registerResource(...args);
32
+ });
33
+ }),
34
+ };
35
+ registerSurface(recordingTarget);
36
+ const compiled = Object.freeze(registrations.slice());
37
+ return (target) => {
38
+ for (const replay of compiled)
39
+ replay(target);
40
+ };
41
+ }
42
+ export function modernMcpAdapterErrorLogFields(error) {
43
+ const cause = error.cause;
44
+ return {
45
+ error: error.message,
46
+ errorName: error.name,
47
+ ...(cause === undefined ? {} : {
48
+ cause: cause instanceof Error
49
+ ? { name: cause.name, message: cause.message }
50
+ : { name: typeof cause, message: String(cause) },
51
+ }),
52
+ };
53
+ }
54
+ function legacyToolHandlerExtra(context) {
55
+ return {
56
+ signal: context.mcpReq.signal,
57
+ authInfo: context.http?.authInfo,
58
+ sessionId: context.sessionId,
59
+ _meta: context.mcpReq._meta,
60
+ requestId: context.mcpReq.id,
61
+ requestInfo: context.http?.req,
62
+ sendNotification: context.mcpReq.notify,
63
+ sendRequest: context.mcpReq.send,
64
+ };
65
+ }
@@ -0,0 +1,331 @@
1
+ import { timingSafeEqual, randomBytes, randomUUID, createHash, createHmac } from "node:crypto";
2
+ import { AccessDeniedError, InvalidGrantError, InvalidRequestError, InvalidTokenError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
3
+ import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
4
+ import { requestIp } from "./logger.js";
5
+ import { DeviceTokenStore } from "./device-tokens.js";
6
+ import { SqliteOAuthClientsStore, SqliteOAuthStore } from "./oauth-store.js";
7
+ const CODE_TTL_MS = 5 * 60 * 1000;
8
+ function randomToken() {
9
+ return randomBytes(32).toString("base64url");
10
+ }
11
+ function safeEquals(a, b) {
12
+ const left = Buffer.from(a);
13
+ const right = Buffer.from(b);
14
+ if (left.byteLength !== right.byteLength)
15
+ return false;
16
+ return timingSafeEqual(left, right);
17
+ }
18
+ function htmlEscape(value) {
19
+ return value
20
+ .replaceAll("&", "&amp;")
21
+ .replaceAll("<", "&lt;")
22
+ .replaceAll(">", "&gt;")
23
+ .replaceAll('"', "&quot;")
24
+ .replaceAll("'", "&#39;");
25
+ }
26
+ function formHtml(params) {
27
+ const scopeText = params.scopes.length > 0 ? params.scopes.join(" ") : "hearth";
28
+ const resourceText = params.resource?.href ?? "Hearth MCP endpoint";
29
+ const error = params.error
30
+ ? `<p class="error">${htmlEscape(params.error)}</p>`
31
+ : "";
32
+ const hiddenFields = Object.entries({ ...params.fields, csrf_token: params.csrfToken })
33
+ .filter((entry) => entry[1] !== undefined)
34
+ .map(([name, value]) => ` <input type="hidden" name="${htmlEscape(name)}" value="${htmlEscape(value)}" />`)
35
+ .join("\n");
36
+ return `<!doctype html>
37
+ <html lang="en">
38
+ <head>
39
+ <meta charset="utf-8" />
40
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
41
+ <title>Connect Hearth</title>
42
+ <style>
43
+ body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; background: #0f172a; color: #e2e8f0; }
44
+ main { max-width: 440px; margin: 12vh auto; padding: 32px; background: #111827; border: 1px solid #334155; border-radius: 18px; box-shadow: 0 24px 80px rgba(0,0,0,.35); }
45
+ h1 { margin: 0 0 12px; font-size: 28px; }
46
+ p { line-height: 1.5; color: #cbd5e1; }
47
+ dl { padding: 16px; background: #020617; border-radius: 12px; }
48
+ dt { color: #94a3b8; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
49
+ dd { margin: 4px 0 12px; word-break: break-word; }
50
+ label { display: block; margin: 18px 0 8px; font-weight: 600; }
51
+ input { box-sizing: border-box; width: 100%; padding: 12px 14px; border-radius: 10px; border: 1px solid #475569; background: #020617; color: #e2e8f0; font-size: 16px; }
52
+ button { margin-top: 18px; width: 100%; border: 0; border-radius: 10px; padding: 12px 14px; font-weight: 700; color: #020617; background: #38bdf8; cursor: pointer; }
53
+ .error { color: #fecaca; background: #7f1d1d; border-radius: 10px; padding: 10px 12px; }
54
+ .warning { color: #fde68a; }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <main>
59
+ <h1>Connect Hearth</h1>
60
+ <p class="warning">Only approve this if you are intentionally connecting your own ChatGPT or MCP client to this local machine.</p>
61
+ ${error}
62
+ <dl>
63
+ <dt>Client</dt><dd>${htmlEscape(params.clientName)}</dd>
64
+ <dt>Scope</dt><dd>${htmlEscape(scopeText)}</dd>
65
+ <dt>Resource</dt><dd>${htmlEscape(resourceText)}</dd>
66
+ </dl>
67
+ <form method="post">
68
+ ${hiddenFields}
69
+ <label for="owner_token">Owner password</label>
70
+ <input id="owner_token" name="owner_token" type="password" autocomplete="current-password" autofocus required />
71
+ <button type="submit">Authorize Hearth</button>
72
+ </form>
73
+ </main>
74
+ </body>
75
+ </html>`;
76
+ }
77
+ function requestedScopesAllowed(requested, supported) {
78
+ return requested.every((scope) => supported.includes(scope));
79
+ }
80
+ const CONSENT_FRAME_HEADERS = {
81
+ "X-Frame-Options": "DENY",
82
+ "Content-Security-Policy": "frame-ancestors 'none'",
83
+ };
84
+ function setConsentHeaders(res) {
85
+ for (const [name, value] of Object.entries(CONSENT_FRAME_HEADERS)) {
86
+ res.setHeader(name, value);
87
+ }
88
+ }
89
+ function consentCsrfToken(config, fields) {
90
+ const key = createHash("sha256").update(`hearth-consent:${config.ownerToken}`).digest();
91
+ const canonical = JSON.stringify([
92
+ "hearth-consent-v1",
93
+ fields.client_id ?? "",
94
+ fields.redirect_uri ?? "",
95
+ fields.code_challenge ?? "",
96
+ fields.scope ?? "",
97
+ fields.state ?? "",
98
+ fields.resource ?? "",
99
+ ]);
100
+ return createHmac("sha256", key).update(canonical).digest("base64url");
101
+ }
102
+ function consentCsrfValid(config, body) {
103
+ const provided = typeof body.csrf_token === "string" ? body.csrf_token : "";
104
+ if (!provided)
105
+ return false;
106
+ const expected = consentCsrfToken(config, {
107
+ client_id: asString(body.client_id),
108
+ redirect_uri: asString(body.redirect_uri),
109
+ code_challenge: asString(body.code_challenge),
110
+ scope: asString(body.scope),
111
+ state: asString(body.state),
112
+ resource: asString(body.resource),
113
+ });
114
+ return safeEquals(provided, expected);
115
+ }
116
+ function asString(value) {
117
+ return typeof value === "string" ? value : undefined;
118
+ }
119
+ export class SingleUserOAuthProvider {
120
+ config;
121
+ ownerTokenFailureLimiter;
122
+ clientsStore;
123
+ codes = new Map();
124
+ oauthStore;
125
+ resourceServerUrl;
126
+ deviceTokens;
127
+ constructor(config, resourceServerUrl, stateDir, ownerTokenFailureLimiter) {
128
+ this.config = config;
129
+ this.ownerTokenFailureLimiter = ownerTokenFailureLimiter;
130
+ this.resourceServerUrl = resourceUrlFromServerUrl(resourceServerUrl);
131
+ this.oauthStore = new SqliteOAuthStore(stateDir);
132
+ this.deviceTokens = new DeviceTokenStore(stateDir);
133
+ this.clientsStore = new SqliteOAuthClientsStore(this.oauthStore, config.allowedRedirectHosts);
134
+ }
135
+ async authorize(client, params, res) {
136
+ if (!params.resource || !checkResourceAllowed({ requestedResource: params.resource, configuredResource: this.resourceServerUrl })) {
137
+ throw new InvalidRequestError("Invalid or missing OAuth resource");
138
+ }
139
+ if (!requestedScopesAllowed(params.scopes ?? [], this.config.scopes)) {
140
+ throw new InvalidRequestError("Requested scope is not supported");
141
+ }
142
+ if (res.req.method !== "POST") {
143
+ const fields = authorizationFormFields(client, params);
144
+ setConsentHeaders(res);
145
+ res.status(200).setHeader("Content-Type", "text/html; charset=utf-8");
146
+ res.send(formHtml({
147
+ clientName: client.client_name ?? client.client_id,
148
+ scopes: params.scopes ?? this.config.scopes,
149
+ resource: params.resource,
150
+ fields,
151
+ csrfToken: consentCsrfToken(this.config, fields),
152
+ }));
153
+ return;
154
+ }
155
+ if (!consentCsrfValid(this.config, res.req.body)) {
156
+ setConsentHeaders(res);
157
+ res.status(400).setHeader("Content-Type", "text/html; charset=utf-8");
158
+ res.send(formHtml({
159
+ error: "This approval request is invalid or stale. Restart the connection from your MCP client.",
160
+ clientName: client.client_name ?? client.client_id,
161
+ scopes: params.scopes ?? this.config.scopes,
162
+ resource: params.resource,
163
+ fields: authorizationFormFields(client, params),
164
+ csrfToken: consentCsrfToken(this.config, authorizationFormFields(client, params)),
165
+ }));
166
+ return;
167
+ }
168
+ const providedToken = String(res.req.body?.owner_token ?? "");
169
+ const failureKey = requestIp(res.req, this.config.trustProxy) ?? "unknown";
170
+ if (!safeEquals(providedToken, this.config.ownerToken)) {
171
+ if (this.ownerTokenFailureLimiter) {
172
+ const verdict = this.ownerTokenFailureLimiter.take(failureKey);
173
+ if (!verdict.allowed) {
174
+ setConsentHeaders(res);
175
+ res.setHeader("Retry-After", String(verdict.retryAfterSeconds));
176
+ res.status(429).setHeader("Content-Type", "text/html; charset=utf-8");
177
+ res.send(formHtml({
178
+ error: `Too many failed approval attempts. Try again in ${verdict.retryAfterSeconds} seconds.`,
179
+ clientName: client.client_name ?? client.client_id,
180
+ scopes: params.scopes ?? this.config.scopes,
181
+ resource: params.resource,
182
+ fields: authorizationFormFields(client, params),
183
+ csrfToken: consentCsrfToken(this.config, authorizationFormFields(client, params)),
184
+ }));
185
+ return;
186
+ }
187
+ }
188
+ setConsentHeaders(res);
189
+ res.status(401).setHeader("Content-Type", "text/html; charset=utf-8");
190
+ res.send(formHtml({
191
+ error: "The Owner password was not accepted.",
192
+ clientName: client.client_name ?? client.client_id,
193
+ scopes: params.scopes ?? this.config.scopes,
194
+ resource: params.resource,
195
+ fields: authorizationFormFields(client, params),
196
+ }));
197
+ return;
198
+ }
199
+ this.ownerTokenFailureLimiter?.reset(failureKey);
200
+ const code = `code-${randomUUID()}`;
201
+ this.codes.set(code, {
202
+ clientId: client.client_id,
203
+ params,
204
+ expiresAtMs: Date.now() + CODE_TTL_MS,
205
+ });
206
+ const redirectUrl = new URL(params.redirectUri);
207
+ redirectUrl.searchParams.set("code", code);
208
+ if (params.state !== undefined)
209
+ redirectUrl.searchParams.set("state", params.state);
210
+ res.redirect(302, redirectUrl.href);
211
+ }
212
+ async challengeForAuthorizationCode(client, authorizationCode) {
213
+ const record = this.validCodeRecord(client, authorizationCode);
214
+ return record.params.codeChallenge;
215
+ }
216
+ async exchangeAuthorizationCode(client, authorizationCode, _codeVerifier, redirectUri, resource) {
217
+ const record = this.validCodeRecord(client, authorizationCode);
218
+ if (redirectUri && redirectUri !== record.params.redirectUri) {
219
+ throw new InvalidGrantError("redirect_uri does not match the authorization request");
220
+ }
221
+ if (resource && !checkResourceAllowed({ requestedResource: resource, configuredResource: this.resourceServerUrl })) {
222
+ throw new InvalidGrantError("Invalid resource");
223
+ }
224
+ this.codes.delete(authorizationCode);
225
+ return this.issueTokens(client.client_id, record.params.scopes ?? this.config.scopes, record.params.resource);
226
+ }
227
+ async exchangeRefreshToken(client, refreshToken, scopes, resource) {
228
+ const refreshTokenHash = hashToken(refreshToken);
229
+ const record = this.oauthStore.getRefreshToken(refreshTokenHash);
230
+ if (!record || record.clientId !== client.client_id || record.expiresAt < Math.floor(Date.now() / 1000)) {
231
+ throw new InvalidGrantError("Invalid refresh token");
232
+ }
233
+ if (resource && !checkResourceAllowed({ requestedResource: resource, configuredResource: this.resourceServerUrl })) {
234
+ throw new InvalidGrantError("Invalid resource");
235
+ }
236
+ const requestedScopes = scopes ?? record.scopes;
237
+ if (!requestedScopes.every((scope) => record.scopes.includes(scope))) {
238
+ throw new AccessDeniedError("Refresh token cannot grant requested scopes");
239
+ }
240
+ return this.issueTokens(client.client_id, requestedScopes, resource ?? (record.resource ? new URL(record.resource) : undefined), refreshTokenHash);
241
+ }
242
+ async verifyAccessToken(token) {
243
+ const record = this.oauthStore.getAccessToken(hashToken(token));
244
+ if (!record || record.expiresAt < Math.floor(Date.now() / 1000)) {
245
+ // Local device tokens (created via `hearth token create`) carry the
246
+ // same authority; they bind to this server's protected resource.
247
+ const device = this.deviceTokens.verify(token);
248
+ if (device) {
249
+ return {
250
+ token,
251
+ clientId: `device:${device.name}`,
252
+ scopes: this.config.scopes,
253
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
254
+ resource: this.resourceServerUrl,
255
+ };
256
+ }
257
+ throw new InvalidTokenError("Invalid or expired access token");
258
+ }
259
+ return {
260
+ token,
261
+ clientId: record.clientId,
262
+ scopes: record.scopes,
263
+ expiresAt: record.expiresAt,
264
+ resource: record.resource ? new URL(record.resource) : undefined,
265
+ };
266
+ }
267
+ async revokeToken(_client, request) {
268
+ const hashed = hashToken(request.token);
269
+ this.oauthStore.deleteAccessToken(hashed);
270
+ this.oauthStore.deleteRefreshToken(hashed);
271
+ }
272
+ close() {
273
+ this.oauthStore.close();
274
+ this.deviceTokens.close();
275
+ }
276
+ validCodeRecord(client, authorizationCode) {
277
+ const record = this.codes.get(authorizationCode);
278
+ if (!record || record.clientId !== client.client_id || record.expiresAtMs < Date.now()) {
279
+ throw new InvalidGrantError("Invalid authorization code");
280
+ }
281
+ return record;
282
+ }
283
+ issueTokens(clientId, scopes, resource, consumedRefreshTokenHash) {
284
+ const now = Math.floor(Date.now() / 1000);
285
+ const accessToken = randomToken();
286
+ const refreshToken = randomToken();
287
+ const accessExpiresAt = now + this.config.accessTokenTtlSeconds;
288
+ const refreshExpiresAt = now + this.config.refreshTokenTtlSeconds;
289
+ const saved = this.oauthStore.saveTokenPair({
290
+ accessTokenHash: hashToken(accessToken),
291
+ accessToken: {
292
+ clientId,
293
+ scopes,
294
+ expiresAt: accessExpiresAt,
295
+ resource: resource?.href,
296
+ },
297
+ refreshTokenHash: hashToken(refreshToken),
298
+ refreshToken: {
299
+ clientId,
300
+ scopes,
301
+ expiresAt: refreshExpiresAt,
302
+ resource: resource?.href,
303
+ },
304
+ }, consumedRefreshTokenHash);
305
+ if (!saved) {
306
+ throw new InvalidGrantError("Invalid refresh token");
307
+ }
308
+ return {
309
+ access_token: accessToken,
310
+ token_type: "bearer",
311
+ expires_in: this.config.accessTokenTtlSeconds,
312
+ refresh_token: refreshToken,
313
+ scope: scopes.join(" "),
314
+ };
315
+ }
316
+ }
317
+ function authorizationFormFields(client, params) {
318
+ return {
319
+ response_type: "code",
320
+ client_id: client.client_id,
321
+ redirect_uri: params.redirectUri,
322
+ code_challenge: params.codeChallenge,
323
+ code_challenge_method: "S256",
324
+ scope: params.scopes?.join(" "),
325
+ state: params.state,
326
+ resource: params.resource?.href,
327
+ };
328
+ }
329
+ function hashToken(token) {
330
+ return createHash("sha256").update(token).digest("base64url");
331
+ }