| name | hermes-client-web-ui |
| description | Web-based chat interface for Hermes Agent with multi-profile management, streaming chat, and interactive terminal integration |
| triggers | ["how do I use Hermes Client web interface","set up Hermes Client dashboard","manage multiple Hermes agent profiles","configure Hermes Client chat UI","integrate with Hermes Agent CLI","stream Hermes conversations in browser","use Hermes Client API endpoints","troubleshoot Hermes Client connection issues"] |
Hermes Client Web UI
Skill by ara.so — Devtools Skills collection.
A web-based chat interface for the Hermes Agent by Nous Research. Manages multiple Hermes profiles as separate "agents", runs conversations with full streaming via SSE, and provides an interactive terminal for setup commands. Each UI agent maps 1:1 to a Hermes profile with its own home directory, config, and sessions.
Installation
Prerequisites:
- Node.js 18+
- Hermes Agent installed with
hermes on your PATH
- Git for Windows (Windows only, for auto-update)
- Visual Studio Build Tools (Windows only, for native modules)
hermes --version
hermes status
git clone https://github.com/lotsoftick/hermes_client.git
cd hermes_client
npm start
npm start builds, deploys to ~/.hermes_client, installs auto-start (LaunchAgent/Startup), and creates the global hermes_client command.
Default URLs:
Default credentials:
- Email:
admin@admin.com
- Password:
123456
Service Management
After npm start, use the global command from any directory:
hermes_client start
hermes_client stop
hermes_client restart
hermes_client status
hermes_client uninstall
hermes_client uninstall --purge
Development Mode
npm run dev
npm run setup
npm run stop
Configuration
Port Configuration (~/.hermes_client/.env)
Created automatically on first run:
API_PORT=18889
CLIENT_PORT=18888
Apply changes:
hermes_client restart
npm run dev
API Configuration (api/.env)
Auto-generated from api/.env.example:
NODE_ENV=development
JWT_SECRET=<random-generated>
DB_PATH=./data/hermes.sqlite
PORT=18889
ALLOWED_DOMAIN=
HERMES_STRICT_CORS=0
API_PUBLIC_URL=
HERMES_BIN=
HERMES_HOME=~/.hermes
HERMES_CLIENT_UPLOADS_DIR=~/.hermes_client/uploads
HERMES_SINGLE_USER_MODE=1
Key variables:
HERMES_BIN: Override Hermes binary path if not on PATH
HERMES_STRICT_CORS=1: Enable strict CORS with ALLOWED_DOMAIN allowlist
HERMES_SINGLE_USER_MODE: Lock UI to single-user account page (1/true/yes/on or 0/false/no/off)
Architecture
CLI-Driven Streaming
Every chat turn spawns hermes -p <profile> chat -Q -q "<message>" and streams stdout over Server-Sent Events:
app.post('/api/conversations/:conversationId/messages', async (req, res) => {
const { profileName, message } = req.body;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const hermes = spawn('hermes', [
'-p', profileName,
'chat',
'-Q',
'-q', message
]);
hermes.stdout.on('data', (chunk) => {
res.write(`data: ${JSON.stringify({ content: chunk.toString() })}\n\n`);
});
hermes.on('close', () => {
res.write('data: [DONE]\n\n');
res.end();
});
});
Profile Management
Each UI agent maps to a Hermes profile:
hermes profile add <name>
hermes profile delete <name>
hermes profile list
hermes -p <name> model
Session Sync
Sessions started in standalone hermes REPL auto-appear in sidebar. Backend watches ~/.hermes/profiles/<profile>/sessions/*.json:
import { watch } from 'fs';
import { readdir, readFile } from 'fs/promises';
async function syncSessions(profileName: string) {
const sessionsDir = `${process.env.HERMES_HOME}/profiles/${profileName}/sessions`;
const files = await readdir(sessionsDir);
for (const file of files.filter(f => f.endsWith('.json'))) {
const session = JSON.parse(await readFile(`${sessionsDir}/${file}`, 'utf-8'));
await db.run(
'INSERT OR REPLACE INTO conversations (session_key, profile_name, title, updated_at) VALUES (?, ?, ?, ?)',
[session.key, profileName, session.title, session.updated_at]
);
}
watch(sessionsDir, { persistent: false }, {
(filename?.()) {
}
});
}
File Uploads
Files stored under ~/.hermes_client/uploads/<conversationId>/ and passed to Hermes by absolute path:
app.post('/api/conversations/:conversationId/upload', upload.single('file'), (req, res) => {
const { conversationId } = req.params;
const uploadDir = `${process.env.HERMES_CLIENT_UPLOADS_DIR}/${conversationId}`;
const absolutePath = path.resolve(uploadDir, req.file.filename);
if (req.file.mimetype.startsWith('image/')) {
spawn('hermes', ['-p', profile, 'chat', '--image', absolutePath, '-q', message]);
} else {
spawn('hermes', ['-p', profile, 'chat', '-q', `File: ${absolutePath}\n\n${message}`]);
}
res.json({ path: `/uploads/${conversationId}/${req.file.filename}` });
});
API Endpoints
Authentication
fetch('http://localhost:18889/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'admin@admin.com',
password: '123456'
})
});
headers: { 'Authorization': `Bearer ${token}` }
Agents (Profiles)
fetch('http://localhost:18889/api/agents', {
headers: { 'Authorization': `Bearer ${token}` }
});
fetch('http://localhost:18889/api/agents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'research-assistant' })
});
fetch('http://localhost:18889/api/agents/research-assistant', {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
Conversations
fetch('http://localhost:18889/api/conversations?profileName=default', {
headers: { 'Authorization': `Bearer ${token}` }
});
fetch('http://localhost:18889/api/conversations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
profileName: 'default',
title: 'New Chat'
})
});
fetch('http://localhost:18889/api/conversations/abc123/messages', {
headers: { 'Authorization': `Bearer ${token}` }
});
Streaming Chat
const eventSource = new EventSource(
'http://localhost:18889/api/conversations/abc123/messages',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: 'Hello!',
profileName: 'default'
})
}
);
eventSource.onmessage = (event) => {
if (event.data === '[DONE]') {
eventSource.close();
} else {
const chunk = JSON.parse(event.data);
console.log(chunk.content);
}
};
Interactive Terminal (PTY)
const ws = new WebSocket('ws://localhost:18889/ws/pty?token=' + token);
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'start',
command: 'hermes',
args: ['-p', 'default', 'model'],
cwd: process.env.HOME
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'output') {
terminal.write(msg.data);
}
};
terminal.onData((data) => {
ws.send(JSON.stringify({ type: 'input', data }));
});
terminal.onResize(({ cols, rows }) => {
ws.send(JSON.stringify({ : , cols, rows }));
});
Client Patterns
React Hook for Streaming
import { useEffect, useState } from 'react';
function useStreamingChat(conversationId: string, token: string) {
const [messages, setMessages] = useState<string[]>([]);
const [streaming, setStreaming] = useState(false);
const sendMessage = async (content: string, profileName: string) => {
setStreaming(true);
const response = await fetch(`/api/conversations/${conversationId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ content, profileName })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = reader.();
(done) ;
chunk = decoder.(value);
lines = chunk.().( l.());
( line lines) {
data = line.();
(data === ) {
();
;
}
parsed = .(data);
( [...prev, parsed.]);
}
}
};
{ messages, streaming, sendMessage };
}
File Upload with Preview
async function uploadFile(
conversationId: string,
file: File,
token: string
): Promise<string> {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`/api/conversations/${conversationId}/upload`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
});
const { path } = await response.json();
return `${API_BASE_URL}${path}`;
}
function MessageComposer() {
const [files, setFiles] = useState<File[]>([]);
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setFiles([...files, ...Array.from(e..)]);
};
= () => {
uploadedUrls = .(
files.( (conversationId, f, token))
);
};
(
);
}
Database Schema
SQLite at ~/.hermes_client/data/hermes.sqlite:
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
name TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE conversations (
id TEXT PRIMARY KEY,
session_key TEXT,
profile_name TEXT NOT NULL,
title TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT,
role TEXT CHECK(role IN ('user', 'assistant', 'system')),
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);
CREATE TABLE themes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
colors TEXT
);
Troubleshooting
Hermes binary not found
which hermes
hermes --version
HERMES_BIN=/path/to/hermes
Port conflicts
lsof -i :18888
lsof -i :18889
API_PORT=19000
CLIENT_PORT=19001
hermes_client restart
CORS issues (remote access)
ALLOWED_DOMAIN=192.168.1.100:18888,100.64.0.1:18888
HERMES_STRICT_CORS=1
hermes_client restart
Session sync not working
ls ~/.hermes/profiles/default/sessions/
hermes_client restart
Interactive terminal (PTY) not connecting
wscat -c "ws://localhost:18889/ws/pty?token=YOUR_JWT_TOKEN"
python --version
Windows install fails
Database locked errors
hermes_client stop
ps aux | grep hermes_client
rm ~/.hermes_client/data/hermes.sqlite-wal
hermes_client start
Uploads not working
ls -la ~/.hermes_client/uploads/
df -h ~/.hermes_client/
Common Patterns
Adding a new agent/profile with model config
const response = await fetch('/api/agents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'code-reviewer' })
});
const ws = new WebSocket(`ws://localhost:18889/ws/pty?token=${token}`);
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'start',
command: 'hermes',
args: ['-p', 'code-reviewer', 'model'],
cwd: process.env.HOME
}));
};
Continuing terminal conversation in web UI
hermes -p myprofile chat
> What is TypeScript?
Resuming web conversation in terminal
hermes -p myprofile chat -r abc123
> Continue our TypeScript discussion
Multi-file context in chat
const files = ['src/index.ts', 'package.json', 'README.md'];
const uploads = await Promise.all(
files.map(f => uploadFile(conversationId, new File([...], f), token))
);
await sendMessage(
`Review these files for issues:\n${uploads.map(u => u.path).join('\n')}`,
profileName
);