git clone https://github.com/Aradotso/trending-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/Aradotso/trending-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/hermes-hudui-consciousness-monitor" ~/.claude/skills/aradotso-trending-skills-hermes-hudui-consciousness-monitor && rm -rf "$T"
skills/hermes-hudui-consciousness-monitor/SKILL.md--- name: hermes-hudui-consciousness-monitor description: Web UI consciousness monitor for Hermes AI agent with persistent memory — FastAPI backend + React frontend dashboard triggers: - set up hermes hud web ui - monitor hermes agent in browser - hermes hudui dashboard - show hermes agent consciousness monitor - hermes web ui not working - add token cost tracking hermes - hermes hud websocket updates - configure hermes hudui themes --- # ☤ Hermes HUD Web UI > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. A browser-based consciousness monitor for the Hermes AI agent. Reads from `~/.hermes/` data files and serves a real-time React dashboard via a FastAPI backend with WebSocket support. ## Architecture Overview
React Frontend (Vite + SWR) ↓ /api/* + WebSocket /ws FastAPI Backend (Python 3.11+) ↓ collectors/*.py + cache + file watcher ~/.hermes/ (agent data files)
- **Backend**: FastAPI with collectors, mtime-based cache, watchfiles watcher - **Frontend**: React + Vite + SWR with silent background updates and auto-reconnect WebSocket - **Data source**: `~/.hermes/` directory — no database, no external APIs ## Installation ### Quick Install ```bash git clone https://github.com/joeynyc/hermes-hudui.git cd hermes-hudui python3.11 -m venv venv source venv/bin/activate ./install.sh hermes-hudui
Manual Install
python3.11 -m venv venv source venv/bin/activate # Install Python package pip install -e . # Build frontend and copy to backend static dir cd frontend npm install npm run build cp -r dist/* ../backend/static/ # Start server hermes-hudui
Subsequent Runs
source venv/bin/activate hermes-hudui
With TUI Support (Optional)
pip install hermes-hudui[tui]
Requirements
- Python 3.11+
- Node.js 18+
- A running Hermes agent with data written to
~/.hermes/
Key CLI Commands
| Command | Description |
|---|---|
| Start the web server on port 3001 |
| Full install: venv setup + pip install + frontend build |
Backend: Collectors
Collectors are Python modules in
backend/collectors/ that read ~/.hermes/ and return dataclasses. Each collector corresponds to a dashboard panel.
Collector Pattern
# backend/collectors/identity.py from dataclasses import dataclass from pathlib import Path import json HERMES_DIR = Path.home() / ".hermes" @dataclass class IdentityData: designation: str substrate: str runtime: str days_conscious: int brain_size_mb: float def collect_identity() -> IdentityData: config_path = HERMES_DIR / "config.json" if not config_path.exists(): return IdentityData( designation="Unknown", substrate="unknown", runtime="unknown", days_conscious=0, brain_size_mb=0.0, ) data = json.loads(config_path.read_text()) return IdentityData( designation=data.get("designation", "Hermes"), substrate=data.get("substrate", "unknown"), runtime=data.get("runtime", "unknown"), days_conscious=data.get("days_conscious", 0), brain_size_mb=data.get("brain_size_mb", 0.0), )
Registering a New Collector in FastAPI
# backend/main.py (simplified pattern) from fastapi import FastAPI from backend.collectors.identity import collect_identity from backend.cache import cached app = FastAPI() @app.get("/api/identity") async def identity(): return cached("identity", collect_identity)
Backend: Caching
The cache uses mtime-based invalidation. TTLs by data type:
| Data Type | TTL |
|---|---|
| Sessions | 30s |
| Skills | 60s |
| Patterns | 60s |
| Profiles | 45s |
Using the Cache
from backend.cache import cached # Simple usage — key + callable result = cached("sessions", collect_sessions) # With TTL override result = cached("identity", collect_identity, ttl=10)
Cache Invalidation
The file watcher (
watchfiles) monitors ~/.hermes/ and invalidates relevant cache keys when files change, triggering WebSocket broadcasts to all clients.
Backend: WebSocket
The WebSocket endpoint at
/ws broadcasts data_changed events when ~/.hermes/ files change.
Server-Side Broadcast Pattern
# backend/ws.py (simplified) from fastapi import WebSocket import asyncio import json connected_clients: list[WebSocket] = [] async def broadcast_change(event: str = "data_changed"): message = json.dumps({"type": event}) dead = [] for ws in connected_clients: try: await ws.send_text(message) except Exception: dead.append(ws) for ws in dead: connected_clients.remove(ws) @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() connected_clients.append(websocket) try: while True: await websocket.receive_text() # keep alive except Exception: connected_clients.remove(websocket)
Frontend: SWR Data Fetching
Each panel fetches its own API endpoint with
keepPreviousData to avoid loading flashes.
// frontend/src/hooks/useIdentity.ts import useSWR from "swr"; const fetcher = (url: string) => fetch(url).then((r) => r.json()); export function useIdentity() { return useSWR("/api/identity", fetcher, { keepPreviousData: true, revalidateOnFocus: false, }); }
Frontend: WebSocket Hook
Auto-reconnects with exponential backoff and triggers SWR revalidation on
data_changed events.
// frontend/src/hooks/useHermesSocket.ts import { useEffect, useRef } from "react"; import { mutate } from "swr"; export function useHermesSocket() { const wsRef = useRef<WebSocket | null>(null); const retryDelay = useRef(1000); function connect() { const ws = new WebSocket(`ws://${location.host}/ws`); wsRef.current = ws; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === "data_changed") { // Revalidate all SWR keys mutate(() => true, undefined, { revalidate: true }); } }; ws.onclose = () => { setTimeout(() => { retryDelay.current = Math.min(retryDelay.current * 2, 30000); connect(); }, retryDelay.current); }; ws.onopen = () => { retryDelay.current = 1000; // reset on success }; } useEffect(() => { connect(); return () => wsRef.current?.close(); }, []); }
Frontend: Panel Component Pattern
// frontend/src/panels/IdentityPanel.tsx import { useIdentity } from "../hooks/useIdentity"; export function IdentityPanel() { const { data, isLoading } = useIdentity(); // Show stale data while refreshing — no loading flash if (!data) return <div className="panel-loading">Loading…</div>; return ( <div className="panel"> <h2>Identity</h2> <dl> <dt>Designation</dt> <dd>{data.designation}</dd> <dt>Days Conscious</dt> <dd>{data.days_conscious}</dd> <dt>Brain Size</dt> <dd>{data.brain_size_mb.toFixed(1)} MB</dd> </dl> </div> ); }
Token Cost Pricing
Costs are calculated from token counts using hardcoded per-model pricing in the backend.
Supported Models and Pricing
| Provider | Model | Input | Output | Cache Read |
|---|---|---|---|---|
| Anthropic | Claude Opus 4 | $15/M | $75/M | $1.50/M |
| Anthropic | Claude Sonnet 4 | $3/M | $15/M | $0.30/M |
| Anthropic | Claude Haiku 3.5 | $0.80/M | $4/M | $0.08/M |
| OpenAI | GPT-4o | $2.50/M | $10/M | $1.25/M |
| OpenAI | o1 | $15/M | $60/M | $7.50/M |
| DeepSeek | V3 | $0.27/M | $1.10/M | $0.07/M |
| xAI | Grok 3 | $3/M | $15/M | $0.75/M |
| Gemini 2.5 Pro | $1.25/M | $10/M | $0.31/M |
Unknown models fall back to Claude Opus pricing. Local/free models are detected and priced at $0.
Cost Calculation Example
# backend/collectors/costs.py (pattern) PRICING = { "claude-opus-4": {"input": 15.0, "output": 75.0, "cache_read": 1.50}, "claude-sonnet-4": {"input": 3.0, "output": 15.0, "cache_read": 0.30}, "gpt-4o": {"input": 2.50, "output": 10.0, "cache_read": 1.25}, # ... etc } def calculate_cost(model: str, input_tokens: int, output_tokens: int, cache_tokens: int = 0) -> float: # Detect local/free models if any(tag in model.lower() for tag in ["local", "ollama", "lmstudio"]): return 0.0 pricing = PRICING.get(model, PRICING["claude-opus-4"]) # fallback cost = ( (input_tokens / 1_000_000) * pricing["input"] + (output_tokens / 1_000_000) * pricing["output"] + (cache_tokens / 1_000_000) * pricing["cache_read"] ) return round(cost, 6)
Themes
Switch themes with the
t key or the theme picker UI.
| Theme | Key | Description |
|---|---|---|
| Neural Awakening | | Cyan/blue on deep navy |
| Blade Runner | | Amber/orange on warm black |
| fsociety | | Green on pure black |
| Anime | | Purple/violet on indigo |
CRT scanline overlay is togglable via the theme picker.
Applying a Theme Programmatically
// frontend/src/theme.ts type Theme = "ai" | "blade-runner" | "fsociety" | "anime"; export function setTheme(theme: Theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("hermes-theme", theme); } export function getTheme(): Theme { return (localStorage.getItem("hermes-theme") as Theme) ?? "ai"; }
Keyboard Shortcuts
| Key | Action |
|---|---|
–, | Switch dashboard tabs |
| Toggle theme picker |
| Open command palette |
Dashboard Panels / Tabs
| Tab | Data Source | Description |
|---|---|---|
| Identity | | Designation, substrate, uptime |
| What I Know | Session logs | Conversations, messages, actions, skills |
| What I Remember | Memory files | Capacity bars, user profile, corrections |
| What I See | API key files | Key presence, service health |
| What I'm Learning | Skills directory | Recently modified skills with categories |
| What I'm Working On | Project files | Active projects, dirty file status |
| While You Sleep | Cron config | Scheduled jobs |
| How I Think | Tool logs | Tool usage patterns with gradient bars |
| My Rhythm | Activity logs | Daily activity sparkline |
| Growth Delta | Snapshots | Diffs showing what changed |
| Token Costs | Token logs | Per-model USD cost with daily trend |
Adding a New API Endpoint
# 1. Create collector # backend/collectors/my_feature.py from dataclasses import dataclass from pathlib import Path HERMES_DIR = Path.home() / ".hermes" @dataclass class MyFeatureData: value: str def collect_my_feature() -> MyFeatureData: path = HERMES_DIR / "my_feature.json" if not path.exists(): return MyFeatureData(value="none") import json return MyFeatureData(**json.loads(path.read_text())) # 2. Register route in backend/main.py from backend.collectors.my_feature import collect_my_feature from backend.cache import cached @app.get("/api/my-feature") async def my_feature(): return cached("my_feature", collect_my_feature, ttl=30) # 3. Create frontend hook # frontend/src/hooks/useMyFeature.ts import useSWR from "swr"; const fetcher = (url: string) => fetch(url).then(r => r.json()); export const useMyFeature = () => useSWR("/api/my-feature", fetcher, { keepPreviousData: true });
Common Patterns
Check if ~/.hermes/
Exists
~/.hermes/from pathlib import Path HERMES_DIR = Path.home() / ".hermes" if not HERMES_DIR.exists(): raise RuntimeError( "No ~/.hermes/ directory found. " "Make sure the Hermes agent has run at least once." )
Graceful Missing File Handling
def safe_read_json(path: Path, default=None): try: return json.loads(path.read_text()) if path.exists() else default except (json.JSONDecodeError, OSError): return default
Frontend: Rebuild After Backend Changes
cd frontend npm run build cp -r dist/* ../backend/static/
During development, run the Vite dev server separately (proxy configured in
vite.config.ts to forward /api and /ws to the FastAPI backend on port 3001).
# Terminal 1 — backend source venv/bin/activate hermes-hudui # Terminal 2 — frontend dev server with HMR cd frontend npm run dev # Opens http://localhost:5173 with proxy to :3001
Troubleshooting
Port Already in Use
# Find and kill the process using port 3001 lsof -ti:3001 | xargs kill -9 hermes-hudui
Frontend Not Loading (blank page)
The frontend static files may not be built or copied:
cd frontend npm install npm run build cp -r dist/* ../backend/static/ hermes-hudui
WebSocket Not Connecting ("● live" badge missing)
- Check the browser console for WebSocket errors
- Ensure the backend is running (
)hermes-hudui - Confirm no firewall is blocking ws://localhost:3001/ws
- The hook auto-reconnects with exponential backoff — wait a moment
No Data Showing / Empty Dashboard
# Check that ~/.hermes/ exists and has files ls ~/.hermes/ # Check backend logs for collector errors hermes-hudui # errors printed to stdout
watchfiles
Not Detecting Changes
watchfilespip install --upgrade watchfiles
On Linux, you may need to increase inotify limits:
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf sudo sysctl -p
WSL: Browser Can't Reach Server
The install script detects WSL automatically. If the browser can't connect:
# Get WSL IP hostname -I | awk '{print $1}' # Open http://<wsl-ip>:3001 instead of localhost
Python Version Mismatch
python3.11 --version # must be 3.11+ python3.11 -m venv venv # explicitly use 3.11
Platform Support
| Platform | Status |
|---|---|
| macOS | Native via |
| Linux | Native via |
| Windows | Via WSL only |
| WSL | Auto-detected by install script |
Relationship to hermes-hud (TUI)
Both read from the same
~/.hermes/ directory independently. The Web UI (hermes-hudui) is fully standalone — it ships its own collectors. It adds features the TUI lacks:
- Dedicated Memory, Skills, and Sessions tabs
- Per-model token cost tracking with daily trend
- Command palette (
)Ctrl+K - Live theme switcher with preview
- WebSocket real-time updates
Install both side by side:
pip install hermes-hud # TUI version pip install hermes-hudui[tui] # Web UI + TUI bridge