@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,567 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hermes WebUI MCP Server — exposes project and session management
4
+ as MCP tools for any MCP-compatible agent.
5
+
6
+ Option A rewrite (2026-05-08): imports api.models and api.profiles
7
+ directly from the webui codebase, using canonical helpers for
8
+ locking, profile scoping, index consistency, and validation.
9
+
10
+ pip install mcp # one-time setup
11
+ python3 mcp_server.py # start via stdio
12
+
13
+ MCP config for Hermes Agent (add to config.yaml):
14
+ mcp_servers:
15
+ hermes-webui:
16
+ command: /path/to/venv/bin/python3
17
+ args: [/path/to/hermes-webui/mcp_server.py]
18
+ env:
19
+ HERMES_WEBUI_PASSWORD: your_password
20
+
21
+ Profile override (optional):
22
+ args: [/path/to/hermes-webui/mcp_server.py, --profile, myprofile]
23
+
24
+ AI-authoring disclosure: this file was rewritten by MILO (Hermes Agent)
25
+ under human direction, per maintainer guidelines for #1616.
26
+ """
27
+
28
+ import argparse
29
+ import json
30
+ import os
31
+ import re
32
+ import sys
33
+ import time
34
+ import uuid
35
+ from pathlib import Path
36
+
37
+ from mcp.server import Server
38
+ from mcp.server.stdio import stdio_server
39
+ from mcp.types import Tool, TextContent
40
+
41
+ # ── Ensure the repo root is on sys.path so api.* imports work ─────────────
42
+ _REPO_ROOT = Path(__file__).parent.resolve()
43
+ if str(_REPO_ROOT) not in sys.path:
44
+ sys.path.insert(0, str(_REPO_ROOT))
45
+
46
+ # ── CLI: optional --profile override ──────────────────────────────────────
47
+ _profile_arg: str | None = None
48
+ _parser = argparse.ArgumentParser(add_help=False)
49
+ _parser.add_argument("--profile", type=str, default=None)
50
+ _args, _unknown = _parser.parse_known_args()
51
+ _profile_arg = _args.profile
52
+
53
+ # ── Import webui canonical modules (after path setup) ─────────────────────
54
+ import api.config as _cfg
55
+ from api.config import (
56
+ STATE_DIR, SESSION_DIR, SESSION_INDEX_FILE, PROJECTS_FILE, HOME,
57
+ )
58
+ from api.models import load_projects, save_projects
59
+ from api.profiles import get_active_profile_name, _is_root_profile, _profiles_match
60
+
61
+ # ── Apply --profile override before any module uses get_active_profile_name
62
+ if _profile_arg is not None:
63
+ import api.profiles as _profiles
64
+ _profiles._active_profile = _profile_arg
65
+
66
+ # ── API auth state ─────────────────────────────────────────────────────────
67
+ # Mirror the env-var contract used by api/config.py:32-33 so a non-default
68
+ # WebUI port/host (e.g. when 8787 is held by another service on the host)
69
+ # Just Works without configuration drift between the WebUI process and MCP.
70
+ WEBUI_HOST = os.environ.get("HERMES_WEBUI_HOST", "127.0.0.1")
71
+ WEBUI_PORT = os.environ.get("HERMES_WEBUI_PORT", "8787")
72
+ WEBUI_URL = f"http://{WEBUI_HOST}:{WEBUI_PORT}"
73
+ _auth_cookie: str | None = None
74
+ _auth_expires: float = 0 # unix timestamp after which we re-auth
75
+
76
+ server = Server("hermes-webui")
77
+
78
+
79
+ # ═══════════════════════════════════════════════════════════════════════════
80
+ # Helpers — filesystem (project CRUD via canonical api.models)
81
+ # ═══════════════════════════════════════════════════════════════════════════
82
+
83
+ def _active_profile() -> str:
84
+ """Shorthand for the current profile name (--profile or auto-detected)."""
85
+ return get_active_profile_name() or 'default'
86
+
87
+
88
+ def _validate_color(color: str | None) -> str | None:
89
+ """Return an error string if color is invalid, else None."""
90
+ if color is not None and not re.match(r"^#[0-9a-fA-F]{3,8}$", color):
91
+ return "Invalid color format (use #RGB, #RRGGBB, or #RRGGBBAA)"
92
+ return None
93
+
94
+
95
+ def _load_index() -> list:
96
+ """Read the session index. Falls back to empty list on failure."""
97
+ if not SESSION_INDEX_FILE.exists():
98
+ return []
99
+ try:
100
+ return json.loads(SESSION_INDEX_FILE.read_text(encoding="utf-8"))
101
+ except Exception:
102
+ return []
103
+
104
+
105
+ def _session_compact(row: dict) -> dict:
106
+ """Lightweight compact representation of a session index entry."""
107
+ return {
108
+ "session_id": row.get("session_id"),
109
+ "title": row.get("title"),
110
+ "project_id": row.get("project_id"),
111
+ "workspace": row.get("workspace"),
112
+ "model": row.get("model"),
113
+ "message_count": row.get("message_count", 0),
114
+ "source_tag": row.get("source_tag"),
115
+ "is_cli_session": row.get("is_cli_session", False),
116
+ "profile": row.get("profile"),
117
+ }
118
+
119
+
120
+ # ═══════════════════════════════════════════════════════════════════════════
121
+ # Helpers — HTTP API (for mutations that need cache sync)
122
+ # ═══════════════════════════════════════════════════════════════════════════
123
+
124
+ def _api_password() -> str | None:
125
+ """Return the plaintext webui password from HERMES_WEBUI_PASSWORD, or None.
126
+
127
+ settings.json stores only the bcrypt hash, which the login endpoint cannot
128
+ accept — it calls verify_password(plaintext) against the stored hash. So
129
+ there's no usable fallback when the env var is unset; the MCP simply runs
130
+ in unauthenticated mode and any auth-protected mutation will fail clearly
131
+ with the server's 401 instead of silently sending an unusable hash.
132
+ """
133
+ pw = os.environ.get("HERMES_WEBUI_PASSWORD", "").strip()
134
+ return pw or None
135
+
136
+
137
+ def _api_auth() -> str | None:
138
+ """Authenticate and return cookie value, or None if auth disabled/fails."""
139
+ global _auth_cookie, _auth_expires
140
+
141
+ pw = _api_password()
142
+ if not pw:
143
+ return None # auth not enabled — API calls will fail anyway
144
+
145
+ # Reuse cookie if still valid (25 days — server issues 30-day cookies)
146
+ if _auth_cookie and time.time() < _auth_expires:
147
+ return _auth_cookie
148
+
149
+ import urllib.request
150
+
151
+ try:
152
+ req = urllib.request.Request(
153
+ f"{WEBUI_URL}/api/auth/login",
154
+ data=json.dumps({"password": pw}).encode(),
155
+ headers={"Content-Type": "application/json"},
156
+ method="POST",
157
+ )
158
+ resp = urllib.request.urlopen(req, timeout=5)
159
+ cookie = resp.headers.get("Set-Cookie", "")
160
+ if cookie:
161
+ _auth_cookie = cookie.split(";")[0] # "hermes_session=VALUE; ..."
162
+ _auth_expires = time.time() + 25 * 86400 # 25 days
163
+ return _auth_cookie
164
+ except Exception:
165
+ _auth_cookie = None
166
+ return None
167
+
168
+
169
+ def _api_post(endpoint: str, body: dict) -> dict:
170
+ """POST to webui API with auth cookie. Returns parsed JSON response."""
171
+ import urllib.request
172
+ import urllib.error
173
+
174
+ cookie = _api_auth()
175
+ headers = {"Content-Type": "application/json"}
176
+ if cookie:
177
+ headers["Cookie"] = cookie
178
+
179
+ try:
180
+ req = urllib.request.Request(
181
+ f"{WEBUI_URL}{endpoint}",
182
+ data=json.dumps(body).encode(),
183
+ headers=headers,
184
+ method="POST",
185
+ )
186
+ resp = urllib.request.urlopen(req, timeout=5)
187
+ return json.loads(resp.read())
188
+ except urllib.error.HTTPError as e:
189
+ err_body = json.loads(e.read())
190
+ return {"error": f"API {e.code}: {err_body.get('error', 'unknown')}"}
191
+ except Exception as e:
192
+ return {"error": f"API unreachable: {e}"}
193
+
194
+
195
+ # ═══════════════════════════════════════════════════════════════════════════
196
+ # Tool handlers — read-only (filesystem, profile-aware)
197
+ # ═══════════════════════════════════════════════════════════════════════════
198
+
199
+ async def handle_list_projects(_arguments: dict) -> list[TextContent]:
200
+ """List all projects with session counts, scoped to active profile."""
201
+ projects = load_projects()
202
+ active = _active_profile()
203
+ index = _load_index()
204
+
205
+ # Session counts per project (from index)
206
+ counts: dict[str, int] = {}
207
+ for s in index:
208
+ pid = s.get("project_id")
209
+ if pid:
210
+ counts[pid] = counts.get(pid, 0) + 1
211
+
212
+ result = []
213
+ for p in projects:
214
+ # Profile filter: legacy untagged rows are treated as 'default' by
215
+ # _profiles_match, so non-root profiles correctly hide them.
216
+ if not _profiles_match(p.get("profile"), active):
217
+ continue
218
+ entry = dict(p)
219
+ entry["session_count"] = counts.get(p["project_id"], 0)
220
+ result.append(entry)
221
+
222
+ return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
223
+
224
+
225
+ async def handle_list_sessions(arguments: dict) -> list[TextContent]:
226
+ """List sessions, optionally filtered by project or unassigned status."""
227
+ project_id = arguments.get("project_id")
228
+ unassigned = arguments.get("unassigned", False)
229
+ limit = max(1, min(500, arguments.get("limit", 50)))
230
+ active = _active_profile()
231
+
232
+ index = _load_index()
233
+ sessions = [_session_compact(s) for s in index if s.get("session_id")]
234
+
235
+ # Filter by profile: legacy untagged rows are treated as 'default' by
236
+ # _profiles_match (canonical convention), so non-root profiles hide them.
237
+ sessions = [s for s in sessions if _profiles_match(s.get("profile"), active)]
238
+
239
+ if unassigned:
240
+ sessions = [s for s in sessions if not s["project_id"]]
241
+ elif project_id:
242
+ sessions = [s for s in sessions if s["project_id"] == project_id]
243
+
244
+ sessions = sessions[:limit]
245
+ return [TextContent(type="text", text=json.dumps(sessions, ensure_ascii=False, indent=2))]
246
+
247
+
248
+ # ═══════════════════════════════════════════════════════════════════════════
249
+ # Tool handlers — project CRUD (canonical helpers, profile-scoped)
250
+ # ═══════════════════════════════════════════════════════════════════════════
251
+
252
+ async def handle_create_project(arguments: dict) -> list[TextContent]:
253
+ """Create a new project (profile-scoped, exact-match title collision)."""
254
+ name = arguments.get("name", "").strip()[:128]
255
+ if not name:
256
+ return [TextContent(type="text", text=json.dumps(
257
+ {"error": "name is required"}, ensure_ascii=False))]
258
+
259
+ color = arguments.get("color")
260
+ color_err = _validate_color(color)
261
+ if color_err:
262
+ return [TextContent(type="text", text=json.dumps(
263
+ {"error": color_err}, ensure_ascii=False))]
264
+
265
+ active = _active_profile()
266
+ projects = load_projects()
267
+
268
+ # Title collision: exact match (consistent with ensure_cron_project)
269
+ if any(p.get("name") == name and _profiles_match(p.get("profile"), active)
270
+ for p in projects):
271
+ return [TextContent(type="text", text=json.dumps(
272
+ {"error": f"Project '{name}' already exists"}, ensure_ascii=False))]
273
+
274
+ proj = {
275
+ "project_id": uuid.uuid4().hex[:12],
276
+ "name": name,
277
+ "color": color,
278
+ "profile": active,
279
+ "created_at": time.time(),
280
+ }
281
+ projects.append(proj)
282
+ save_projects(projects)
283
+
284
+ proj["session_count"] = 0
285
+ return [TextContent(type="text", text=json.dumps(proj, ensure_ascii=False, indent=2))]
286
+
287
+
288
+ async def handle_rename_project(arguments: dict) -> list[TextContent]:
289
+ """Rename a project and optionally change its color (profile-checked)."""
290
+ project_id = arguments.get("project_id")
291
+ name = arguments.get("name", "").strip()[:128]
292
+ if not project_id or not name:
293
+ return [TextContent(type="text", text=json.dumps(
294
+ {"error": "project_id and name are required"}, ensure_ascii=False))]
295
+
296
+ color = arguments.get("color")
297
+ color_err = _validate_color(color)
298
+ if color_err:
299
+ return [TextContent(type="text", text=json.dumps(
300
+ {"error": color_err}, ensure_ascii=False))]
301
+
302
+ active = _active_profile()
303
+ projects = load_projects()
304
+ proj = next((p for p in projects if p["project_id"] == project_id), None)
305
+ if not proj:
306
+ return [TextContent(type="text", text=json.dumps(
307
+ {"error": "Project not found"}, ensure_ascii=False))]
308
+
309
+ # #1614: profile ownership check
310
+ if not _profiles_match(proj.get("profile"), active):
311
+ return [TextContent(type="text", text=json.dumps(
312
+ {"error": "Project not found"}, ensure_ascii=False))]
313
+
314
+ proj["name"] = name
315
+ if color is not None:
316
+ proj["color"] = color
317
+ save_projects(projects)
318
+ return [TextContent(type="text", text=json.dumps(proj, ensure_ascii=False, indent=2))]
319
+
320
+
321
+ async def handle_delete_project(arguments: dict) -> list[TextContent]:
322
+ """Delete a project and unassign all its sessions (profile-checked)."""
323
+ project_id = arguments.get("project_id")
324
+ if not project_id:
325
+ return [TextContent(type="text", text=json.dumps(
326
+ {"error": "project_id is required"}, ensure_ascii=False))]
327
+
328
+ active = _active_profile()
329
+ projects = load_projects()
330
+ proj = next((p for p in projects if p["project_id"] == project_id), None)
331
+ if not proj:
332
+ return [TextContent(type="text", text=json.dumps(
333
+ {"error": "Project not found"}, ensure_ascii=False))]
334
+
335
+ # #1614: profile ownership check
336
+ if not _profiles_match(proj.get("profile"), active):
337
+ return [TextContent(type="text", text=json.dumps(
338
+ {"error": "Project not found"}, ensure_ascii=False))]
339
+
340
+ projects = [p for p in projects if p["project_id"] != project_id]
341
+ save_projects(projects)
342
+
343
+ # Unassign sessions only when we can do it cache-safely via the HTTP API.
344
+ # The previous filesystem fallback wrote session_data directly with
345
+ # os.replace(), which bypassed _write_session_index() in api/models.py
346
+ # and left _index.json holding the stale project_id — a running WebUI
347
+ # would still group those sessions under the deleted project until a
348
+ # subsequent re-compact. Even calling Session.save() in-process would
349
+ # not help because the WebUI's SESSIONS dict cache (a separate process)
350
+ # still has the old project_id and overwrites our update on its next
351
+ # save. The HTTP API is the only cache-safe path; without auth we
352
+ # refuse and surface the limitation so the operator can act.
353
+ has_auth = bool(_api_password())
354
+ if not has_auth:
355
+ return [TextContent(type="text", text=json.dumps({
356
+ "ok": True,
357
+ "deleted": proj["name"],
358
+ "unassigned_sessions": 0,
359
+ "warning": "Set HERMES_WEBUI_PASSWORD to unassign sessions; "
360
+ "without auth the session index cannot be safely "
361
+ "updated and direct filesystem writes would cause "
362
+ "index drift in a running WebUI.",
363
+ }, ensure_ascii=False))]
364
+
365
+ unassigned = 0
366
+ if SESSION_DIR.exists():
367
+ for p in SESSION_DIR.glob("*.json"):
368
+ if p.name.startswith("_"):
369
+ continue
370
+ try:
371
+ session_data = json.loads(p.read_text(encoding="utf-8"))
372
+ if session_data.get("project_id") == project_id:
373
+ sid = p.stem
374
+ result = _api_post("/api/session/move",
375
+ {"session_id": sid, "project_id": None})
376
+ if "ok" in result or "session" in result:
377
+ unassigned += 1
378
+ except Exception:
379
+ pass
380
+
381
+ return [TextContent(type="text", text=json.dumps({
382
+ "ok": True,
383
+ "deleted": proj["name"],
384
+ "unassigned_sessions": unassigned,
385
+ }, ensure_ascii=False))]
386
+
387
+
388
+ # ═══════════════════════════════════════════════════════════════════════════
389
+ # Tool handlers — mutations (HTTP API with auth, cache-safe)
390
+ # ═══════════════════════════════════════════════════════════════════════════
391
+
392
+ async def handle_rename_session(arguments: dict) -> list[TextContent]:
393
+ """Rename a session via the authenticated webui API (cache-safe)."""
394
+ session_id = arguments.get("session_id")
395
+ title = arguments.get("title", "").strip()[:80]
396
+ if not session_id or not title:
397
+ return [TextContent(type="text", text=json.dumps(
398
+ {"error": "session_id and title are required"}, ensure_ascii=False))]
399
+
400
+ result = _api_post("/api/session/rename",
401
+ {"session_id": session_id, "title": title})
402
+ if "error" in result:
403
+ return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
404
+
405
+ session = result.get("session", {})
406
+ return [TextContent(type="text", text=json.dumps({
407
+ "ok": True,
408
+ "session_id": session_id,
409
+ "title": session.get("title", title),
410
+ "method": "api",
411
+ }, ensure_ascii=False, indent=2))]
412
+
413
+
414
+ async def handle_move_session(arguments: dict) -> list[TextContent]:
415
+ """Assign a session to a project via the authenticated webui API (cache-safe)."""
416
+ session_id = arguments.get("session_id")
417
+ project_id = arguments.get("project_id") # None/null = unassign
418
+ if not session_id:
419
+ return [TextContent(type="text", text=json.dumps(
420
+ {"error": "session_id is required"}, ensure_ascii=False))]
421
+
422
+ # If project_id is provided, verify it exists and is profile-accessible
423
+ if project_id is not None:
424
+ projects = load_projects()
425
+ active = _active_profile()
426
+ target = next((p for p in projects if p["project_id"] == project_id), None)
427
+ if not target:
428
+ return [TextContent(type="text", text=json.dumps(
429
+ {"error": "Project not found"}, ensure_ascii=False))]
430
+ # #1614: refuse moves into projects owned by another profile
431
+ if not _profiles_match(target.get("profile"), active):
432
+ return [TextContent(type="text", text=json.dumps(
433
+ {"error": "Project not found"}, ensure_ascii=False))]
434
+
435
+ result = _api_post("/api/session/move",
436
+ {"session_id": session_id, "project_id": project_id})
437
+ if "error" in result:
438
+ return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False))]
439
+
440
+ session = result.get("session", {})
441
+ return [TextContent(type="text", text=json.dumps({
442
+ "ok": True,
443
+ "session_id": session_id,
444
+ "project_id": project_id,
445
+ "title": session.get("title"),
446
+ "method": "api",
447
+ }, ensure_ascii=False, indent=2))]
448
+
449
+
450
+ # ═══════════════════════════════════════════════════════════════════════════
451
+ # MCP Server wiring
452
+ # ═══════════════════════════════════════════════════════════════════════════
453
+
454
+ TOOLS = [
455
+ Tool(
456
+ name="list_projects",
457
+ description="List all session projects with their IDs, names, colors, and session counts (scoped to active profile).",
458
+ inputSchema={"type": "object", "properties": {}, "required": []},
459
+ ),
460
+ Tool(
461
+ name="create_project",
462
+ description="Create a new project for organizing sessions (profile-scoped).",
463
+ inputSchema={
464
+ "type": "object",
465
+ "properties": {
466
+ "name": {"type": "string", "description": "Project name (max 128 chars)"},
467
+ "color": {"type": "string", "description": "Optional hex color (#RGB, #RRGGBB, or #RRGGBBAA)"},
468
+ },
469
+ "required": ["name"],
470
+ },
471
+ ),
472
+ Tool(
473
+ name="rename_project",
474
+ description="Rename a project and optionally change its color (profile-checked).",
475
+ inputSchema={
476
+ "type": "object",
477
+ "properties": {
478
+ "project_id": {"type": "string", "description": "12-char project ID"},
479
+ "name": {"type": "string", "description": "New name (max 128 chars)"},
480
+ "color": {"type": "string", "description": "Optional new hex color"},
481
+ },
482
+ "required": ["project_id", "name"],
483
+ },
484
+ ),
485
+ Tool(
486
+ name="delete_project",
487
+ description="Delete a project and unassign all its sessions (profile-checked).",
488
+ inputSchema={
489
+ "type": "object",
490
+ "properties": {
491
+ "project_id": {"type": "string", "description": "12-char project ID to delete"},
492
+ },
493
+ "required": ["project_id"],
494
+ },
495
+ ),
496
+ Tool(
497
+ name="rename_session",
498
+ description="Rename a session (updates sidebar via authenticated API, cache-safe).",
499
+ inputSchema={
500
+ "type": "object",
501
+ "properties": {
502
+ "session_id": {"type": "string", "description": "Session ID"},
503
+ "title": {"type": "string", "description": "New title (max 80 chars)"},
504
+ },
505
+ "required": ["session_id", "title"],
506
+ },
507
+ ),
508
+ Tool(
509
+ name="move_session",
510
+ description="Assign a session to a project. Pass project_id=null to unassign. Uses authenticated API for cache safety (profile-checked).",
511
+ inputSchema={
512
+ "type": "object",
513
+ "properties": {
514
+ "session_id": {"type": "string", "description": "Session ID"},
515
+ "project_id": {"type": ["string", "null"], "description": "Project ID (or null to unassign)"},
516
+ },
517
+ "required": ["session_id", "project_id"],
518
+ },
519
+ ),
520
+ Tool(
521
+ name="list_sessions",
522
+ description="List sessions, optionally filtered by project or unassigned status (profile-scoped).",
523
+ inputSchema={
524
+ "type": "object",
525
+ "properties": {
526
+ "project_id": {"type": "string", "description": "Filter sessions by project ID"},
527
+ "unassigned": {"type": "boolean", "description": "Show only sessions with no project"},
528
+ "limit": {"type": "integer", "description": "Max results (default: 50, max: 500)"},
529
+ },
530
+ "required": [],
531
+ },
532
+ ),
533
+ ]
534
+
535
+ HANDLERS = {
536
+ "list_projects": handle_list_projects,
537
+ "create_project": handle_create_project,
538
+ "rename_project": handle_rename_project,
539
+ "delete_project": handle_delete_project,
540
+ "rename_session": handle_rename_session,
541
+ "move_session": handle_move_session,
542
+ "list_sessions": handle_list_sessions,
543
+ }
544
+
545
+
546
+ @server.list_tools()
547
+ async def list_tools() -> list[Tool]:
548
+ return TOOLS
549
+
550
+
551
+ @server.call_tool()
552
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
553
+ handler = HANDLERS.get(name)
554
+ if not handler:
555
+ return [TextContent(type="text", text=json.dumps(
556
+ {"error": f"Unknown tool: {name}"}, ensure_ascii=False))]
557
+ return await handler(arguments)
558
+
559
+
560
+ async def main():
561
+ async with stdio_server() as (read, write):
562
+ await server.run(read, write, server.create_initialization_options())
563
+
564
+
565
+ if __name__ == "__main__":
566
+ import asyncio
567
+ asyncio.run(main())
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "hermes-webui-devtools",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Dev-only tooling for hermes-webui. NOT a build step — the app remains pure Python + vanilla JS with no bundler. The only dependency is ESLint, used solely as a runtime-error guard over static/*.js (catches brick-class bugs like #3162 const-reassignment that node --check and source-presence tests miss). See TESTING.md > 'Static JS runtime lint'.",
6
+ "scripts": {
7
+ "lint:runtime": "eslint --no-config-lookup -c eslint.runtime-guard.config.mjs \"static/**/*.js\""
8
+ },
9
+ "devDependencies": {
10
+ "eslint": "^10.4.0"
11
+ }
12
+ }
@@ -0,0 +1,56 @@
1
+ # Hermes WebUI — Python tooling config.
2
+ #
3
+ # This project is NOT a packaged distribution. The app is a plain Python + vanilla
4
+ # JavaScript server with no build step and no bundler (see AGENTS.md / README). This
5
+ # file exists only to configure dev tooling — currently ruff, used as a curated,
6
+ # forward-looking lint gate over the Python codebase. There is intentionally no
7
+ # [build-system] section: nothing pip-installs this directory.
8
+ #
9
+ # The ruff gate is the Python twin of the ESLint runtime guard (package.json
10
+ # `lint:runtime` + eslint.runtime-guard.config.mjs + tests/test_static_js_runtime_lint.py).
11
+ # It is enforced on NEW/CHANGED code only — see scripts/ruff_lint.py and TESTING.md
12
+ # > "Python lint gate (ruff)". The existing tree carries a cosmetic backlog (mostly
13
+ # unused-import F401) that is deliberately NOT reformatted here; cleaning it is a
14
+ # separate, maintainer-run, safe-fixes-only decision (tracked in #3273).
15
+
16
+ [tool.ruff]
17
+ # Match the Python versions exercised in CI (tests.yml matrix: 3.11–3.13).
18
+ target-version = "py311"
19
+ # Keep the linter scoped to the application + tests; never crawl vendored or
20
+ # generated trees. (These are belt-and-suspenders; the gate passes explicit files.)
21
+ extend-exclude = [
22
+ "node_modules",
23
+ "static",
24
+ ".git",
25
+ "scripts/windows",
26
+ "scripts/wsl",
27
+ ]
28
+
29
+ [tool.ruff.lint]
30
+ # Curated, correctness-leaning ruleset — high signal, low noise. We deliberately do
31
+ # NOT enable the pure-style families (E1/E2/E5/E7 line-length & whitespace) so the
32
+ # gate never demands a whitespace reformat of existing code.
33
+ #
34
+ # E9 — syntax / IO / runtime errors (E999 etc). The whole tree is already clean
35
+ # of these and the in-suite test (tests/test_ruff_forward_lint.py) keeps it
36
+ # that way across every shard.
37
+ # F — pyflakes: unused imports (F401), unused/undefined names (F841/F821),
38
+ # redefinitions (F811), f-strings with no placeholders (F541). The most
39
+ # valuable family for keeping NEW code clean.
40
+ # B — flake8-bugbear: genuine latent-bug shapes — mutable default args (B006),
41
+ # raise-without-from (B904), loop-variable capture in closures (B023),
42
+ # zip-without-strict (B905). This is where the real future-regression-
43
+ # prevention value lives.
44
+ select = ["E9", "F", "B"]
45
+
46
+ # No global `ignore` of F401/F841/etc. The existing-tree backlog is handled by
47
+ # line-scoping the gate to changed lines (scripts/ruff_lint.py), NOT by globally
48
+ # disabling the rules — disabling them would blind the gate to the single most
49
+ # common new-code defect (a stray unused import). Forward enforcement of the full
50
+ # curated set is the whole point.
51
+
52
+ [tool.ruff.lint.per-file-ignores]
53
+ # Tests legitimately use `assert False` as an explicit failure marker (B011) and
54
+ # occasionally shadow loop vars in table-driven cases (B007); that's idiomatic in a
55
+ # test suite and not a production-code risk.
56
+ "tests/**" = ["B011", "B007"]
@@ -0,0 +1,3 @@
1
+ [pytest]
2
+ markers =
3
+ integration: tests that hit the live test server or external integration surface
@@ -0,0 +1,5 @@
1
+ # Hermes Web UI -- minimal Python dependencies
2
+ # The server uses PyYAML plus cryptography for optional local passkey/WebAuthn support.
3
+ # All heavy ML/agent deps live in the Hermes agent venv.
4
+ pyyaml>=6.0
5
+ cryptography>=42.0