hermes-client-web-ui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Hermes 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
    hermes
    on your
    PATH
  • 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
undefined

Verify 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
npm start
, use the global command from any directory:
bash
undefined
执行
npm start
后,可在任意目录使用全局命令:
bash
undefined

Start/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)
undefined
hermes_client uninstall # 保留数据库 hermes_client uninstall --purge # 删除数据库(需确认)
undefined

Development Mode

开发模式

bash
undefined
bash
undefined

Hot 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
undefined
npm run stop
undefined

Configuration

配置

Port Configuration (
~/.hermes_client/.env
)

端口配置(
~/.hermes_client/.env

Created automatically on first run:
env
API_PORT=18889
CLIENT_PORT=18888
Apply 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配置(
api/.env

Auto-generated from
api/.env.example
:
env
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
    )
api/.env.example
自动生成:
env
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
    不在
    PATH
    中,可指定Hermes二进制文件路径
  • HERMES_STRICT_CORS=1
    :启用严格CORS,配合
    ALLOWED_DOMAIN
    白名单
  • HERMES_SINGLE_USER_MODE
    :将UI锁定为单用户账户页面(取值为
    1/true/yes/on
    0/false/no/off

Architecture

架构

CLI-Driven Streaming

CLI驱动的流式传输

Every chat turn spawns
hermes -p <profile> chat -Q -q "<message>"
and streams stdout over Server-Sent Events:
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();
  });
});
每次对话轮次都会启动
hermes -p <profile> chat -Q -q "<message>"
,并通过Server-Sent Events流式传输标准输出:
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
undefined

Backend runs these commands

后端执行这些命令

hermes profile add <name> hermes profile delete <name> hermes profile list hermes -p <name> model # Via interactive terminal
undefined
hermes profile add <name> hermes profile delete <name> hermes profile list hermes -p <name> model # 通过交互式终端
undefined

Session Sync

会话同步

Sessions started in standalone
hermes
REPL auto-appear in sidebar. Backend watches
~/.hermes/profiles/<profile>/sessions/*.json
:
typescript
// 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
    }
  });
}
在独立
hermes
REPL中启动的会话会自动出现在侧边栏。后端监听
~/.hermes/profiles/<profile>/sessions/*.json
typescript
// 示例会话同步逻辑
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
~/.hermes_client/uploads/<conversationId>/
and passed to Hermes by absolute path:
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_client/uploads/<conversationId>/
,并通过绝对路径传递给Hermes:
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.sqlite
:
sql
-- 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.sqlite
sql
-- 用户表(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
undefined
bash
undefined

Check 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

undefined
undefined

Port conflicts

端口冲突

bash
undefined
bash
undefined

Check 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
undefined
API_PORT=19000 CLIENT_PORT=19001
hermes_client restart
undefined

CORS issues (remote access)

CORS问题(远程访问)

bash
undefined
bash
undefined

Edit ~/.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
undefined
ALLOWED_DOMAIN=192.168.1.100:18888,100.64.0.1:18888 # 局域网 + Tailscale HERMES_STRICT_CORS=1
hermes_client restart
undefined

Session sync not working

会话同步失败

bash
undefined
bash
undefined

Verify 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
undefined
hermes_client restart
undefined

Interactive terminal (PTY) not connecting

交互式终端(PTY)无法连接

bash
undefined
bash
undefined

Verify 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
undefined
python --version
undefined

Windows install fails

Windows系统安装失败

bash
undefined
bash
undefined

Run 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

undefined
undefined

Database locked errors

数据库锁定错误

bash
undefined
bash
undefined

Stop 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
undefined
rm ~/.hermes_client/data/hermes.sqlite-wal hermes_client start
undefined

Uploads not working

上传功能失效

bash
undefined
bash
undefined

Check 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/
undefined
df -h ~/.hermes_client/
undefined

Common 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 bridge
typescript
// 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
undefined
bash
undefined

Start 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

undefined
undefined

Resuming web conversation in terminal

在终端中恢复Web对话

bash
undefined
bash
undefined

Get 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

undefined
undefined

Multi-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 command
typescript
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(
Review these files for issues:\n${uploads.map(u => u.path).join('\n')}
, profileName );

后端将绝对路径传递给hermes chat命令

undefined