From aradotso-trending-skills-37
Deploys and configures Hermes Web UI dashboard for Hermes Agent: Vue 3 app with AI chat streaming, multi-platform channels (Telegram/Discord/Slack), session/cron/job management, analytics, logs, and web terminal.
npx claudepluginhub joshuarweaver/cascade-ai-ml-agents-misc-1 --plugin aradotso-trending-skills-37This skill uses the workspace's default tool permissions.
```markdown
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
---
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
bash <(curl -fsSL https://raw.githubusercontent.com/EKKOLearnAI/hermes-web-ui/main/scripts/setup.sh)
bash <(curl -fsSL https://raw.githubusercontent.com/EKKOLearnAI/hermes-web-ui/main/scripts/setup.sh)
hermes-web-ui start
| Command | Description |
|---|---|
hermes-web-ui start | Start in background (daemon mode) on port 8648 |
hermes-web-ui start --port 9000 | Start on a custom port |
hermes-web-ui stop | Stop the background process |
hermes-web-ui restart | Restart the background process |
hermes-web-ui status | Check if running |
hermes-web-ui update | Update to latest version and restart |
hermes-web-ui -v | Print version number |
hermes-web-ui -h | Show help |
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)
:8642, manages configs, SSE streaming, file uploads, WeChat QR login, model discovery, log reading, static servingAll Hermes-specific code lives under hermes/ directories (api/, components/, views/, stores/) for multi-agent extensibility.
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/
~/.hermes/config.yaml — Channel Behaviorapi_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{
"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 CredentialsTELEGRAM_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
// 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
}
}
}
}
// 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[]>)
},
},
})
<!-- 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>
// 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 * *' },
]
// 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
}
<!-- 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>
<!-- 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>
// 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')
| Method | Path | Description |
|---|---|---|
| GET | /api/hermes/sessions | List all sessions |
| POST | /api/hermes/sessions | Create session |
| DELETE | /api/hermes/sessions/:id | Delete session |
| PATCH | /api/hermes/sessions/:id | Rename session |
| POST | /api/hermes/chat/:id/stream | SSE streaming chat |
| GET | /api/hermes/models | Discover models from auth.json |
| POST | /api/hermes/models/providers | Add custom provider |
| GET | /api/hermes/channels/:platform | Get channel config |
| PUT | /api/hermes/channels/:platform | Save channel config + restart |
| GET | /api/hermes/jobs | List cron jobs |
| POST | /api/hermes/jobs | Create cron job |
| PATCH | /api/hermes/jobs/:id | Update/toggle cron job |
| DELETE | /api/hermes/jobs/:id | Delete cron job |
| POST | /api/hermes/jobs/:id/trigger | Trigger immediately |
| GET | /api/hermes/analytics | Usage stats and cost data |
| GET | /api/hermes/logs | Read log files |
| GET | /api/hermes/skills | List installed skills |
| WS | /api/terminal/:sessionId | PTY WebSocket |
~/.hermes/.env via the Channels UI (writes key=value pairs)~/.hermes/config.yaml (written by BFF on PUT)hermes gateway restart# 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"
}
]
}
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)
}
hermes-web-ui stop
hermes-web-ui start --port 9000
# BFF auto-kills stale processes on the default port at startup
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
# 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
# Check write permissions
ls -la ~/.hermes/
chmod 644 ~/.hermes/config.yaml
chmod 644 ~/.hermes/.env
~/.hermes/.env via Tencent iLink APIhermes gateway statusproxy_buffering off in nginx)Transfer-Encoding: chunked — verify no middleware strips it# node-pty requires native compilation
cd node_modules/node-pty && npm rebuild
# On macOS, may need Xcode CLI tools:
xcode-select --install