@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.
- package/README.md +213 -0
- package/bin/hermes-webui.mjs +588 -0
- package/package.json +25 -0
- package/scripts/sync-vendor.mjs +74 -0
- package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
- package/templates/systemd/hermes-webui.service +13 -0
- package/templates/windows/hermes-webui-task.ps1 +3 -0
- package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
- package/vendor/agent-frontend-shell/.dockerignore +7 -0
- package/vendor/agent-frontend-shell/.env.docker.example +89 -0
- package/vendor/agent-frontend-shell/.env.example +34 -0
- package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
- package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
- package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
- package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
- package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
- package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
- package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
- package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
- package/vendor/agent-frontend-shell/AGENTS.md +80 -0
- package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
- package/vendor/agent-frontend-shell/BUGS.md +52 -0
- package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
- package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
- package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
- package/vendor/agent-frontend-shell/DESIGN.md +173 -0
- package/vendor/agent-frontend-shell/Dockerfile +91 -0
- package/vendor/agent-frontend-shell/LICENSE +21 -0
- package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
- package/vendor/agent-frontend-shell/README.md +705 -0
- package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
- package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
- package/vendor/agent-frontend-shell/TESTING.md +1932 -0
- package/vendor/agent-frontend-shell/THEMES.md +170 -0
- package/vendor/agent-frontend-shell/api/__init__.py +1 -0
- package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
- package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
- package/vendor/agent-frontend-shell/api/auth.py +592 -0
- package/vendor/agent-frontend-shell/api/background.py +87 -0
- package/vendor/agent-frontend-shell/api/clarify.py +238 -0
- package/vendor/agent-frontend-shell/api/commands.py +124 -0
- package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
- package/vendor/agent-frontend-shell/api/config.py +5178 -0
- package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
- package/vendor/agent-frontend-shell/api/extensions.py +253 -0
- package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
- package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
- package/vendor/agent-frontend-shell/api/goals.py +608 -0
- package/vendor/agent-frontend-shell/api/helpers.py +474 -0
- package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
- package/vendor/agent-frontend-shell/api/metering.py +194 -0
- package/vendor/agent-frontend-shell/api/models.py +4210 -0
- package/vendor/agent-frontend-shell/api/oauth.py +770 -0
- package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
- package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
- package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
- package/vendor/agent-frontend-shell/api/providers.py +2175 -0
- package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
- package/vendor/agent-frontend-shell/api/rollback.py +320 -0
- package/vendor/agent-frontend-shell/api/routes.py +13990 -0
- package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
- package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
- package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
- package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
- package/vendor/agent-frontend-shell/api/session_events.py +45 -0
- package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
- package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
- package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
- package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
- package/vendor/agent-frontend-shell/api/startup.py +128 -0
- package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
- package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
- package/vendor/agent-frontend-shell/api/system_health.py +167 -0
- package/vendor/agent-frontend-shell/api/terminal.py +410 -0
- package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
- package/vendor/agent-frontend-shell/api/updates.py +1261 -0
- package/vendor/agent-frontend-shell/api/upload.py +322 -0
- package/vendor/agent-frontend-shell/api/usage.py +26 -0
- package/vendor/agent-frontend-shell/api/workspace.py +867 -0
- package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
- package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
- package/vendor/agent-frontend-shell/bootstrap.py +492 -0
- package/vendor/agent-frontend-shell/ctl.sh +427 -0
- package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
- package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
- package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
- package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
- package/vendor/agent-frontend-shell/docker_init.bash +459 -0
- package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
- package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
- package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
- package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
- package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
- package/vendor/agent-frontend-shell/docs/docker.md +337 -0
- package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
- package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
- package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
- package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
- package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
- package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
- package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
- package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
- package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
- package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
- package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
- package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
- package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
- package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
- package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
- package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
- package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
- package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
- package/vendor/agent-frontend-shell/extensions/index.js +67 -0
- package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
- package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
- package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
- package/vendor/agent-frontend-shell/install.sh +63 -0
- package/vendor/agent-frontend-shell/mcp_server.py +567 -0
- package/vendor/agent-frontend-shell/package.json +12 -0
- package/vendor/agent-frontend-shell/pyproject.toml +56 -0
- package/vendor/agent-frontend-shell/pytest.ini +3 -0
- package/vendor/agent-frontend-shell/requirements.txt +5 -0
- package/vendor/agent-frontend-shell/server.py +624 -0
- package/vendor/agent-frontend-shell/start.ps1 +210 -0
- package/vendor/agent-frontend-shell/start.sh +65 -0
- package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/static/boot.js +1990 -0
- package/vendor/agent-frontend-shell/static/commands.js +1402 -0
- package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
- package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
- package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
- package/vendor/agent-frontend-shell/static/icons.js +92 -0
- package/vendor/agent-frontend-shell/static/index.html +1506 -0
- package/vendor/agent-frontend-shell/static/login.js +177 -0
- package/vendor/agent-frontend-shell/static/manifest.json +53 -0
- package/vendor/agent-frontend-shell/static/messages.js +3521 -0
- package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
- package/vendor/agent-frontend-shell/static/panels.js +7995 -0
- package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
- package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
- package/vendor/agent-frontend-shell/static/style.css +4774 -0
- package/vendor/agent-frontend-shell/static/sw.js +173 -0
- package/vendor/agent-frontend-shell/static/terminal.js +632 -0
- package/vendor/agent-frontend-shell/static/ui.js +8997 -0
- package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
- package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
- 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"]
|