ym-hermes-desktop 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # ym-hermes-desktop
2
+
3
+ 本地 Hermes 接入点客户端(Node.js)。通过配置接入 **Hermes Agent 的 API server**(OpenAI 兼容接口),让你的 AI 工具/脚本调用 Hermes——Hermes 会自行执行它注册的工具(包括 MCP 工具)并返回结果。纯本地运行,零额外依赖(Node 22 内置 fetch)。
4
+
5
+ ## 环境要求
6
+
7
+ - Node.js **20+**
8
+ - **Hermes Agent 已安装并启动 API server**(本机已配置好,端口 8642)
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ cd ~/Desktop/ym-hermes-desktop
14
+ npm install
15
+ ```
16
+
17
+ ## 配置
18
+
19
+ 编辑 `.env`(已配好,密钥为本机 Hermes API server 的 Bearer token):
20
+
21
+ ```
22
+ HERMES_API_ENDPOINT=http://127.0.0.1:8642/v1
23
+ HERMES_API_KEY=你的 Bearer token
24
+ HERMES_MODEL=hermes-agent
25
+ ```
26
+
27
+ - `HERMES_API_ENDPOINT`:Hermes API server 地址(默认本地 8642)
28
+ - `HERMES_API_KEY`:Hermes 的 `API_SERVER_KEY`(与 `~/.hermes/.env` 一致)
29
+ - `HERMES_MODEL`:`/v1/models` 返回的模型 id(默认 `hermes-agent`)
30
+
31
+ ## 使用
32
+
33
+ ```bash
34
+ npm run check # 检查 Hermes 接入点是否可用
35
+ npm run models # 查看模型列表
36
+ npm start # 交互模式:直接输入问题,Hermes 执行并回答
37
+ npm run ask "帮我查一下今天杭州天气" # 单次提问
38
+ ```
39
+
40
+ 交互模式内可用 `/models`、`/check`、`/help`、`/exit`。
41
+
42
+ ## 作为模块接入你自己的 AI 工具
43
+
44
+ `src/hermes.js` 可直接 import:
45
+
46
+ ```js
47
+ import { chat, listModels } from './src/hermes.js';
48
+
49
+ // 简单对话
50
+ const r = await chat({ messages: [{ role: 'user', content: '你好' }] });
51
+ console.log(r.choices[0].message.content);
52
+
53
+ // 透传 tools(function calling)给 Hermes 编排
54
+ const r2 = await chat({
55
+ messages: [{ role: 'user', content: '搜索 AI 相关的文章' }],
56
+ tools: [...], // 可选:传给 Hermes 的工具定义
57
+ });
58
+ ```
59
+
60
+ Hermes 内部已注册的 MCP 工具会自动通过 function calling 被调用,无需在本地重复接入。
61
+
62
+ ## 发布到 npm 后(作为包安装使用)
63
+
64
+ ```bash
65
+ npm i -g ym-hermes-desktop # 全局安装
66
+ # 配置环境变量(包内不读取 .env,用系统环境变量)
67
+ export HERMES_API_ENDPOINT=http://127.0.0.1:8642/v1
68
+ export HERMES_API_KEY=你的 Bearer token
69
+ export HERMES_MODEL=hermes-agent
70
+ ym-hermes # 启动交互 CLI
71
+ ```
72
+
73
+ 或作为库:
74
+
75
+ ```js
76
+ import { chat } from 'ym-hermes-desktop';
77
+ const r = await chat({ messages: [{ role: 'user', content: '你好' }] });
78
+ ```
79
+
80
+ > 说明:`ym-hermes-desktop` 的发布包只包含 `src/` 与 `README.md`,不含任何本地配置文件与密钥。
81
+
82
+ ## 作为 MCP server 使用(发布到 npm 的形态)
83
+
84
+ 包内置 stdio MCP server,任何 MCP 客户端(Cursor / Claude Desktop / Cherry Studio 等)可连接:
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "ym-hermes": {
90
+ "command": "npx",
91
+ "args": ["-y", "ym-hermes-desktop", "ym-hermes-mcp"],
92
+ "env": {
93
+ "HERMES_API_ENDPOINT": "http://127.0.0.1:8642/v1",
94
+ "HERMES_API_KEY": "你的 Bearer token",
95
+ "HERMES_MODEL": "hermes-agent"
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ 本地调试:
103
+
104
+ ```bash
105
+ npm run mcp # 以 MCP server 方式启动(stdio)
106
+ ```
107
+
108
+ 暴露的工具:
109
+
110
+ | 工具 | 参数 | 说明 |
111
+ | --- | --- | --- |
112
+ | `hermes_chat` | `question`(或 `messages`) | 让 Hermes 处理任务,Hermes 自动调度其注册的工具/MCP |
113
+
114
+ ## 常见问题
115
+
116
+ - **`✗ Hermes 接入点不可用`**:Hermes API server 未启动。检查 `hermes gateway status`,若 api_server 被拒绝,确认 `~/.hermes/.env` 中 `API_SERVER_KEY` 是强密钥(≥32 字符,可用 `openssl rand -hex 32` 生成),然后 `hermes gateway restart`。
117
+ - **HTTP 401**:`.env` 的 `HERMES_API_KEY` 与 Hermes 实际 `API_SERVER_KEY` 不一致。
118
+ - **想接入更多 MCP 工具**:在 Hermes 里注册即可(`hermes mcp add` 或配置 `~/.hermes/config.yaml` 的 mcp_servers),无需改本工具。
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "ym-hermes-desktop",
3
+ "version": "1.0.0",
4
+ "description": "Hermes Agent 接入点客户端与 MCP server:把 Hermes(含其 MCP 工具)通过 OpenAI 兼容 API / MCP stdio 协议暴露给任意 AI 工具。",
5
+ "type": "module",
6
+ "main": "./src/hermes.js",
7
+ "exports": {
8
+ ".": "./src/hermes.js",
9
+ "./hermes": "./src/hermes.js",
10
+ "./config": "./src/config.js",
11
+ "./server": "./src/mcp-server.js"
12
+ },
13
+ "bin": {
14
+ "ym-hermes": "src/index.js",
15
+ "ym-hermes-mcp": "src/mcp-server.js"
16
+ },
17
+ "files": [
18
+ "src",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "start": "node src/index.js",
23
+ "ask": "node src/index.js --ask",
24
+ "models": "node src/index.js --models",
25
+ "check": "node src/index.js --check",
26
+ "mcp": "node src/mcp-server.js"
27
+ },
28
+ "keywords": [
29
+ "hermes",
30
+ "mcp",
31
+ "model-context-protocol",
32
+ "api-server",
33
+ "openai-compatible",
34
+ "agent",
35
+ "cli"
36
+ ],
37
+ "license": "MIT",
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.12.0"
43
+ }
44
+ }
package/src/config.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * config.js — 配置加载
3
+ * 读取项目根目录 .env(HERMES_API_* 系列)
4
+ */
5
+ import { readFileSync, existsSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+ import path from 'node:path';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ export const ROOT_DIR = path.resolve(__dirname, '..');
11
+ const ENV_PATH = path.join(ROOT_DIR, '.env');
12
+
13
+ /** 简单解析 .env(支持 # 注释、引号、空行) */
14
+ export function loadDotEnv(filePath = ENV_PATH) {
15
+ const result = {};
16
+ if (!existsSync(filePath)) return result;
17
+ const lines = readFileSync(filePath, 'utf-8').split(/\r?\n/);
18
+ for (const raw of lines) {
19
+ const line = raw.trim();
20
+ if (!line || line.startsWith('#')) continue;
21
+ const eq = line.indexOf('=');
22
+ if (eq === -1) continue;
23
+ const key = line.slice(0, eq).trim();
24
+ let value = line.slice(eq + 1).trim();
25
+ if (
26
+ (value.startsWith('"') && value.endsWith('"')) ||
27
+ (value.startsWith("'") && value.endsWith("'"))
28
+ ) {
29
+ value = value.slice(1, -1);
30
+ }
31
+ if (key) result[key] = value;
32
+ }
33
+ return result;
34
+ }
35
+
36
+ /** 读取 Hermes 接入点配置(环境变量优先,.env 仅本地开发兜底) */
37
+ export function loadEnv() {
38
+ const dot = loadDotEnv();
39
+ return {
40
+ endpoint: (process.env.HERMES_API_ENDPOINT || dot.HERMES_API_ENDPOINT || '').replace(/\/+$/, ''),
41
+ apiKey: process.env.HERMES_API_KEY || dot.HERMES_API_KEY || '',
42
+ model: process.env.HERMES_MODEL || dot.HERMES_MODEL || 'hermes-agent',
43
+ };
44
+ }
package/src/hermes.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * hermes.js — Hermes Agent API 客户端
3
+ * 通过 OpenAI 兼容接口(Hermes API server)发起对话,
4
+ * Hermes 会自行执行它已注册的工具(含 MCP 工具),把结果返回。
5
+ * 支持 tools 参数透传(function calling),方便你自己的 AI 工具编排。
6
+ */
7
+ import { loadEnv } from './config.js';
8
+
9
+ const { endpoint, apiKey, model } = loadEnv();
10
+
11
+ /** 发起一次 chat 请求(非流式) */
12
+ export async function chat({ messages, tools = [], toolChoice = undefined, temperature = undefined }) {
13
+ if (!endpoint || !apiKey) {
14
+ throw new Error('未配置 Hermes 接入点:请检查 .env 中的 HERMES_API_ENDPOINT / HERMES_API_KEY');
15
+ }
16
+ const body = { model, messages };
17
+ if (tools && tools.length) body.tools = tools;
18
+ if (toolChoice) body.tool_choice = toolChoice;
19
+ if (temperature !== undefined) body.temperature = temperature;
20
+
21
+ const resp = await fetch(`${endpoint}/chat/completions`, {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ Authorization: `Bearer ${apiKey}`,
26
+ },
27
+ body: JSON.stringify(body),
28
+ });
29
+
30
+ if (!resp.ok) {
31
+ const text = await resp.text();
32
+ throw new Error(`Hermes API 请求失败 (HTTP ${resp.status}): ${text.slice(0, 500)}`);
33
+ }
34
+ return resp.json();
35
+ }
36
+
37
+ /** 查询模型列表 */
38
+ export async function listModels() {
39
+ const resp = await fetch(`${endpoint}/models`, {
40
+ headers: { Authorization: `Bearer ${apiKey}` },
41
+ });
42
+ if (!resp.ok) throw new Error(`Hermes API 请求失败 (HTTP ${resp.status})`);
43
+ const data = await resp.json();
44
+ return (data.data || []).map((m) => m.id);
45
+ }
46
+
47
+ /** 输出 chat 响应中可读的部分(含工具调用过程) */
48
+ export function summarizeChoice(choice) {
49
+ const msg = choice.message || {};
50
+ const lines = [];
51
+ if (Array.isArray(msg.tool_calls)) {
52
+ for (const tc of msg.tool_calls) {
53
+ if (tc.function) {
54
+ lines.push(`[调用工具] ${tc.function.name}(${tc.function.arguments})`);
55
+ }
56
+ }
57
+ }
58
+ if (msg.content) lines.push(String(msg.content));
59
+ if (lines.length === 0) lines.push('(无文本输出)');
60
+ return lines.join('\n');
61
+ }
package/src/index.js ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * index.js — ym-hermes-desktop 入口(Hermes 接入点客户端)
4
+ *
5
+ * 用法:
6
+ * npm start 进入交互模式(输入问题,Hermes 执行并回答)
7
+ * npm run ask "你的问题" 单次提问
8
+ * npm run models 查看 Hermes API 模型列表
9
+ * npm run check 检查 Hermes 接入点是否可用
10
+ *
11
+ * Hermes 会自行执行它注册的工具(包括 MCP 工具),本工具负责转发与展示。
12
+ * 你自己的 AI 工具可直接 import src/hermes.js 使用。
13
+ */
14
+ import readline from 'node:readline';
15
+ import { chat, listModels, summarizeChoice } from './hermes.js';
16
+ import { loadEnv } from './config.js';
17
+
18
+ function printHelp() {
19
+ console.log(`
20
+ ym-hermes-desktop — Hermes 接入点客户端
21
+ Hermes 地址: ${loadEnv().endpoint || '(未配置)'} 模型: ${loadEnv().model}
22
+
23
+ (直接输入问题,回车后由 Hermes 执行并回答)
24
+ /models 查看模型列表
25
+ /check 检查接入点连通性
26
+ /help 显示帮助
27
+ /exit 退出
28
+ `);
29
+ }
30
+
31
+ async function checkHealth() {
32
+ try {
33
+ const models = await listModels();
34
+ console.log(`✓ Hermes 接入点正常,模型: ${models.join(', ')}`);
35
+ } catch (err) {
36
+ console.log(`✗ Hermes 接入点不可用: ${err.message}`);
37
+ console.log(' 请确认 Hermes API server 已启动(hermes gateway status / 8642 端口可访问)');
38
+ }
39
+ }
40
+
41
+ async function askOnce(question) {
42
+ const result = await chat({ messages: [{ role: 'user', content: question }] });
43
+ const choice = result.choices && result.choices[0];
44
+ if (!choice) return console.log('(无返回结果)');
45
+ console.log(summarizeChoice(choice));
46
+ }
47
+
48
+ async function main() {
49
+ const args = process.argv.slice(2);
50
+
51
+ if (args.includes('--models')) {
52
+ try {
53
+ console.log((await listModels()).join('\n'));
54
+ } catch (err) {
55
+ console.log(`✗ ${err.message}`);
56
+ }
57
+ process.exit(0);
58
+ }
59
+ if (args.includes('--check')) {
60
+ await checkHealth();
61
+ process.exit(0);
62
+ }
63
+ // 单次提问模式: node src/index.js --ask "问题"
64
+ const askIdx = args.indexOf('--ask');
65
+ if (askIdx !== -1) {
66
+ const q = args[askIdx + 1];
67
+ if (!q) return console.log('用法: --ask "你的问题"');
68
+ try {
69
+ await askOnce(q);
70
+ } catch (err) {
71
+ console.log(`✗ ${err.message}`);
72
+ }
73
+ process.exit(0);
74
+ }
75
+
76
+ await checkHealth();
77
+ printHelp();
78
+
79
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
80
+ rl.setPrompt('hermes> ');
81
+ rl.prompt();
82
+
83
+ let chain = Promise.resolve();
84
+ rl.on('line', (line) => {
85
+ chain = chain
86
+ .then(async () => {
87
+ const input = line.trim();
88
+ if (!input) return;
89
+ if (input === '/exit' || input === '/quit') {
90
+ rl.close();
91
+ process.exit(0);
92
+ } else if (input === '/help') {
93
+ printHelp();
94
+ } else if (input === '/models') {
95
+ try {
96
+ console.log((await listModels()).join('\n'));
97
+ } catch (err) {
98
+ console.log(`✗ ${err.message}`);
99
+ }
100
+ } else if (input === '/check') {
101
+ await checkHealth();
102
+ } else {
103
+ try {
104
+ await askOnce(input);
105
+ } catch (err) {
106
+ console.log(`✗ ${err.message}`);
107
+ }
108
+ }
109
+ })
110
+ .then(() => rl.prompt());
111
+ });
112
+ }
113
+
114
+ main().catch((err) => {
115
+ console.error('启动失败:', err.message);
116
+ process.exit(1);
117
+ });
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mcp-server.js — 把 Hermes 能力暴露为 MCP server(stdio 传输)
4
+ *
5
+ * 任何 MCP 客户端(Cursor / Claude Desktop / Cherry Studio 等)连接本 server 后,
6
+ * 可通过 hermes_chat 工具让 Hermes 执行任务(Hermes 会自行调度其注册的工具/MCP)。
7
+ *
8
+ * 客户端配置:
9
+ * {
10
+ * "mcpServers": {
11
+ * "ym-hermes": {
12
+ * "command": "npx",
13
+ * "args": ["-y", "ym-hermes-desktop", "ym-hermes-mcp"],
14
+ * "env": {
15
+ * "HERMES_API_ENDPOINT": "http://127.0.0.1:8642/v1",
16
+ * "HERMES_API_KEY": "你的 Bearer token",
17
+ * "HERMES_MODEL": "hermes-agent"
18
+ * }
19
+ * }
20
+ * }
21
+ * }
22
+ */
23
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
24
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
25
+ import {
26
+ ListToolsRequestSchema,
27
+ CallToolRequestSchema,
28
+ } from '@modelcontextprotocol/sdk/types.js';
29
+ import { chat } from './hermes.js';
30
+ import { loadEnv } from './config.js';
31
+
32
+ const server = new Server(
33
+ { name: 'ym-hermes-desktop', version: '1.0.0' },
34
+ { capabilities: { tools: {} } }
35
+ );
36
+
37
+ const TOOLS = [
38
+ {
39
+ name: 'hermes_chat',
40
+ description:
41
+ '向 Hermes Agent 发送对话并获取回复。Hermes 会自动执行它已注册的工具(含 MCP 工具)来完成你的请求。',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ question: {
46
+ type: 'string',
47
+ description: '要 Hermes 处理的问题或任务描述(与 messages 二选一)',
48
+ },
49
+ messages: {
50
+ type: 'array',
51
+ description: '完整对话消息数组,如 [{"role":"user","content":"..."}](与 question 二选一)',
52
+ items: {
53
+ type: 'object',
54
+ properties: {
55
+ role: { type: 'string', enum: ['system', 'user', 'assistant'] },
56
+ content: { type: 'string' },
57
+ },
58
+ },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ ];
64
+
65
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
66
+
67
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
68
+ const { name, arguments: args } = request.params;
69
+ if (name !== 'hermes_chat') {
70
+ throw new Error(`Unknown tool: ${name}`);
71
+ }
72
+ const messages =
73
+ args.messages || (args.question ? [{ role: 'user', content: String(args.question) }] : null);
74
+ if (!messages) {
75
+ throw new Error('缺少参数:请提供 question 或 messages');
76
+ }
77
+ const result = await chat({ messages });
78
+ const content =
79
+ result?.choices?.[0]?.message?.content ?? JSON.stringify(result ?? '');
80
+ return { content: [{ type: 'text', text: String(content) }] };
81
+ });
82
+
83
+ // 启动前给出接入点检查提示(缺失配置时输出到 stderr,不干扰 MCP 协议)
84
+ const { endpoint, apiKey } = loadEnv();
85
+ if (!endpoint || !apiKey) {
86
+ process.stderr.write(
87
+ '[ym-hermes] 警告: 未配置 HERMES_API_ENDPOINT / HERMES_API_KEY,调用 hermes_chat 将失败\n'
88
+ );
89
+ }
90
+
91
+ const transport = new StdioServerTransport();
92
+ await server.connect(transport);