@bitseek/hermes-webui 0.1.0-beta.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 (233) hide show
  1. package/README.md +213 -0
  2. package/bin/hermes-webui.mjs +588 -0
  3. package/package.json +25 -0
  4. package/scripts/sync-vendor.mjs +74 -0
  5. package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
  6. package/templates/systemd/hermes-webui.service +13 -0
  7. package/templates/windows/hermes-webui-task.ps1 +3 -0
  8. package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
  9. package/vendor/agent-frontend-shell/.dockerignore +7 -0
  10. package/vendor/agent-frontend-shell/.env.docker.example +89 -0
  11. package/vendor/agent-frontend-shell/.env.example +34 -0
  12. package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
  13. package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
  14. package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
  15. package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
  16. package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
  17. package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
  18. package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
  19. package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
  20. package/vendor/agent-frontend-shell/AGENTS.md +80 -0
  21. package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
  22. package/vendor/agent-frontend-shell/BUGS.md +52 -0
  23. package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
  24. package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
  25. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
  26. package/vendor/agent-frontend-shell/DESIGN.md +173 -0
  27. package/vendor/agent-frontend-shell/Dockerfile +91 -0
  28. package/vendor/agent-frontend-shell/LICENSE +21 -0
  29. package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
  30. package/vendor/agent-frontend-shell/README.md +705 -0
  31. package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
  32. package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
  33. package/vendor/agent-frontend-shell/TESTING.md +1932 -0
  34. package/vendor/agent-frontend-shell/THEMES.md +170 -0
  35. package/vendor/agent-frontend-shell/api/__init__.py +1 -0
  36. package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
  37. package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
  38. package/vendor/agent-frontend-shell/api/auth.py +592 -0
  39. package/vendor/agent-frontend-shell/api/background.py +87 -0
  40. package/vendor/agent-frontend-shell/api/clarify.py +238 -0
  41. package/vendor/agent-frontend-shell/api/commands.py +124 -0
  42. package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
  43. package/vendor/agent-frontend-shell/api/config.py +5178 -0
  44. package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
  45. package/vendor/agent-frontend-shell/api/extensions.py +253 -0
  46. package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
  47. package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
  48. package/vendor/agent-frontend-shell/api/goals.py +608 -0
  49. package/vendor/agent-frontend-shell/api/helpers.py +474 -0
  50. package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
  51. package/vendor/agent-frontend-shell/api/metering.py +194 -0
  52. package/vendor/agent-frontend-shell/api/models.py +4210 -0
  53. package/vendor/agent-frontend-shell/api/oauth.py +770 -0
  54. package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
  55. package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
  56. package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
  57. package/vendor/agent-frontend-shell/api/providers.py +2175 -0
  58. package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
  59. package/vendor/agent-frontend-shell/api/rollback.py +320 -0
  60. package/vendor/agent-frontend-shell/api/routes.py +13990 -0
  61. package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
  62. package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
  63. package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
  64. package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
  65. package/vendor/agent-frontend-shell/api/session_events.py +45 -0
  66. package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
  67. package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
  68. package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
  69. package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
  70. package/vendor/agent-frontend-shell/api/startup.py +128 -0
  71. package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
  72. package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
  73. package/vendor/agent-frontend-shell/api/system_health.py +167 -0
  74. package/vendor/agent-frontend-shell/api/terminal.py +410 -0
  75. package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
  76. package/vendor/agent-frontend-shell/api/updates.py +1261 -0
  77. package/vendor/agent-frontend-shell/api/upload.py +322 -0
  78. package/vendor/agent-frontend-shell/api/usage.py +26 -0
  79. package/vendor/agent-frontend-shell/api/workspace.py +867 -0
  80. package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
  81. package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
  82. package/vendor/agent-frontend-shell/bootstrap.py +492 -0
  83. package/vendor/agent-frontend-shell/ctl.sh +427 -0
  84. package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
  85. package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
  86. package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
  87. package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
  88. package/vendor/agent-frontend-shell/docker_init.bash +459 -0
  89. package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
  90. package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
  91. package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
  92. package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
  93. package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
  94. package/vendor/agent-frontend-shell/docs/docker.md +337 -0
  95. package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
  96. package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
  97. package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
  98. package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
  99. package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
  100. package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
  101. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
  102. package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
  103. package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
  104. package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
  105. package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
  106. package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
  107. package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
  108. package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
  109. package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
  110. package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
  111. package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
  112. package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
  113. package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
  114. package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
  115. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
  116. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
  117. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
  118. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
  119. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
  120. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
  121. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
  122. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
  123. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
  124. package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
  125. package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
  126. package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
  127. package/vendor/agent-frontend-shell/extensions/index.js +67 -0
  128. package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
  129. package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
  130. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
  131. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
  132. package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
  133. package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
  134. package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
  135. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
  136. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
  137. package/vendor/agent-frontend-shell/install.sh +63 -0
  138. package/vendor/agent-frontend-shell/mcp_server.py +567 -0
  139. package/vendor/agent-frontend-shell/package.json +12 -0
  140. package/vendor/agent-frontend-shell/pyproject.toml +56 -0
  141. package/vendor/agent-frontend-shell/pytest.ini +3 -0
  142. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  143. package/vendor/agent-frontend-shell/server.py +624 -0
  144. package/vendor/agent-frontend-shell/start.ps1 +210 -0
  145. package/vendor/agent-frontend-shell/start.sh +65 -0
  146. package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
  147. package/vendor/agent-frontend-shell/static/boot.js +1990 -0
  148. package/vendor/agent-frontend-shell/static/commands.js +1402 -0
  149. package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
  150. package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
  151. package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
  152. package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
  153. package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
  154. package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
  155. package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
  156. package/vendor/agent-frontend-shell/static/icons.js +92 -0
  157. package/vendor/agent-frontend-shell/static/index.html +1506 -0
  158. package/vendor/agent-frontend-shell/static/login.js +177 -0
  159. package/vendor/agent-frontend-shell/static/manifest.json +53 -0
  160. package/vendor/agent-frontend-shell/static/messages.js +3521 -0
  161. package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
  162. package/vendor/agent-frontend-shell/static/panels.js +7995 -0
  163. package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
  164. package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
  165. package/vendor/agent-frontend-shell/static/style.css +4774 -0
  166. package/vendor/agent-frontend-shell/static/sw.js +173 -0
  167. package/vendor/agent-frontend-shell/static/terminal.js +632 -0
  168. package/vendor/agent-frontend-shell/static/ui.js +8997 -0
  169. package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
  170. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
  171. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
  172. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  173. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  174. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  175. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  176. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  177. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  178. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  179. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  180. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  181. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  182. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  183. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  184. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  185. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
  186. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
  187. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
  188. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  189. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  190. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  191. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
  192. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
  193. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
  194. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
  195. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
  196. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
  197. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  198. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  199. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  200. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
  201. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
  202. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
  203. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  204. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  205. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  206. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  207. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  208. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  209. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  210. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  211. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  212. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
  213. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
  214. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
  215. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
  216. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
  217. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  218. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
  219. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
  220. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  221. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
  222. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
  223. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  224. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
  225. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
  226. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  227. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  228. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  229. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  230. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
  231. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
  232. package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
  233. package/vendor/agent-frontend-shell/static/workspace.js +680 -0
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env python3
2
+ """One-shot bootstrap launcher for Hermes Web UI."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import os
8
+ import platform
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ import urllib.error
14
+ import urllib.request
15
+ import venv
16
+ import webbrowser
17
+ from pathlib import Path
18
+
19
+
20
+ INSTALLER_URL = "https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh"
21
+ REPO_ROOT = Path(__file__).resolve().parent
22
+
23
+
24
+ def _load_repo_dotenv() -> None:
25
+ """Load REPO_ROOT/.env into os.environ.
26
+
27
+ Mirrors what start.sh does via ``set -a; source .env`` so that running
28
+ ``python3 bootstrap.py`` directly behaves identically to ``./start.sh``.
29
+ Variables are set unconditionally (matching shell source semantics), so a
30
+ value in .env overrides one already present in the shell environment.
31
+ ``ctl.sh`` sets HERMES_WEBUI_PRESERVE_ENV=1 when it has already resolved
32
+ launcher-specific values such as HERMES_HOME or HERMES_WEBUI_STATE_DIR.
33
+
34
+ Only loads the webui repo .env — not ~/.hermes/.env, which the server
35
+ loads independently at startup for provider credentials.
36
+
37
+ Note: does not handle the ``export FOO=bar`` prefix — strip ``export``
38
+ from .env values if copy-pasting from a shell rc file.
39
+ """
40
+ env_path = REPO_ROOT / ".env"
41
+ if not env_path.exists():
42
+ return
43
+ try:
44
+ preserve_existing = os.getenv("HERMES_WEBUI_PRESERVE_ENV", "").strip().lower() in {
45
+ "1",
46
+ "true",
47
+ "yes",
48
+ "on",
49
+ }
50
+ for raw_line in env_path.read_text(encoding="utf-8").splitlines():
51
+ line = raw_line.strip()
52
+ if not line or line.startswith("#") or "=" not in line:
53
+ continue
54
+ k, v = line.split("=", 1)
55
+ k = k.strip()
56
+ # Strip optional 'export' prefix (common in copy-pasted shell snippets)
57
+ if k.startswith("export "):
58
+ k = k[7:].strip()
59
+ v = v.strip().strip('"').strip("'")
60
+ if k:
61
+ if preserve_existing and k in os.environ:
62
+ continue
63
+ os.environ[k] = v
64
+ except Exception as exc:
65
+ import sys as _sys
66
+ print(f"[bootstrap] Warning: could not load .env — {exc}", file=_sys.stderr)
67
+
68
+
69
+ # Side effect: loads REPO_ROOT/.env into os.environ on import.
70
+ # Must run before DEFAULT_HOST / DEFAULT_PORT so os.getenv() picks up
71
+ # values from .env even when bootstrap.py is invoked directly (not via start.sh).
72
+ _load_repo_dotenv()
73
+
74
+ DEFAULT_HOST = os.getenv("HERMES_WEBUI_HOST", "127.0.0.1")
75
+ DEFAULT_PORT = int(os.getenv("HERMES_WEBUI_PORT", "8787"))
76
+ # Set HERMES_WEBUI_SKIP_ONBOARDING=1 to bypass the first-run wizard when
77
+ # the environment is already fully configured (e.g. managed hosting).
78
+
79
+
80
+ def info(msg: str) -> None:
81
+ print(f"[bootstrap] {msg}", flush=True)
82
+
83
+
84
+ def is_wsl() -> bool:
85
+ if platform.system() != "Linux":
86
+ return False
87
+ release = platform.release().lower()
88
+ return (
89
+ "microsoft" in release or "wsl" in release or bool(os.getenv("WSL_DISTRO_NAME"))
90
+ )
91
+
92
+
93
+ def ensure_supported_platform() -> None:
94
+ if platform.system() == "Windows" and not is_wsl():
95
+ raise RuntimeError(
96
+ "Native Windows is not supported for this bootstrap yet. "
97
+ "Please run it from Linux, macOS, or inside WSL2."
98
+ )
99
+
100
+
101
+ def _agent_dir_from_hermes_cli() -> Path | None:
102
+ """Resolve the agent install root by inspecting the `hermes` CLI shebang.
103
+
104
+ The Hermes Agent installer drops a `hermes` console-script in the user's
105
+ PATH whose shebang points at the agent's bundled venv:
106
+
107
+ #!/path/to/hermes-agent/venv/bin/python3
108
+
109
+ Walking up the parents until we find a directory that contains
110
+ `run_agent.py` recovers the install root regardless of where the user
111
+ chose to clone the agent (e.g. ~/Projects/GitHub/hermes-agent), which
112
+ the hard-coded candidate list in :func:`discover_agent_dir` cannot.
113
+
114
+ Last-resort only: this is invoked after every explicit candidate
115
+ (`HERMES_WEBUI_AGENT_DIR`, `$HERMES_HOME/hermes-agent`, etc.) has missed.
116
+ A stale clone in a known location still wins over the live `hermes` CLI
117
+ — that's intentional, since the candidate list is treated as
118
+ authoritative when present, and matches existing behavior.
119
+ """
120
+ hermes_path = shutil.which("hermes")
121
+ if not hermes_path:
122
+ return None
123
+ try:
124
+ with open(hermes_path, "r", encoding="utf-8", errors="replace") as f:
125
+ first_line = f.readline().strip()
126
+ except OSError:
127
+ return None
128
+ if not first_line.startswith("#!"):
129
+ return None
130
+ interp_field = first_line[2:].strip().split(None, 1)
131
+ if not interp_field:
132
+ return None
133
+ interp = Path(interp_field[0])
134
+ if not interp.is_absolute():
135
+ return None
136
+ for parent in interp.parents:
137
+ if (parent / "run_agent.py").exists():
138
+ return parent.resolve()
139
+ return None
140
+
141
+
142
+ def discover_agent_dir() -> Path | None:
143
+ home = Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes"))).expanduser()
144
+ candidates = [
145
+ os.getenv("HERMES_WEBUI_AGENT_DIR", ""),
146
+ str(home / "hermes-agent"),
147
+ str(REPO_ROOT.parent / "hermes-agent"),
148
+ str(Path.home() / ".hermes" / "hermes-agent"),
149
+ str(Path.home() / "hermes-agent"),
150
+ ]
151
+ for raw in candidates:
152
+ if not raw:
153
+ continue
154
+ candidate = Path(raw).expanduser().resolve()
155
+ if candidate.exists() and (candidate / "run_agent.py").exists():
156
+ return candidate
157
+ return _agent_dir_from_hermes_cli()
158
+
159
+
160
+ def discover_launcher_python(agent_dir: Path | None) -> str:
161
+ env_python = os.getenv("HERMES_WEBUI_PYTHON")
162
+ if env_python:
163
+ return env_python
164
+ if agent_dir:
165
+ for rel in ("venv/bin/python", "venv/Scripts/python.exe", ".venv/bin/python", ".venv/Scripts/python.exe"):
166
+ candidate = agent_dir / rel
167
+ if candidate.exists():
168
+ return str(candidate)
169
+ for rel in (".venv/bin/python", ".venv/Scripts/python.exe"):
170
+ candidate = REPO_ROOT / rel
171
+ if candidate.exists():
172
+ return str(candidate)
173
+ return shutil.which("python3") or shutil.which("python") or sys.executable
174
+
175
+
176
+ def _python_can_run_webui_and_agent(python_exe: str, agent_dir: Path | None = None) -> bool:
177
+ script = "import yaml\nfrom run_agent import AIAgent\n"
178
+ env = os.environ.copy()
179
+ if agent_dir:
180
+ # PREPEND agent_dir to PYTHONPATH so an `agent_dir/run_agent.py` wins
181
+ # over any stale `run_agent` package in system site-packages (sys.path
182
+ # order: script-dir → PYTHONPATH entries → site-packages). The
183
+ # "if PYTHONPATH unset" branch avoids a leading os.pathsep, which
184
+ # CPython would interpret as "current directory" — a footgun.
185
+ env["PYTHONPATH"] = (
186
+ str(agent_dir)
187
+ if not env.get("PYTHONPATH")
188
+ else f"{agent_dir}{os.pathsep}{env['PYTHONPATH']}"
189
+ )
190
+ check = subprocess.run(
191
+ [python_exe, "-c", script],
192
+ capture_output=True,
193
+ text=True,
194
+ env=env,
195
+ )
196
+ return check.returncode == 0
197
+
198
+
199
+ def ensure_python_has_webui_deps(python_exe: str, agent_dir: Path | None = None) -> str:
200
+ """Return a Python executable that can run both WebUI and Hermes Agent.
201
+
202
+ The WebUI can be launched directly with its local .venv. That venv has the
203
+ WebUI dependencies (for example PyYAML), but may not have Hermes Agent on its
204
+ import path. In that case the server starts healthy, then chat fails later
205
+ with "AIAgent not available". Prefer the agent venv when it is usable, and
206
+ validate the final interpreter before starting the server.
207
+ """
208
+ if _python_can_run_webui_and_agent(python_exe, agent_dir):
209
+ return python_exe
210
+
211
+ agent_candidates: list[Path] = []
212
+ if agent_dir:
213
+ for rel in (
214
+ "venv/bin/python",
215
+ "venv/Scripts/python.exe",
216
+ ".venv/bin/python",
217
+ ".venv/Scripts/python.exe",
218
+ ):
219
+ agent_candidates.append(agent_dir / rel)
220
+ for candidate in agent_candidates:
221
+ if str(candidate) != python_exe and candidate.exists():
222
+ if _python_can_run_webui_and_agent(str(candidate), agent_dir):
223
+ return str(candidate)
224
+
225
+ venv_dir = REPO_ROOT / ".venv"
226
+ venv_python = venv_dir / (
227
+ "Scripts/python.exe" if platform.system() == "Windows" else "bin/python"
228
+ )
229
+ if not venv_python.exists():
230
+ info(f"Creating local virtualenv at {venv_dir}")
231
+ # symlinks=True: some Python builds (notably mise/asdf shared-library
232
+ # installs on macOS) default venv to copy mode. The copied binary still
233
+ # uses @executable_path/../lib/libpython3.X.dylib for its load command,
234
+ # so the venv binary aborts with SIGABRT on first import because the
235
+ # dylib never gets copied into .venv/lib. Symlinking the interpreter
236
+ # keeps @executable_path resolving back to the original install.
237
+ # CPython's venv falls back to copy mode automatically when symlink
238
+ # creation fails (e.g. older Windows without SeCreateSymbolicLinkPrivilege),
239
+ # so this is safe to set unconditionally.
240
+ venv.EnvBuilder(with_pip=True, symlinks=True).create(venv_dir)
241
+
242
+ info("Installing WebUI dependencies into local virtualenv")
243
+ subprocess.run(
244
+ [str(venv_python), "-m", "pip", "install", "--quiet", "--upgrade", "pip"],
245
+ check=True,
246
+ )
247
+ subprocess.run(
248
+ [
249
+ str(venv_python),
250
+ "-m",
251
+ "pip",
252
+ "install",
253
+ "--quiet",
254
+ "-r",
255
+ str(REPO_ROOT / "requirements.txt"),
256
+ ],
257
+ check=True,
258
+ )
259
+ if _python_can_run_webui_and_agent(str(venv_python), agent_dir):
260
+ return str(venv_python)
261
+ raise RuntimeError(
262
+ "Python environment cannot import both WebUI dependencies and Hermes Agent. "
263
+ "Set HERMES_WEBUI_PYTHON to the Hermes Agent venv Python or install the "
264
+ "WebUI requirements into that environment."
265
+ )
266
+
267
+
268
+ def hermes_command_exists() -> bool:
269
+ return shutil.which("hermes") is not None
270
+
271
+
272
+ def install_hermes_agent() -> None:
273
+ info(f"Hermes Agent not found. Attempting install via {INSTALLER_URL}")
274
+ subprocess.run(
275
+ ["/bin/bash", "-lc", f"curl -fsSL {INSTALLER_URL} | bash"], check=True
276
+ )
277
+
278
+
279
+ def wait_for_health(url: str, timeout: float = 25.0) -> bool:
280
+ deadline = time.time() + timeout
281
+ # Validate URL scheme to prevent file:// and other dangerous schemes
282
+ if not url.startswith(("http://", "https://")):
283
+ raise ValueError(f"Invalid health check URL: {url}")
284
+ while time.time() < deadline:
285
+ try:
286
+ with urllib.request.urlopen(url, timeout=2) as response: # nosec B310
287
+ if b'"status": "ok"' in response.read():
288
+ return True
289
+ except Exception:
290
+ time.sleep(0.4)
291
+ return False
292
+
293
+
294
+ def open_browser(url: str) -> None:
295
+ try:
296
+ webbrowser.open(url)
297
+ except Exception as exc:
298
+ info(f"Could not open browser automatically: {exc}")
299
+
300
+
301
+ def parse_args() -> argparse.Namespace:
302
+ parser = argparse.ArgumentParser(description="Bootstrap Hermes Web UI onboarding.")
303
+ parser.add_argument("port", nargs="?", type=int, default=DEFAULT_PORT)
304
+ parser.add_argument("--host", default=DEFAULT_HOST)
305
+ parser.add_argument(
306
+ "--no-browser",
307
+ action="store_true",
308
+ help="Do not open a browser tab automatically.",
309
+ )
310
+ parser.add_argument(
311
+ "--skip-agent-install",
312
+ action="store_true",
313
+ help="Fail instead of attempting the official Hermes installer.",
314
+ )
315
+ parser.add_argument(
316
+ "--foreground",
317
+ action="store_true",
318
+ help=(
319
+ "Run server.py in this process (via os.execv) instead of spawning a "
320
+ "child. Use this under launchd / systemd / supervisord so the "
321
+ "supervisor sees the long-lived server as the original child. "
322
+ "Implies --no-browser. Skips the post-launch health probe — the "
323
+ "supervisor's own KeepAlive / Restart=on-failure handles liveness."
324
+ ),
325
+ )
326
+ return parser.parse_args()
327
+
328
+
329
+ # Env vars whose presence indicates this process was launched by a supervisor
330
+ # that wants to manage the server's lifecycle (KeepAlive, Restart=always, etc.).
331
+ # When any is set, we auto-promote to --foreground so we don't double-fork.
332
+ #
333
+ # - INVOCATION_ID systemd (set on every service activation)
334
+ # - JOURNAL_STREAM systemd (set when stdio is wired to the journal)
335
+ # - NOTIFY_SOCKET systemd Type=notify, s6 sd_notify-style
336
+ # - XPC_SERVICE_NAME launchd (set to the Label of the running plist)
337
+ # - SUPERVISOR_ENABLED supervisord
338
+ # - HERMES_WEBUI_FOREGROUND explicit user opt-in (=1 / true / yes / on)
339
+ #
340
+ # Note on XPC_SERVICE_NAME: macOS launchd sets this in EVERY Terminal-launched
341
+ # shell too — typical values include "0" (truthy in Python!) and
342
+ # "application.com.apple.Terminal.<UUID>". A bare existence check would
343
+ # false-positive on every Mac dev machine running ./start.sh interactively.
344
+ # We narrow to launchd Label-style names (com.<reverse-dns>.<svc>) — those
345
+ # are real services. Verified with `launchctl getenv XPC_SERVICE_NAME` and
346
+ # Apple's documented launchd behavior.
347
+ _SUPERVISOR_ENV_VARS = (
348
+ "INVOCATION_ID",
349
+ "JOURNAL_STREAM",
350
+ "NOTIFY_SOCKET",
351
+ "XPC_SERVICE_NAME",
352
+ "SUPERVISOR_ENABLED",
353
+ )
354
+
355
+
356
+ def _is_real_supervisor_value(name: str, value: str) -> bool:
357
+ """Filter out known-noise env-var values that aren't actual supervisors.
358
+
359
+ Most env vars in _SUPERVISOR_ENV_VARS are only set by the supervisor we
360
+ care about, so any non-empty value is meaningful. XPC_SERVICE_NAME is the
361
+ exception: macOS launchd sets it in every Terminal-spawned shell with
362
+ values like "0" or "application.com.apple.Terminal.<UUID>". A real
363
+ launchd-managed service has a reverse-DNS Label like "com.example.foo".
364
+ """
365
+ if not value:
366
+ return False
367
+ if name == "XPC_SERVICE_NAME":
368
+ # Reject Apple's noise values; accept Label-style names.
369
+ if value == "0":
370
+ return False
371
+ if value.startswith("application."):
372
+ return False
373
+ return True
374
+
375
+
376
+ def _detect_supervisor() -> str | None:
377
+ """Return the name of the detected supervisor env var, or None.
378
+
379
+ Pure inspection of os.environ — no side effects. Returned name is the env
380
+ var that triggered detection, useful for log messages and for tests.
381
+ """
382
+ explicit = os.environ.get("HERMES_WEBUI_FOREGROUND", "").strip().lower()
383
+ if explicit in ("1", "true", "yes", "on"):
384
+ return "HERMES_WEBUI_FOREGROUND"
385
+ for name in _SUPERVISOR_ENV_VARS:
386
+ value = os.environ.get(name, "")
387
+ if _is_real_supervisor_value(name, value):
388
+ return name
389
+ return None
390
+
391
+
392
+ def main() -> int:
393
+ args = parse_args()
394
+ ensure_supported_platform()
395
+
396
+ agent_dir = discover_agent_dir()
397
+ if not agent_dir and not hermes_command_exists():
398
+ if args.skip_agent_install:
399
+ raise RuntimeError(
400
+ "Hermes Agent was not found and auto-install was disabled."
401
+ )
402
+ install_hermes_agent()
403
+ agent_dir = discover_agent_dir()
404
+
405
+ python_exe = ensure_python_has_webui_deps(discover_launcher_python(agent_dir), agent_dir)
406
+ state_dir = Path(
407
+ os.getenv("HERMES_WEBUI_STATE_DIR", str(Path.home() / ".hermes" / "webui"))
408
+ ).expanduser()
409
+ state_dir.mkdir(parents=True, exist_ok=True)
410
+
411
+ # Mutate os.environ so child (or post-execv) inherits the resolved values.
412
+ os.environ["HERMES_WEBUI_HOST"] = args.host
413
+ os.environ["HERMES_WEBUI_PORT"] = str(args.port)
414
+ os.environ.setdefault("HERMES_WEBUI_STATE_DIR", str(state_dir))
415
+ if agent_dir:
416
+ os.environ["HERMES_WEBUI_AGENT_DIR"] = str(agent_dir)
417
+
418
+ server_cwd = str(agent_dir or REPO_ROOT)
419
+ server_path = str(REPO_ROOT / "server.py")
420
+
421
+ # --foreground (or auto-detected supervisor): replace this process with the
422
+ # server. The supervisor sees the long-lived server as the original child,
423
+ # so KeepAlive / Restart=always / autorestart=true work correctly. No
424
+ # health probe — the supervisor's own restart-on-exit handles liveness.
425
+ foreground_reason = "--foreground" if args.foreground else _detect_supervisor()
426
+ if foreground_reason:
427
+ info(
428
+ f"Starting Hermes Web UI on http://{args.host}:{args.port} "
429
+ f"(foreground mode: {foreground_reason})"
430
+ )
431
+ try:
432
+ os.chdir(server_cwd)
433
+ except OSError as exc:
434
+ raise RuntimeError(
435
+ f"Could not chdir to {server_cwd!r} before exec: {exc}"
436
+ ) from exc
437
+ # Defensive check: if python_exe is missing or non-executable, execv
438
+ # raises OSError, the wrapper catches and SystemExit(1)s, and the
439
+ # supervisor restarts — looping forever, exactly the failure mode this
440
+ # PR is meant to eliminate. Convert to a single visible error.
441
+ if not os.access(python_exe, os.X_OK):
442
+ raise RuntimeError(
443
+ f"Python interpreter at {python_exe!r} is not executable. "
444
+ f"Set HERMES_WEBUI_PYTHON to a working interpreter or fix "
445
+ f"the agent venv at {agent_dir}."
446
+ )
447
+ # os.execv replaces the current process image. Anything after this line
448
+ # only runs if execv itself fails (it raises OSError on failure).
449
+ os.execv(python_exe, [python_exe, server_path])
450
+ # Unreachable — execv either replaces the process or raises.
451
+ raise RuntimeError("os.execv returned unexpectedly")
452
+
453
+ # Default (legacy) path: spawn the server as a detached child, probe
454
+ # /health, then return. Suitable for an interactive `bash start.sh` run.
455
+ log_path = state_dir / f"bootstrap-{args.port}.log"
456
+
457
+ info(f"Starting Hermes Web UI on http://{args.host}:{args.port}")
458
+ with log_path.open("ab") as log_file:
459
+ proc = subprocess.Popen(
460
+ [python_exe, server_path],
461
+ cwd=server_cwd,
462
+ env=os.environ.copy(),
463
+ stdout=log_file,
464
+ stderr=subprocess.STDOUT,
465
+ start_new_session=True,
466
+ )
467
+
468
+ health_url = f"http://{args.host}:{args.port}/health"
469
+ if not wait_for_health(health_url):
470
+ raise RuntimeError(
471
+ f"Web UI did not become healthy at {health_url}. "
472
+ f"Check the log at {log_path}. Server PID: {proc.pid}"
473
+ )
474
+
475
+ app_url = (
476
+ f"http://localhost:{args.port}"
477
+ if args.host in ("127.0.0.1", "localhost")
478
+ else f"http://{args.host}:{args.port}"
479
+ )
480
+ info(f"Web UI is ready: {app_url}")
481
+ info(f"Log file: {log_path}")
482
+ if not args.no_browser:
483
+ open_browser(app_url)
484
+ return 0
485
+
486
+
487
+ if __name__ == "__main__":
488
+ try:
489
+ raise SystemExit(main())
490
+ except Exception as exc:
491
+ print(f"[bootstrap] ERROR: {exc}", file=sys.stderr)
492
+ raise SystemExit(1)