install
source · Clone the upstream repo
git clone https://github.com/Aradotso/trending-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Aradotso/trending-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/hermes-web-ui" ~/.claude/skills/aradotso-trending-skills-hermes-web-ui && rm -rf "$T"
manifest:
skills/hermes-web-ui/SKILL.mdsource content
--- name: hermes-web-ui description: Web dashboard for Hermes Agent — multi-platform AI chat, session management, scheduled jobs, usage analytics & channel configuration triggers: - set up hermes web ui dashboard - configure hermes agent channels - add telegram discord slack to hermes - manage hermes chat sessions - schedule cron jobs for hermes agent - view hermes usage analytics and costs - integrate hermes web ui into my project - build custom hermes agent dashboard --- # Hermes Web UI > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. Full-featured Vue 3 web dashboard for [Hermes Agent](https://github.com/NousResearch/hermes-agent). Provides AI chat with streaming, multi-platform channel configuration (Telegram, Discord, Slack, WhatsApp, Matrix, Feishu, WeChat, WeCom), usage analytics, cron job scheduling, skill browsing, log viewing, and an integrated web terminal. --- ## Installation ### Global npm (Recommended) ```bash npm install -g hermes-web-ui hermes-web-ui start # Open http://localhost:8648
One-line Setup (Debian/Ubuntu/macOS)
bash <(curl -fsSL https://raw.githubusercontent.com/EKKOLearnAI/hermes-web-ui/main/scripts/setup.sh)
WSL
bash <(curl -fsSL https://raw.githubusercontent.com/EKKOLearnAI/hermes-web-ui/main/scripts/setup.sh) hermes-web-ui start
CLI Commands
| Command | Description |
|---|---|
| Start in background (daemon mode) on port 8648 |
| Start on a custom port |
| Stop the background process |
| Restart the background process |
| Check if running |
| Update to latest version and restart |
| Print version number |
| Show help |
Architecture
Browser → BFF (Koa, :8648) → Hermes Gateway (:8642) ↓ Hermes CLI (sessions, logs, version) ↓ ~/.hermes/config.yaml (channel behavior) ~/.hermes/auth.json (credential pool) ~/.hermes/.env (platform credentials)
- Frontend: Vue 3 + TypeScript + Vite + Naive UI + Pinia + Vue Router + vue-i18n + SCSS + markdown-it + highlight.js
- BFF: Koa 2 server — proxies to Hermes on
, manages configs, SSE streaming, file uploads, WeChat QR login, model discovery, log reading, static serving:8642 - Terminal: node-pty + @xterm/xterm over WebSocket
All Hermes-specific code lives under
hermes/ directories (api/, components/, views/, stores/) for multi-agent extensibility.
Development Setup
git clone https://github.com/EKKOLearnAI/hermes-web-ui.git cd hermes-web-ui npm install npm run dev # Frontend: http://localhost:5173 # BFF: http://localhost:8648
npm run build # outputs to dist/
Configuration Files
~/.hermes/config.yaml
— Channel Behavior
~/.hermes/config.yamlapi_server: host: 0.0.0.0 port: 8642 telegram: enabled: true require_mention: false reactions: true free_response_chats: ["@my_chat"] discord: enabled: true require_mention: true auto_thread: true reactions: true channel_allowlist: [] channel_ignorelist: [] slack: enabled: false require_mention: true handle_bot_messages: false whatsapp: enabled: false require_mention: true mention_patterns: ["@hermes"] matrix: enabled: false homeserver: "https://matrix.org" auto_thread: false dm_mention_threads: true
~/.hermes/auth.json
— Credential Pool
~/.hermes/auth.json{ "providers": [ { "name": "openai", "base_url": "https://api.openai.com/v1", "api_key": "$OPENAI_API_KEY", "models": ["gpt-4o", "gpt-4o-mini"] }, { "name": "custom", "base_url": "https://my-provider.example.com/v1", "api_key": "$CUSTOM_API_KEY" } ] }
~/.hermes/.env
— Platform Credentials
~/.hermes/.envTELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN DISCORD_BOT_TOKEN=$DISCORD_BOT_TOKEN SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN SLACK_APP_TOKEN=$SLACK_APP_TOKEN FEISHU_APP_ID=$FEISHU_APP_ID FEISHU_APP_SECRET=$FEISHU_APP_SECRET WECOM_BOT_ID=$WECOM_BOT_ID WECOM_BOT_SECRET=$WECOM_BOT_SECRET
Frontend — Key Patterns
API Client (BFF proxy calls)
// packages/client/src/hermes/api/chat.ts import axios from 'axios' const BASE = '/api/hermes' export async function sendMessage( sessionId: string, content: string, model?: string ): Promise<void> { const response = await fetch(`${BASE}/chat/${sessionId}/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content, model }), }) const reader = response.body!.getReader() const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value) // parse SSE lines for (const line of chunk.split('\n')) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)) // handle delta, tool_call, done events } } } }
Pinia Store — Sessions
// packages/client/src/hermes/stores/sessions.ts import { defineStore } from 'pinia' import axios from 'axios' interface Session { id: string name: string source: string model: string createdAt: string } export const useSessionStore = defineStore('sessions', { state: () => ({ sessions: [] as Session[], activeSessionId: null as string | null, }), actions: { async fetchSessions() { const { data } = await axios.get('/api/hermes/sessions') this.sessions = data }, async createSession(name: string, model: string) { const { data } = await axios.post('/api/hermes/sessions', { name, model }) this.sessions.unshift(data) this.activeSessionId = data.id return data }, async deleteSession(id: string) { await axios.delete(`/api/hermes/sessions/${id}`) this.sessions = this.sessions.filter(s => s.id !== id) if (this.activeSessionId === id) this.activeSessionId = null }, async renameSession(id: string, name: string) { await axios.patch(`/api/hermes/sessions/${id}`, { name }) const s = this.sessions.find(s => s.id === id) if (s) s.name = name }, }, getters: { sessionsBySource: (state) => { return state.sessions.reduce((acc, s) => { ;(acc[s.source] ??= []).push(s) return acc }, {} as Record<string, Session[]>) }, }, })
Vue Component — Streaming Chat Message
<!-- packages/client/src/hermes/components/ChatMessage.vue --> <template> <div class="message" :class="role"> <div v-if="role === 'assistant'" class="content"> <div v-html="renderedMarkdown" /> <ToolCallExpander v-for="call in toolCalls" :key="call.id" :call="call" /> <span v-if="streaming" class="cursor">▋</span> </div> <div v-else class="content">{{ content }}</div> <div class="meta"> <n-tag size="small">{{ model }}</n-tag> <span v-if="tokens">{{ tokens }} tokens</span> </div> </div> </template> <script setup lang="ts"> import { computed } from 'vue' import MarkdownIt from 'markdown-it' import hljs from 'highlight.js' const md = new MarkdownIt({ highlight: (str, lang) => { if (lang && hljs.getLanguage(lang)) { return hljs.highlight(str, { language: lang }).value } return '' }, }) const props = defineProps<{ role: 'user' | 'assistant' content: string model?: string tokens?: number toolCalls?: Array<{ id: string; name: string; args: unknown; result: unknown }> streaming?: boolean }>() const renderedMarkdown = computed(() => md.render(props.content)) </script>
Cron Job Management
// packages/client/src/hermes/api/jobs.ts import axios from 'axios' export interface CronJob { id: string name: string cron: string prompt: string enabled: boolean lastRun?: string nextRun?: string } export const jobsApi = { list: () => axios.get<CronJob[]>('/api/hermes/jobs').then(r => r.data), create: (job: Omit<CronJob, 'id'>) => axios.post<CronJob>('/api/hermes/jobs', job).then(r => r.data), update: (id: string, patch: Partial<CronJob>) => axios.patch<CronJob>(`/api/hermes/jobs/${id}`, patch).then(r => r.data), delete: (id: string) => axios.delete(`/api/hermes/jobs/${id}`), trigger: (id: string) => axios.post(`/api/hermes/jobs/${id}/trigger`), toggle: (id: string, enabled: boolean) => axios.patch(`/api/hermes/jobs/${id}`, { enabled }), } // Common cron presets export const CRON_PRESETS = [ { label: 'Every minute', value: '* * * * *' }, { label: 'Every hour', value: '0 * * * *' }, { label: 'Daily at 9am', value: '0 9 * * *' }, { label: 'Every Monday', value: '0 9 * * 1' }, { label: 'First of month', value: '0 9 1 * *' }, ]
Model Discovery
// packages/client/src/hermes/api/models.ts import axios from 'axios' export interface ModelInfo { id: string provider: string endpoint: string } // Fetches models by reading auth.json then hitting each provider's /v1/models export async function discoverModels(): Promise<ModelInfo[]> { const { data } = await axios.get<ModelInfo[]>('/api/hermes/models') return data } // Add a custom OpenAI-compatible provider export async function addProvider(config: { name: string base_url: string api_key: string }) { const { data } = await axios.post('/api/hermes/models/providers', config) return data }
Channel Configuration Component
<!-- packages/client/src/hermes/views/Channels/TelegramConfig.vue --> <template> <n-form :model="form" label-placement="left" label-width="180px"> <n-form-item label="Bot Token"> <n-input v-model:value="form.token" type="password" show-password-on="click" placeholder="Enter from @BotFather" /> </n-form-item> <n-form-item label="Require Mention"> <n-switch v-model:value="form.requireMention" /> </n-form-item> <n-form-item label="Enable Reactions"> <n-switch v-model:value="form.reactions" /> </n-form-item> <n-form-item> <n-button type="primary" :loading="saving" @click="save"> Save & Restart Gateway </n-button> </n-form-item> </n-form> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import axios from 'axios' import { useMessage } from 'naive-ui' const msg = useMessage() const saving = ref(false) const form = ref({ token: '', requireMention: false, reactions: true, }) onMounted(async () => { const { data } = await axios.get('/api/hermes/channels/telegram') Object.assign(form.value, data) }) async function save() { saving.value = true try { await axios.put('/api/hermes/channels/telegram', form.value) msg.success('Telegram config saved. Gateway restarting…') } finally { saving.value = false } } </script>
Web Terminal Integration
<!-- packages/client/src/hermes/views/Terminal/TerminalTab.vue --> <template> <div ref="termEl" class="terminal-container" /> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue' import { Terminal } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' import '@xterm/xterm/css/xterm.css' const props = defineProps<{ sessionId: string }>() const termEl = ref<HTMLElement>() onMounted(() => { const term = new Terminal({ cursorBlink: true, fontSize: 14 }) const fit = new FitAddon() term.loadAddon(fit) term.open(termEl.value!) fit.fit() const ws = new WebSocket( `ws://${location.host}/api/terminal/${props.sessionId}` ) ws.onmessage = e => term.write(e.data) term.onData(data => ws.send(data)) const ro = new ResizeObserver(() => { fit.fit() ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })) }) ro.observe(termEl.value!) onUnmounted(() => { ws.close() ro.disconnect() term.dispose() }) }) </script> <style scoped> .terminal-container { height: 100%; background: #1a1a2e; padding: 8px; } </style>
i18n Setup
// packages/client/src/i18n/index.ts import { createI18n } from 'vue-i18n' import en from './locales/en.json' import zh from './locales/zh.json' export const i18n = createI18n({ legacy: false, locale: localStorage.getItem('locale') ?? 'en', fallbackLocale: 'en', messages: { en, zh }, }) // Usage in component // const { t } = useI18n() // t('chat.newSession')
BFF API Endpoints (Koa)
| Method | Path | Description |
|---|---|---|
| GET | | List all sessions |
| POST | | Create session |
| DELETE | | Delete session |
| PATCH | | Rename session |
| POST | | SSE streaming chat |
| GET | | Discover models from auth.json |
| POST | | Add custom provider |
| GET | | Get channel config |
| PUT | | Save channel config + restart |
| GET | | List cron jobs |
| POST | | Create cron job |
| PATCH | | Update/toggle cron job |
| DELETE | | Delete cron job |
| POST | | Trigger immediately |
| GET | | Usage stats and cost data |
| GET | | Read log files |
| GET | | List installed skills |
| WS | | PTY WebSocket |
Common Patterns
Adding a New Platform Channel
- Add credentials to
via the Channels UI (writes key=value pairs)~/.hermes/.env - Add behavior config to
(written by BFF on PUT)~/.hermes/config.yaml - BFF auto-triggers
hermes gateway restart
Custom OpenAI-Compatible Provider
# Via UI: Settings → Model Management → Add Provider # Or directly in ~/.hermes/auth.json:
{ "providers": [ { "name": "my-local-llm", "base_url": "http://localhost:11434/v1", "api_key": "ollama" } ] }
SSE Streaming (Raw Fetch)
async function* streamChat(sessionId: string, prompt: string) { const res = await fetch(`/api/hermes/chat/${sessionId}/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: prompt }), }) const reader = res.body!.getReader() const dec = new TextDecoder() let buf = '' while (true) { const { done, value } = await reader.read() if (done) break buf += dec.decode(value, { stream: true }) const lines = buf.split('\n') buf = lines.pop()! for (const line of lines) { if (line.startsWith('data: ')) { yield JSON.parse(line.slice(6)) } } } } // Usage for await (const event of streamChat('sess_123', 'Hello!')) { if (event.type === 'delta') appendText(event.content) if (event.type === 'done') finalize(event.usage) }
Troubleshooting
Port Already in Use
hermes-web-ui stop hermes-web-ui start --port 9000 # BFF auto-kills stale processes on the default port at startup
Gateway Not Connecting
hermes-web-ui status # Check ~/.hermes/config.yaml has correct api_server.port (default 8642) # BFF validates and patches missing api_server fields on startup # Backup created at ~/.hermes/config.yaml.bak before any modification
Models Not Appearing
# Verify auth.json is valid JSON cat ~/.hermes/auth.json | python3 -m json.tool # Check provider endpoint is reachable curl -H "Authorization: Bearer $YOUR_API_KEY" \ https://api.openai.com/v1/models
Channel Config Not Saving
# Check write permissions ls -la ~/.hermes/ chmod 644 ~/.hermes/config.yaml chmod 644 ~/.hermes/.env
WeChat QR Login
- Open Channels → WeChat → click "Generate QR Code"
- Scan with WeChat mobile app within 90 seconds
- Credentials auto-saved to
via Tencent iLink API~/.hermes/.env
SSE Streaming Stops Mid-Response
- Check Hermes gateway is running:
hermes gateway status - Ensure no reverse proxy is buffering (set
in nginx)proxy_buffering off - BFF proxies SSE with
— verify no middleware strips itTransfer-Encoding: chunked
Web Terminal Not Opening
# node-pty requires native compilation cd node_modules/node-pty && npm rebuild # On macOS, may need Xcode CLI tools: xcode-select --install