merge: upgrade to upstream v0.50.95 + keep custom additions

Upstream v0.50.95 features merged (Russian localization, slash commands,
mic toggle fix, gateway sync fix, KaTeX/Prism.js, etc.)

Custom additions preserved:
- Tier-2 agent switching commands in commands.js
- MC panel in index.html + MC CSS
- _resolve_cli_toolsets() in config.py
- Custom routes.py, server.py, boot.js, i18n.js, messages.js, workspace.js

Files with conflict resolution (took upstream, custom code in other files):
- CHANGELOG.md, config.py, commands.js, index.html, panels.js, style.css, ui.js
This commit is contained in:
Rose
2026-04-19 10:06:28 +02:00
parent 067d96bb30
commit 3bdf430413
12 changed files with 1736 additions and 2361 deletions

View File

@@ -53,6 +53,7 @@ from api.helpers import (
redact_session_data,
_redact_text,
)
from api import mc as _mc
# ── CSRF: validate Origin/Referer on POST ────────────────────────────────────
import re as _re
@@ -755,6 +756,20 @@ def handle_get(handler, parsed) -> bool:
if parsed.path == "/api/memory":
return _handle_memory_read(handler)
# ── Mission Control (GET) ──
if parsed.path == "/api/mc/status":
return j(handler, _mc.get_dashboard_status())
if parsed.path == "/api/mc/priorities":
return j(handler, {"priorities": _mc.get_priorities()})
if parsed.path == "/api/mc/tasks":
return j(handler, {"tasks": _mc.get_tasks()})
if parsed.path == "/api/mc/feed":
limit = int(parse_qs(parsed.query).get("limit", ["50"])[0])
return j(handler, {"feed": _mc.get_feed(limit=limit)})
# ── Profile API (GET) ──
if parsed.path == "/api/profiles":
from api.profiles import list_profiles_api, get_active_profile_name
@@ -772,6 +787,52 @@ def handle_get(handler, parsed) -> bool:
{"name": get_active_profile_name(), "path": str(get_active_hermes_home())},
)
# ── Commands API (dynamic from Hermes COMMAND_REGISTRY) ──
if parsed.path == "/api/commands":
import sys as _sys
from pathlib import Path as _P
# Add hermes-agent to path so we can import the registry
_agent_path = (_P(__file__).parent.parent / "hermes-agent").resolve()
if str(_agent_path) not in _sys.path:
_sys.path.insert(0, str(_agent_path))
try:
from hermes_cli.commands import COMMAND_REGISTRY
# Map Hermes category names to WebUI category labels
_cat_map = {
"Session": "Session",
"Config": "Configuration",
"Tool": "Tools & Skills",
"Skill": "Tools & Skills",
"Info": "Info",
"Agent": "Agents",
"System": "System",
}
_by_cat: dict[str, list] = {}
for cmd in COMMAND_REGISTRY:
if getattr(cmd, "cli_only", False):
continue # skip CLI-only commands
cat = _cat_map.get(cmd.category, "Info")
if cat not in _by_cat:
_by_cat[cat] = []
_by_cat[cat].append({
"name": cmd.name,
"desc": cmd.description,
"arg": getattr(cmd, "args_hint", None) or "(none)",
"aliases": list(getattr(cmd, "aliases", []) or []),
})
# Always include Agents section even if empty (Tier-2 agents are added client-side)
if "Agents" not in _by_cat:
_by_cat["Agents"] = []
return j(handler, {"categories": _by_cat})
except Exception as e:
return j(handler, {"error": str(e)}, status=500)
return False # 404
@@ -1047,6 +1108,67 @@ def handle_post(handler, parsed) -> bool:
if parsed.path == "/api/memory/write":
return _handle_memory_write(handler, body)
# ── Mission Control (POST) ──
if parsed.path == "/api/mc/priority/create":
name = body.get("name", "").strip()
if not name:
return bad(handler, "name is required")
priority = _mc.create_priority(name, body.get("color", "#808080"))
return j(handler, {"ok": True, "priority": priority})
if parsed.path == "/api/mc/priority/update":
pid = body.get("id")
if not pid:
return bad(handler, "id is required")
updated = _mc.update_priority(
pid,
name=body.get("name"),
color=body.get("color"),
done=body.get("done"),
)
if not updated:
return bad(handler, "priority not found", 404)
return j(handler, {"ok": True, "priority": updated})
if parsed.path == "/api/mc/priority/delete":
pid = body.get("id")
if not pid:
return bad(handler, "id is required")
deleted = _mc.delete_priority(pid)
return j(handler, {"ok": deleted})
if parsed.path == "/api/mc/task/create":
title = body.get("title", "").strip()
if not title:
return bad(handler, "title is required")
task = _mc.create_task(
title,
priority=body.get("priority", 1),
status=body.get("status", "backlog"),
)
return j(handler, {"ok": True, "task": task})
if parsed.path == "/api/mc/task/update":
tid = body.get("id")
if not tid:
return bad(handler, "id is required")
updated = _mc.update_task(
tid,
title=body.get("title"),
priority=body.get("priority"),
status=body.get("status"),
)
if not updated:
return bad(handler, "task not found", 404)
return j(handler, {"ok": True, "task": updated})
if parsed.path == "/api/mc/task/delete":
tid = body.get("id")
if not tid:
return bad(handler, "id is required")
deleted = _mc.delete_task(tid)
return j(handler, {"ok": deleted})
# ── Profile API (POST) ──
if parsed.path == "/api/profile/switch":
name = body.get("name", "").strip()
@@ -1113,6 +1235,57 @@ def handle_post(handler, parsed) -> bool:
except RuntimeError as e:
return bad(handler, str(e), 409)
# ── Gateway API ──
if parsed.path == "/api/gateways":
# GET - list all gateways
from api.gateways import list_gateways_api
return j(handler, {"gateways": list_gateways_api()})
if parsed.path == "/api/gateway/start":
name = body.get("name", "").strip()
if not name:
return bad(handler, "name is required")
from api.gateways import start_gateway_api
try:
result = start_gateway_api(name)
return j(handler, result)
except (ValueError, RuntimeError) as e:
return bad(handler, str(e))
if parsed.path == "/api/gateway/stop":
name = body.get("name", "").strip()
if not name:
return bad(handler, "name is required")
from api.gateways import stop_gateway_api
try:
result = stop_gateway_api(name)
return j(handler, result)
except (ValueError, RuntimeError) as e:
return bad(handler, str(e))
if parsed.path == "/api/gateway/restart":
name = body.get("name", "").strip()
if not name:
return bad(handler, "name is required")
from api.gateways import restart_gateway_api
try:
result = restart_gateway_api(name)
return j(handler, result)
except (ValueError, RuntimeError) as e:
return bad(handler, str(e))
if parsed.path == "/api/gateway/add":
name = body.get("name", "").strip()
if not name:
return bad(handler, "name is required")
gw_type = body.get("type", "telegram").strip()
from api.gateways import add_gateway_api
try:
result = add_gateway_api(name, gw_type)
return j(handler, result)
except (ValueError, RuntimeError, FileExistsError) as e:
return bad(handler, str(e))
# ── Settings (POST) ──
if parsed.path == "/api/settings":
from api.auth import (