hermes-client-web-ui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHermes Client Web UI
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.
由ara.so开发的Skill——Devtools Skills合集。
这是Nous Research开发的Hermes Agent的Web版聊天界面。它将多个Hermes配置文件作为独立的“agent”进行管理,通过SSE实现完整的流式对话,并提供交互式终端用于执行设置命令。每个UI agent与Hermes配置文件一一对应,拥有独立的主目录、配置和会话。
Installation
安装
Prerequisites:
- Node.js 18+
- Hermes Agent installed with on your
hermesPATH - Git for Windows (Windows only, for auto-update)
- Visual Studio Build Tools (Windows only, for native modules)
bash
undefined前置条件:
- Node.js 18+
- 已安装Hermes Agent,且命令在
hermes中PATH - Git for Windows(仅Windows系统,用于自动更新)
- Visual Studio Build Tools(仅Windows系统,用于原生模块)
bash
undefinedVerify Hermes is installed
验证Hermes是否安装
hermes --version
hermes status
hermes --version
hermes status
Clone and start
克隆并启动
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:
- Client: http://localhost:18888
- API: http://localhost:18889
Default credentials:
- Email: `admin@admin.com`
- Password: `123456`git clone https://github.com/lotsoftick/hermes_client.git
cd hermes_client
npm start
`npm start`会进行构建,部署到`~/.hermes_client`,设置自动启动(LaunchAgent/Startup),并创建全局`hermes_client`命令。
默认URL:
- 客户端:http://localhost:18888
- API:http://localhost:18889
默认凭据:
- 邮箱:`admin@admin.com`
- 密码:`123456`Service Management
服务管理
After , use the global command from any directory:
npm startbash
undefined执行后,可在任意目录使用全局命令:
npm startbash
undefinedStart/stop/restart
启动/停止/重启
hermes_client start
hermes_client stop
hermes_client restart
hermes_client status
hermes_client start
hermes_client stop
hermes_client restart
hermes_client status
Uninstall
卸载
hermes_client uninstall # Keeps database
hermes_client uninstall --purge # Deletes database (confirms)
undefinedhermes_client uninstall # 保留数据库
hermes_client uninstall --purge # 删除数据库(需确认)
undefinedDevelopment Mode
开发模式
bash
undefinedbash
undefinedHot reload (API + Client)
热重载(API + 客户端)
npm run dev
npm run dev
Generate .env only
仅生成.env文件
npm run setup
npm run setup
Stop services
停止服务
npm run stop
undefinednpm run stop
undefinedConfiguration
配置
Port Configuration (~/.hermes_client/.env
)
~/.hermes_client/.env端口配置(~/.hermes_client/.env
)
~/.hermes_client/.envCreated automatically on first run:
env
API_PORT=18889
CLIENT_PORT=18888Apply changes:
bash
hermes_client restart # Production
npm run dev # Development首次运行时自动创建:
env
API_PORT=18889
CLIENT_PORT=18888应用更改:
bash
hermes_client restart # 生产环境
npm run dev # 开发环境API Configuration (api/.env
)
api/.envAPI配置(api/.env
)
api/.envAuto-generated from :
api/.env.exampleenv
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=1Key variables:
- : Override Hermes binary path if not on
HERMES_BINPATH - : Enable strict CORS with
HERMES_STRICT_CORS=1allowlistALLOWED_DOMAIN - : Lock UI to single-user account page (
HERMES_SINGLE_USER_MODEor1/true/yes/on)0/false/no/off
从自动生成:
api/.env.exampleenv
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关键变量:
- :若
HERMES_BIN不在hermes中,可指定Hermes二进制文件路径PATH - :启用严格CORS,配合
HERMES_STRICT_CORS=1白名单ALLOWED_DOMAIN - :将UI锁定为单用户账户页面(取值为
HERMES_SINGLE_USER_MODE或1/true/yes/on)0/false/no/off
Architecture
架构
CLI-Driven Streaming
CLI驱动的流式传输
Every chat turn spawns and streams stdout over Server-Sent Events:
hermes -p <profile> chat -Q -q "<message>"typescript
// Example API streaming endpoint structure
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', // Quiet mode
'-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();
});
});每次对话轮次都会启动,并通过Server-Sent Events流式传输标准输出:
hermes -p <profile> chat -Q -q "<message>"typescript
// 示例API流式端点结构
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:
bash
undefined每个UI agent对应一个Hermes配置文件:
bash
undefinedBackend runs these commands
后端执行这些命令
hermes profile add <name>
hermes profile delete <name>
hermes profile list
hermes -p <name> model # Via interactive terminal
undefinedhermes profile add <name>
hermes profile delete <name>
hermes profile list
hermes -p <name> model # 通过交互式终端
undefinedSession Sync
会话同步
Sessions started in standalone REPL auto-appear in sidebar. Backend watches :
hermes~/.hermes/profiles/<profile>/sessions/*.jsontypescript
// Example session sync pattern
import { watch } from 'fs';
import { readdir, readFile } from 'fs/promises';
async function syncSessions(profileName: string) {
const sessionsDir = `${process.env.HERMES_HOME}/profiles/${profileName}/sessions`;
// Initial load
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'));
// Insert/update in SQLite
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 for changes
watch(sessionsDir, { persistent: false }, (event, filename) => {
if (filename?.endsWith('.json')) {
// Re-sync changed session
}
});
}在独立 REPL中启动的会话会自动出现在侧边栏。后端监听:
hermes~/.hermes/profiles/<profile>/sessions/*.jsontypescript
// 示例会话同步逻辑
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'));
// 插入/更新SQLite
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 }, (event, filename) => {
if (filename?.endsWith('.json')) {
// 重新同步修改后的会话
}
});
}File Uploads
文件上传
Files stored under and passed to Hermes by absolute path:
~/.hermes_client/uploads/<conversationId>/typescript
// Example upload handling
app.post('/api/conversations/:conversationId/upload', upload.single('file'), (req, res) => {
const { conversationId } = req.params;
const uploadDir = `${process.env.HERMES_CLIENT_UPLOADS_DIR}/${conversationId}`;
// multer stores file at uploadDir/filename
const absolutePath = path.resolve(uploadDir, req.file.filename);
// For images, pass via --image flag
if (req.file.mimetype.startsWith('image/')) {
spawn('hermes', ['-p', profile, 'chat', '--image', absolutePath, '-q', message]);
} else {
// For other files, reference in prompt
spawn('hermes', ['-p', profile, 'chat', '-q', `File: ${absolutePath}\n\n${message}`]);
}
res.json({ path: `/uploads/${conversationId}/${req.file.filename}` });
});文件存储在,并通过绝对路径传递给Hermes:
~/.hermes_client/uploads/<conversationId>/typescript
// 示例上传处理
app.post('/api/conversations/:conversationId/upload', upload.single('file'), (req, res) => {
const { conversationId } = req.params;
const uploadDir = `${process.env.HERMES_CLIENT_UPLOADS_DIR}/${conversationId}`;
// multer将文件存储在uploadDir/filename
const absolutePath = path.resolve(uploadDir, req.file.filename);
// 图片文件通过--image参数传递
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
API端点
Authentication
认证
typescript
// POST /api/auth/login
fetch('http://localhost:18889/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'admin@admin.com',
password: '123456'
})
});
// Response: { token: string, user: { id, email, name } }
// JWT required for all other endpoints
headers: { 'Authorization': `Bearer ${token}` }typescript
// POST /api/auth/login
fetch('http://localhost:18889/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'admin@admin.com',
password: '123456'
})
});
// 响应:{ token: string, user: { id, email, name } }
// 所有其他端点需要JWT
headers: { 'Authorization': `Bearer ${token}` }Agents (Profiles)
Agents(配置文件)
typescript
// GET /api/agents - List all profiles
fetch('http://localhost:18889/api/agents', {
headers: { 'Authorization': `Bearer ${token}` }
});
// POST /api/agents - Create new profile
fetch('http://localhost:18889/api/agents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'research-assistant' })
});
// DELETE /api/agents/:name - Delete profile
fetch('http://localhost:18889/api/agents/research-assistant', {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});typescript
// GET /api/agents - 列出所有配置文件
fetch('http://localhost:18889/api/agents', {
headers: { 'Authorization': `Bearer ${token}` }
});
// POST /api/agents - 创建新配置文件
fetch('http://localhost:18889/api/agents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'research-assistant' })
});
// DELETE /api/agents/:name - 删除配置文件
fetch('http://localhost:18889/api/agents/research-assistant', {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});Conversations
对话
typescript
// GET /api/conversations?profileName=default - List conversations
fetch('http://localhost:18889/api/conversations?profileName=default', {
headers: { 'Authorization': `Bearer ${token}` }
});
// POST /api/conversations - Create conversation
fetch('http://localhost:18889/api/conversations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
profileName: 'default',
title: 'New Chat'
})
});
// GET /api/conversations/:id/messages - Get messages
fetch('http://localhost:18889/api/conversations/abc123/messages', {
headers: { 'Authorization': `Bearer ${token}` }
});typescript
// GET /api/conversations?profileName=default - 列出对话
fetch('http://localhost:18889/api/conversations?profileName=default', {
headers: { 'Authorization': `Bearer ${token}` }
});
// POST /api/conversations - 创建对话
fetch('http://localhost:18889/api/conversations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
profileName: 'default',
title: 'New Chat'
})
});
// GET /api/conversations/:id/messages - 获取消息
fetch('http://localhost:18889/api/conversations/abc123/messages', {
headers: { 'Authorization': `Bearer ${token}` }
});Streaming Chat
流式聊天
typescript
// POST /api/conversations/:id/messages - Send message (SSE stream)
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);
}
};typescript
// POST /api/conversations/:id/messages - 发送消息(SSE流)
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)
交互式终端(PTY)
typescript
// WebSocket upgrade for xterm.js
const ws = new WebSocket('ws://localhost:18889/ws/pty?token=' + token);
ws.onopen = () => {
// Start model config command
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); // xterm.js instance
}
};
// Send input
terminal.onData((data) => {
ws.send(JSON.stringify({ type: 'input', data }));
});
// Resize PTY
terminal.onResize(({ cols, rows }) => {
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
});typescript
// 为xterm.js升级WebSocket
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); // xterm.js实例
}
};
// 发送输入
terminal.onData((data) => {
ws.send(JSON.stringify({ type: 'input', data }));
});
// 调整PTY尺寸
terminal.onResize(({ cols, rows }) => {
ws.send(JSON.stringify({ type: 'resize', cols, rows }));
});Client Patterns
客户端模式
React Hook for Streaming
流式聊天的React Hook
typescript
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 } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(l => l.startsWith('data: '));
for (const line of lines) {
const data = line.slice(6);
if (data === '[DONE]') {
setStreaming(false);
break;
}
const parsed = JSON.parse(data);
setMessages(prev => [...prev, parsed.content]);
}
}
};
return { messages, streaming, sendMessage };
}typescript
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 } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(l => l.startsWith('data: '));
for (const line of lines) {
const data = line.slice(6);
if (data === '[DONE]') {
setStreaming(false);
break;
}
const parsed = JSON.parse(data);
setMessages(prev => [...prev, parsed.content]);
}
}
};
return { messages, streaming, sendMessage };
}File Upload with Preview
文件上传与预览
typescript
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}`;
}
// Usage in React
function MessageComposer() {
const [files, setFiles] = useState<File[]>([]);
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setFiles([...files, ...Array.from(e.dataTransfer.files)]);
};
const handleSend = async (message: string) => {
const uploadedUrls = await Promise.all(
files.map(f => uploadFile(conversationId, f, token))
);
// Send message with file references
};
return (
<div onDrop={handleDrop} onDragOver={e => e.preventDefault()}>
{/* Composer UI */}
</div>
);
}typescript
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}`;
}
// 在React中使用
function MessageComposer() {
const [files, setFiles] = useState<File[]>([]);
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setFiles([...files, ...Array.from(e.dataTransfer.files)]);
};
const handleSend = async (message: string) => {
const uploadedUrls = await Promise.all(
files.map(f => uploadFile(conversationId, f, token))
);
// 发送包含文件引用的消息
};
return (
<div onDrop={handleDrop} onDragOver={e => e.preventDefault()}>
{/* 编辑器UI */}
</div>
);
}Database Schema
数据库架构
SQLite at :
~/.hermes_client/data/hermes.sqlitesql
-- Users (JWT auth)
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
);
-- Conversations (mirrors Hermes sessions)
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
);
-- Messages (mirrors Hermes session messages)
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
);
-- Themes
CREATE TABLE themes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
colors TEXT -- JSON blob
);SQLite数据库位于:
~/.hermes_client/data/hermes.sqlitesql
-- 用户表(JWT认证)
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
);
-- 对话表(镜像Hermes会话)
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
);
-- 消息表(镜像Hermes会话消息)
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 -- JSON格式的颜色配置
);Troubleshooting
故障排除
Hermes binary not found
找不到Hermes二进制文件
bash
undefinedbash
undefinedCheck PATH
检查PATH
which hermes
hermes --version
which hermes
hermes --version
If not found, set explicit path in api/.env
若未找到,在api/.env中指定路径
HERMES_BIN=/path/to/hermes
HERMES_BIN=/path/to/hermes
Common locations checked automatically:
自动检查的常见位置:
~/.local/bin/hermes
~/.local/bin/hermes
~/.hermes/hermes-agent/venv/bin/hermes
~/.hermes/hermes-agent/venv/bin/hermes
/opt/homebrew/bin/hermes (macOS)
/opt/homebrew/bin/hermes(macOS)
/usr/local/bin/hermes
/usr/local/bin/hermes
undefinedundefinedPort conflicts
端口冲突
bash
undefinedbash
undefinedCheck what's using the port
检查端口占用情况
lsof -i :18888
lsof -i :18889
lsof -i :18888
lsof -i :18889
Change ports in ~/.hermes_client/.env
在~/.hermes_client/.env中修改端口
API_PORT=19000
CLIENT_PORT=19001
hermes_client restart
undefinedAPI_PORT=19000
CLIENT_PORT=19001
hermes_client restart
undefinedCORS issues (remote access)
CORS问题(远程访问)
bash
undefinedbash
undefinedEdit ~/.hermes_client/api/.env
编辑~/.hermes_client/api/.env
ALLOWED_DOMAIN=192.168.1.100:18888,100.64.0.1:18888 # LAN + Tailscale
HERMES_STRICT_CORS=1
hermes_client restart
undefinedALLOWED_DOMAIN=192.168.1.100:18888,100.64.0.1:18888 # 局域网 + Tailscale
HERMES_STRICT_CORS=1
hermes_client restart
undefinedSession sync not working
会话同步失败
bash
undefinedbash
undefinedVerify Hermes session directory exists
验证Hermes会话目录是否存在
ls ~/.hermes/profiles/default/sessions/
ls ~/.hermes/profiles/default/sessions/
Check file watcher permissions
检查文件监听器权限
Backend watches ~/.hermes/profiles//sessions/.json
后端监听~/.hermes/profiles//sessions/.json
Ensure readable by user running API server
确保运行API服务的用户拥有读取权限
Force sync by restarting
重启服务强制同步
hermes_client restart
undefinedhermes_client restart
undefinedInteractive terminal (PTY) not connecting
交互式终端(PTY)无法连接
bash
undefinedbash
undefinedVerify WebSocket upgrade works
验证WebSocket升级是否正常
wscat -c "ws://localhost:18889/ws/pty?token=YOUR_JWT_TOKEN"
wscat -c "ws://localhost:18889/ws/pty?token=YOUR_JWT_TOKEN"
Check JWT_SECRET matches between client and server
检查客户端与服务器的JWT_SECRET是否一致
Both use the same secret from api/.env
两者均使用api/.env中的同一个密钥
Windows: Ensure Python is on PATH (PTY bridge requires it)
Windows系统:确保Python在PATH中(PTY桥接需要)
python --version
undefinedpython --version
undefinedWindows install fails
Windows系统安装失败
bash
undefinedbash
undefinedRun PowerShell as Administrator for first npm start
以管理员身份运行PowerShell执行首次npm start
Required for npm link and auto-start setup
这是npm link和自动启动设置的必要条件
Install Visual Studio Build Tools
安装Visual Studio Build Tools
Select "Desktop development with C++"
选择“使用C++的桌面开发”
Install Git for Windows
安装Git for Windows
undefinedundefinedDatabase locked errors
数据库锁定错误
bash
undefinedbash
undefinedStop all services
停止所有服务
hermes_client stop
hermes_client stop
Check for stale processes
检查残留进程
ps aux | grep hermes_client
ps aux | grep hermes_client
Remove lock and restart
删除锁文件并重启
rm ~/.hermes_client/data/hermes.sqlite-wal
hermes_client start
undefinedrm ~/.hermes_client/data/hermes.sqlite-wal
hermes_client start
undefinedUploads not working
上传功能失效
bash
undefinedbash
undefinedCheck upload directory exists and is writable
检查上传目录是否存在且可写
ls -la ~/.hermes_client/uploads/
ls -la ~/.hermes_client/uploads/
Verify HERMES_CLIENT_UPLOADS_DIR in api/.env
验证api/.env中的HERMES_CLIENT_UPLOADS_DIR
Default: ~/.hermes_client/uploads
默认值:~/.hermes_client/uploads
Check disk space
检查磁盘空间
df -h ~/.hermes_client/
undefineddf -h ~/.hermes_client/
undefinedCommon Patterns
常见使用模式
Adding a new agent/profile with model config
添加新agent/配置文件并配置模型
typescript
// 1. Create profile via API
const response = await fetch('/api/agents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'code-reviewer' })
});
// 2. Open PTY terminal for model config
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
}));
};
// User interacts with arrow-key picker in xterm.js
// API key prompts work via PTY bridgetypescript
// 1. 通过API创建配置文件
const response = await fetch('/api/agents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'code-reviewer' })
});
// 2. 打开PTY终端进行模型配置
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
}));
};
// 用户在xterm.js中通过箭头选择器进行交互
// API密钥提示通过PTY桥接正常工作Continuing terminal conversation in web UI
在Web UI中继续终端对话
bash
undefinedbash
undefinedStart in terminal
在终端启动对话
hermes -p myprofile chat
What is TypeScript?
hermes -p myprofile chat
What is TypeScript?
[Hermes responds with session key abc123]
[Hermes返回会话密钥abc123]
Session auto-appears in web UI sidebar within seconds
几秒内会话会自动出现在Web UI侧边栏
Click to continue conversation in browser
点击即可在浏览器中继续对话
Backend detects ~/.hermes/profiles/myprofile/sessions/abc123.json
后端检测到~/.hermes/profiles/myprofile/sessions/abc123.json
undefinedundefinedResuming web conversation in terminal
在终端中恢复Web对话
bash
undefinedbash
undefinedGet session key from web UI URL or conversation list
从Web UI URL或对话列表获取会话密钥
hermes -p myprofile chat -r abc123
Continue our TypeScript discussion
hermes -p myprofile chat -r abc123
Continue our TypeScript discussion
New turns stream back to web UI if chat is open
若Web聊天窗口已打开,新的对话内容会流式同步回Web UI
undefinedundefinedMulti-file context in chat
聊天中使用多文件上下文
typescript
// Upload multiple files
const files = ['src/index.ts', 'package.json', 'README.md'];
const uploads = await Promise.all(
files.map(f => uploadFile(conversationId, new File([...], f), token))
);
// Send message with all file contexts
await sendMessage(
`Review these files for issues:\n${uploads.map(u => u.path).join('\n')}`,
profileName
);
// Backend passes absolute paths to hermes chat commandtypescript
undefined—
上传多个文件
—
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(
,
profileName
);
Review these files for issues:\n${uploads.map(u => u.path).join('\n')}—
后端将绝对路径传递给hermes chat命令
—
undefined