@bulolo/hermes-link 0.3.0 → 0.3.1
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.en.md +630 -0
- package/README.md +37 -12
- package/dist/{chunk-AA2LZ6QZ.js → chunk-ZO2S4ZIO.js} +5 -5
- package/dist/cli/index.js +1 -1
- package/dist/http/app.js +1 -1
- package/package.json +2 -2
package/README.en.md
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# hermes-link
|
|
4
|
+
|
|
5
|
+
**Local access layer for Hermes Agent**
|
|
6
|
+
|
|
7
|
+
Provides full client API, multi-device auth and conversation management for [Hermes Agent](https://github.com/nousresearch/hermes-agent), with LAN and internet connectivity.
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@bulolo/hermes-link)
|
|
10
|
+
[](https://nodejs.org/)
|
|
11
|
+
[](#)
|
|
12
|
+
[](#)
|
|
13
|
+
|
|
14
|
+
[中文](./README.md) | **English**
|
|
15
|
+
|
|
16
|
+
[npm package](https://www.npmjs.com/package/@bulolo/hermes-link)
|
|
17
|
+
|
|
18
|
+
<p>
|
|
19
|
+
<a href="https://github.com/bulolo/HermesLink">
|
|
20
|
+
<img src="https://img.shields.io/badge/⭐_Star-Project-yellow?style=for-the-badge&logo=github" alt="Star Project"/>
|
|
21
|
+
</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
**If this project helps you, please ⭐ Star it — it means a lot to the developer!**
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Overview
|
|
31
|
+
|
|
32
|
+
Hermes Link is a background HTTP service running on your local machine, listening on `http://0.0.0.0:18642` by default. Clients (App / browser) connect directly over LAN or the internet — all conversations, files and commands are processed locally, with no data leaving your machine.
|
|
33
|
+
|
|
34
|
+
All API requests fall into two categories:
|
|
35
|
+
|
|
36
|
+
- **No auth required**: `/pair`, `/api/v1/bootstrap`
|
|
37
|
+
- **Bearer Token required**: all other endpoints require `Authorization: Bearer hlat_xxx`, obtained through the pairing flow
|
|
38
|
+
|
|
39
|
+
## Why HermesLink?
|
|
40
|
+
|
|
41
|
+
Hermes Agent ships with a built-in API Server (port 8642), but it only exposes **12 endpoints**:
|
|
42
|
+
|
|
43
|
+
| Feature | Hermes API Server `:8642` | HermesLink `:18642` |
|
|
44
|
+
|---------|:---:|:---:|
|
|
45
|
+
| Endpoint count | 12 | **97** |
|
|
46
|
+
| Agent execution / event stream | ✓ | ✓ (proxied) |
|
|
47
|
+
| Model list / Cron jobs | ✓ | ✓ (proxied) |
|
|
48
|
+
| Authentication | Single shared key | Per-device token, individually revocable |
|
|
49
|
+
| Device pairing | — | ✓ QR code / multi-device management |
|
|
50
|
+
| Conversation storage | — | ✓ Local history + attachments |
|
|
51
|
+
| Profile & Memory management | — | ✓ Multi-profile, memory, permissions, tool switches |
|
|
52
|
+
| Usage statistics | — | ✓ Token usage by date / model / profile |
|
|
53
|
+
| Tool call approval | — | ✓ Approve / deny flow |
|
|
54
|
+
| Update management / autostart | — | ✓ |
|
|
55
|
+
|
|
56
|
+
### When to use which
|
|
57
|
+
|
|
58
|
+
- **Local scripts / trusted internal services calling Hermes directly** → use Hermes API Server (8642)
|
|
59
|
+
- **Building a mobile app or multi-device access** → HermesLink (18642) required
|
|
60
|
+
- **Need conversation history / Profile management / statistics** → HermesLink (18642) required
|
|
61
|
+
|
|
62
|
+
## How It Works
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Client (browser / App)
|
|
66
|
+
│
|
|
67
|
+
└──→ hermeslink (local machine, port 18642)
|
|
68
|
+
│
|
|
69
|
+
├── auth / device management / conversation storage / Profile & Memory ← handled by HermesLink (~87 endpoints)
|
|
70
|
+
│
|
|
71
|
+
└──→ Hermes Agent API Server (127.0.0.1:8642) ← only runs / models / cron jobs (~10 endpoints)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The vast majority of functionality is handled by HermesLink independently, without relying on the API Server. If the API Server is not running, auth, pairing, conversation queries, and Profile management all continue to work — only Agent execution, model listing, and cron jobs become unavailable. All data is stored locally.
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
### 1. Node.js >= 20.0.0
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node --version # must be >= v20.0.0
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If not installed, use [nvm](https://github.com/nvm-sh/nvm):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
nvm install 20
|
|
88
|
+
nvm use 20
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. Start the Hermes Agent API Server
|
|
92
|
+
|
|
93
|
+
HermesLink communicates with the **Hermes Agent API Server** at `127.0.0.1:8642`.
|
|
94
|
+
|
|
95
|
+
> If the API Server is not running, conversation and Profile features will be unavailable, but auth, pairing, device management, and logs still work fine.
|
|
96
|
+
|
|
97
|
+
Add the following to `~/.hermes/.env` to enable the API Server:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
API_SERVER_ENABLED=true
|
|
101
|
+
API_SERVER_KEY=your-secret-key
|
|
102
|
+
API_SERVER_HOST=0.0.0.0
|
|
103
|
+
API_SERVER_CORS_ORIGINS=*
|
|
104
|
+
GATEWAY_ALLOW_ALL_USERS=true
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Option | Description |
|
|
108
|
+
|--------|-------------|
|
|
109
|
+
| `API_SERVER_ENABLED` | Set to `true` to enable the API Server |
|
|
110
|
+
| `API_SERVER_KEY` | Auth key — replace with a strong random string: `openssl rand -hex 32` |
|
|
111
|
+
| `API_SERVER_HOST` | Listen address — `0.0.0.0` allows all network interfaces |
|
|
112
|
+
| `API_SERVER_CORS_ORIGINS` | CORS origins — can be `*` for local development |
|
|
113
|
+
|
|
114
|
+
Then restart:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
hermes gateway restart
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Verify it's running:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
curl -s http://127.0.0.1:8642/v1/health
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If missing or outdated:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
hermes update
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Installation
|
|
133
|
+
|
|
134
|
+
npm package: [https://www.npmjs.com/package/@bulolo/hermes-link](https://www.npmjs.com/package/@bulolo/hermes-link)
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm install -g @bulolo/hermes-link
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
> If the `hermeslink` command is not found after installation, the npm global bin directory is not in your PATH. Fix it with:
|
|
141
|
+
>
|
|
142
|
+
> ```bash
|
|
143
|
+
> export PATH="$(npm prefix -g)/bin:$PATH"
|
|
144
|
+
> ```
|
|
145
|
+
>
|
|
146
|
+
> Or call it directly: `$(npm prefix -g)/bin/hermeslink`
|
|
147
|
+
|
|
148
|
+
## Quick Start
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# 1. Start the background daemon
|
|
152
|
+
hermeslink start
|
|
153
|
+
|
|
154
|
+
# 2. Open the pairing page in a browser
|
|
155
|
+
hermeslink pair
|
|
156
|
+
|
|
157
|
+
# 3. Check status
|
|
158
|
+
hermeslink status
|
|
159
|
+
|
|
160
|
+
# 4. View logs
|
|
161
|
+
hermeslink logs
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Pairing
|
|
165
|
+
|
|
166
|
+
### Method 1: App QR code scan (standard flow)
|
|
167
|
+
|
|
168
|
+
The QR code contains a JSON payload the app parses to get everything it needs:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"kind": "hermes_link_pairing",
|
|
173
|
+
"version": 1,
|
|
174
|
+
"link_id": "link_xxx",
|
|
175
|
+
"display_name": "Hermes Link",
|
|
176
|
+
"session_id": "ps_xxx",
|
|
177
|
+
"code": "xxx",
|
|
178
|
+
"preferred_urls": ["http://192.168.1.10:18642", "http://127.0.0.1:18642"]
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Once the app has this:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
1. Use preferred_urls[0] as the base URL (LAN IP preferred)
|
|
186
|
+
2. POST {baseUrl}/api/v1/pairing/claim
|
|
187
|
+
Body: { "session_id": "...", "claim_token": "<value of the code field>" }
|
|
188
|
+
3. Response contains access_token and refresh_token
|
|
189
|
+
4. Include Authorization: Bearer <access_token> on all subsequent requests
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Method 2: Browser pairing
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
hermeslink pair
|
|
196
|
+
# Open the Pairing page URL printed in the terminal
|
|
197
|
+
# Click "Pair on this device" — the page shows your access_token and refresh_token
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Method 3: CLI / script (automation-friendly)
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# 1. Get the connect_token
|
|
204
|
+
CONNECT_TOKEN=$(hermeslink pair 2>&1 | grep "Connect token:" | awk '{print $NF}')
|
|
205
|
+
|
|
206
|
+
# 2. Exchange it for an access_token
|
|
207
|
+
curl -s -X POST http://localhost:18642/api/v1/auth/device-session \
|
|
208
|
+
-H "Authorization: Bearer $CONNECT_TOKEN" \
|
|
209
|
+
-H "Content-Type: application/json" \
|
|
210
|
+
-d '{"device_label":"my-script","device_platform":"cli"}'
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
> **Token expiry**: access_token lasts 2 hours, refresh_token lasts 90 days. When the access_token expires, use the refresh_token to get a new one — no need to re-pair.
|
|
216
|
+
|
|
217
|
+
## Commands
|
|
218
|
+
|
|
219
|
+
| Command | Description |
|
|
220
|
+
|---------|-------------|
|
|
221
|
+
| `hermeslink start` | Start the background daemon |
|
|
222
|
+
| `hermeslink stop` | Stop the daemon |
|
|
223
|
+
| `hermeslink restart` | Restart the daemon |
|
|
224
|
+
| `hermeslink status` | Show running status |
|
|
225
|
+
| `hermeslink pair` | Generate pairing URL and QR code |
|
|
226
|
+
| `hermeslink config get` | Show current config |
|
|
227
|
+
| `hermeslink config set <key> <value>` | Update a config value |
|
|
228
|
+
| `hermeslink autostart on` | Enable autostart on login (alias: `enable`) |
|
|
229
|
+
| `hermeslink autostart off` | Disable autostart (alias: `disable`) |
|
|
230
|
+
| `hermeslink logs` | View Link logs |
|
|
231
|
+
| `hermeslink logs --gateway` | View Hermes gateway logs |
|
|
232
|
+
| `hermeslink logs -n 100` | View last 100 log lines |
|
|
233
|
+
| `hermeslink version` | Show version |
|
|
234
|
+
|
|
235
|
+
## Configuration
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
hermeslink config set port 18642 # Change listen port (default: 18642)
|
|
239
|
+
hermeslink config set lan-host 192.168.1.10 # Set LAN IP manually (default: auto-detect)
|
|
240
|
+
hermeslink config set language en # Language: auto / en / zh-CN
|
|
241
|
+
hermeslink config set log-level debug # Log level: debug / info / warn / error
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Config file is at `~/.hermeslink/config.json`.
|
|
245
|
+
|
|
246
|
+
## API Reference
|
|
247
|
+
|
|
248
|
+
Service listens on `http://0.0.0.0:18642` by default. All authenticated endpoints use a Bearer Token with the `hlat_` prefix.
|
|
249
|
+
|
|
250
|
+
### Token Types
|
|
251
|
+
|
|
252
|
+
| Token | Prefix | Expiry | Purpose |
|
|
253
|
+
|-------|--------|--------|---------|
|
|
254
|
+
| Connect Token | none (base64url) | 5 min, one-time | Exchange for access_token |
|
|
255
|
+
| Access Token | `hlat_` | 15 min | All API requests |
|
|
256
|
+
| Refresh Token | `hlrt_` | 90 days | Refresh access_token |
|
|
257
|
+
|
|
258
|
+
### No Auth Required
|
|
259
|
+
|
|
260
|
+
| Method | Path | Description |
|
|
261
|
+
|--------|------|-------------|
|
|
262
|
+
| GET | `/pair` | Pairing web page (open in browser) |
|
|
263
|
+
| GET | `/api/v1/bootstrap` | Service info: link_id, version, capabilities |
|
|
264
|
+
|
|
265
|
+
### Auth / Devices
|
|
266
|
+
|
|
267
|
+
All endpoints below require `Authorization: Bearer hlat_xxx`
|
|
268
|
+
|
|
269
|
+
| Method | Path | Description |
|
|
270
|
+
|--------|------|-------------|
|
|
271
|
+
| GET | `/api/v1/auth/me` | Current token info and device details |
|
|
272
|
+
| POST | `/api/v1/auth/device-session` | Exchange connect_token for access/refresh tokens |
|
|
273
|
+
| POST | `/api/v1/auth/refresh` | Refresh access_token using refresh_token |
|
|
274
|
+
| POST | `/api/v1/auth/logout` | Revoke refresh_token |
|
|
275
|
+
|
|
276
|
+
**POST `/api/v1/auth/device-session`** — Authorization header takes the **connect_token** (not hlat_). Body:
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"device_label": "My Device",
|
|
281
|
+
"device_platform": "ios|android|web|cli",
|
|
282
|
+
"device_model": "optional device model"
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Response:
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"ok": true,
|
|
291
|
+
"device": { "device_id": "dev_xxx", "label": "My Device", "platform": "ios" },
|
|
292
|
+
"access_token": { "token": "hlat_xxx", "expires_at": "2026-05-08T13:00:00Z" },
|
|
293
|
+
"refresh_token": { "token": "hlrt_xxx", "expires_at": "2026-08-06T12:00:00Z" }
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**POST `/api/v1/auth/refresh`** Body:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{ "refresh_token": "hlrt_xxx" }
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Pairing
|
|
304
|
+
|
|
305
|
+
| Method | Path | Description |
|
|
306
|
+
|--------|------|-------------|
|
|
307
|
+
| GET | `/api/v1/pairing/session` | Query pairing session status (includes `claimed` field) |
|
|
308
|
+
| POST | `/api/v1/pairing/claim` | Complete pairing from the app side |
|
|
309
|
+
|
|
310
|
+
**GET `/api/v1/pairing/session`** query: `?session_id=ps_xxx`
|
|
311
|
+
|
|
312
|
+
**POST `/api/v1/pairing/claim`** Body:
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{
|
|
316
|
+
"session_id": "ps_xxx",
|
|
317
|
+
"claim_token": "<connect_token value>",
|
|
318
|
+
"device_label": "My App",
|
|
319
|
+
"device_platform": "ios"
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### System Status
|
|
324
|
+
|
|
325
|
+
| Method | Path | Description |
|
|
326
|
+
|--------|------|-------------|
|
|
327
|
+
| GET | `/api/v1/status` | Overall service status (version, device count, profile count, etc.) |
|
|
328
|
+
| GET | `/api/v1/logs` | Recent logs (`?source=link\|gateway&limit=50`) |
|
|
329
|
+
|
|
330
|
+
### Devices
|
|
331
|
+
|
|
332
|
+
| Method | Path | Description |
|
|
333
|
+
|--------|------|-------------|
|
|
334
|
+
| GET | `/api/v1/devices` | List all paired devices |
|
|
335
|
+
| PATCH | `/api/v1/devices/:deviceId` | Rename device (`{"label":"New Name"}`) |
|
|
336
|
+
| DELETE | `/api/v1/devices/:deviceId` | Revoke device (invalidates its tokens) |
|
|
337
|
+
| DELETE | `/api/v1/devices/:deviceId/app-listing` | Hide a revoked device from the list |
|
|
338
|
+
|
|
339
|
+
### Conversations
|
|
340
|
+
|
|
341
|
+
| Method | Path | Description |
|
|
342
|
+
|--------|------|-------------|
|
|
343
|
+
| GET | `/api/v1/conversations` | List conversations (`?limit=20&cursor=xxx`) |
|
|
344
|
+
| GET | `/api/v1/conversations/search` | Search conversations (`?q=keyword`) |
|
|
345
|
+
| POST | `/api/v1/conversations` | Create a new conversation |
|
|
346
|
+
| DELETE | `/api/v1/conversations` | Bulk delete conversations |
|
|
347
|
+
| DELETE | `/api/v1/conversations/:id` | Delete a single conversation |
|
|
348
|
+
| GET | `/api/v1/conversations/:id/messages` | Get messages in a conversation |
|
|
349
|
+
| POST | `/api/v1/conversations/:id/messages` | Send a message |
|
|
350
|
+
| GET | `/api/v1/conversations/:id/events` | SSE event stream for a conversation |
|
|
351
|
+
| GET | `/api/v1/conversations/events` | SSE stream for all conversations |
|
|
352
|
+
| PATCH | `/api/v1/conversations/:id/title` | Rename conversation (`{"title":"New Title"}`) |
|
|
353
|
+
| PATCH | `/api/v1/conversations/:id/model` | Switch model |
|
|
354
|
+
| PATCH | `/api/v1/conversations/:id/profile` | Switch profile |
|
|
355
|
+
| POST | `/api/v1/conversations/:id/ack` | Acknowledge events read |
|
|
356
|
+
| POST | `/api/v1/conversations/clear-plans` | Create a bulk-clear plan |
|
|
357
|
+
| GET | `/api/v1/conversations/clear-plans/:planId` | Query clear plan status |
|
|
358
|
+
| POST | `/api/v1/conversations/clear-plans/:planId/execute` | Execute clear plan |
|
|
359
|
+
| POST | `/api/v1/conversations/:id/runs/:runId/cancel` | Cancel an in-progress run |
|
|
360
|
+
| POST | `/api/v1/conversations/:id/approvals/:approvalId/approve` | Approve a tool call |
|
|
361
|
+
| POST | `/api/v1/conversations/:id/approvals/:approvalId/deny` | Deny a tool call |
|
|
362
|
+
| POST | `/api/v1/conversations/:id/blobs` | Upload attachment |
|
|
363
|
+
| GET | `/api/v1/conversations/:id/blobs/:blobId` | Download attachment |
|
|
364
|
+
| DELETE | `/api/v1/conversations/:id/blobs/:blobId` | Delete attachment |
|
|
365
|
+
|
|
366
|
+
### Statistics
|
|
367
|
+
|
|
368
|
+
| Method | Path | Description |
|
|
369
|
+
|--------|------|-------------|
|
|
370
|
+
| GET | `/api/v1/statistics` | Global usage stats (conversation count, message count, etc.) |
|
|
371
|
+
| GET | `/api/v1/statistics/usage` | Token usage (`?days=7&from=2026-05-01&to=2026-05-08&model=xxx&profile=xxx`) |
|
|
372
|
+
|
|
373
|
+
### Models
|
|
374
|
+
|
|
375
|
+
| Method | Path | Description |
|
|
376
|
+
|--------|------|-------------|
|
|
377
|
+
| GET | `/api/v1/models` | List available models (from Hermes API Server, OpenAI-compatible format) |
|
|
378
|
+
| GET | `/api/v1/model-configs` | List global model configs |
|
|
379
|
+
| POST | `/api/v1/model-configs` | Add global model config |
|
|
380
|
+
| PATCH | `/api/v1/model-configs/defaults` | Update default model config |
|
|
381
|
+
| DELETE | `/api/v1/model-configs` | Delete global model config |
|
|
382
|
+
|
|
383
|
+
### Profiles
|
|
384
|
+
|
|
385
|
+
| Method | Path | Description |
|
|
386
|
+
|--------|------|-------------|
|
|
387
|
+
| GET | `/api/v1/profiles` | List all profile names |
|
|
388
|
+
| POST | `/api/v1/profiles` | Create new profile (async, returns 202) |
|
|
389
|
+
| PATCH | `/api/v1/profiles/:name` | Rename (`{"name":"new-name"}`) or update metadata |
|
|
390
|
+
| DELETE | `/api/v1/profiles/:name` | Delete profile |
|
|
391
|
+
| GET | `/api/v1/profiles/catalog` | Full catalog (capabilities, permissions, modelConfigs per profile) |
|
|
392
|
+
| GET | `/api/v1/profile-creation/status` | Query creation progress |
|
|
393
|
+
| GET | `/api/v1/profile-creation/events` | Creation progress SSE stream |
|
|
394
|
+
| GET | `/api/v1/profiles/:name/status` | Profile status (existence, API key configuration, etc.) |
|
|
395
|
+
| GET | `/api/v1/profiles/:name/statistics` | Profile conversation statistics |
|
|
396
|
+
| GET | `/api/v1/profiles/:name/skills` | List profile skills (`?include_disabled=true`) |
|
|
397
|
+
| PATCH | `/api/v1/profiles/:name/skills/:skillName` | Enable/disable skill (`{"enabled":true}`) |
|
|
398
|
+
| GET | `/api/v1/profiles/:name/memory` | View memory (USER.md + MEMORY.md) |
|
|
399
|
+
| POST | `/api/v1/profiles/:name/memory/entries` | Add memory entry (`{"target":"memory","content":"..."}`) |
|
|
400
|
+
| PATCH | `/api/v1/profiles/:name/memory/entries` | Replace memory entry (`{"target":"memory","match":"old","content":"new"}`) |
|
|
401
|
+
| DELETE | `/api/v1/profiles/:name/memory/entries` | Delete memory entry (`{"target":"memory","match":"text to match"}`) |
|
|
402
|
+
| DELETE | `/api/v1/profiles/:name/memory` | Reset memory (`{"target":"memory\|user\|all"}`) |
|
|
403
|
+
| PATCH | `/api/v1/profiles/:name/memory/settings` | Update memory provider settings |
|
|
404
|
+
| PATCH | `/api/v1/profiles/:name/memory/provider` | Switch memory provider (`{"provider":"built-in"}`) |
|
|
405
|
+
| GET | `/api/v1/profiles/:name/permissions` | View permissions config |
|
|
406
|
+
| PATCH | `/api/v1/profiles/:name/permissions` | Update permissions config |
|
|
407
|
+
| GET | `/api/v1/profiles/:name/tool-configs/:toolKey` | View tool config (toolKey: `web` / `image_gen` / `stt` / `tts` / `messaging` / `homeassistant` / `rl`) |
|
|
408
|
+
| PATCH | `/api/v1/profiles/:name/tool-configs/:toolKey` | Update tool config |
|
|
409
|
+
| GET | `/api/v1/profiles/:name/model-configs` | List profile model configs |
|
|
410
|
+
| POST | `/api/v1/profiles/:name/model-configs` | Add profile model config |
|
|
411
|
+
| PATCH | `/api/v1/profiles/:name/model-configs/defaults` | Update profile default model |
|
|
412
|
+
| DELETE | `/api/v1/profiles/:name/model-configs` | Delete profile model config |
|
|
413
|
+
|
|
414
|
+
Memory `target` values: `"memory"` (agent notes, MEMORY.md) or `"user"` (user info, USER.md).
|
|
415
|
+
|
|
416
|
+
### Cron Jobs
|
|
417
|
+
|
|
418
|
+
| Method | Path | Description |
|
|
419
|
+
|--------|------|-------------|
|
|
420
|
+
| GET | `/api/v1/cron-jobs` | List all cron jobs across all profiles |
|
|
421
|
+
| GET | `/api/v1/profiles/:name/cron-jobs` | List cron jobs for a specific profile |
|
|
422
|
+
| POST | `/api/v1/profiles/:name/cron-jobs` | Create cron job |
|
|
423
|
+
| GET | `/api/v1/profiles/:name/cron-jobs/:jobId` | Get cron job details |
|
|
424
|
+
| PATCH | `/api/v1/profiles/:name/cron-jobs/:jobId` | Update cron job |
|
|
425
|
+
| DELETE | `/api/v1/profiles/:name/cron-jobs/:jobId` | Delete cron job |
|
|
426
|
+
| POST | `/api/v1/profiles/:name/cron-jobs/:jobId/pause` | Pause cron job |
|
|
427
|
+
| POST | `/api/v1/profiles/:name/cron-jobs/:jobId/resume` | Resume cron job |
|
|
428
|
+
| POST | `/api/v1/profiles/:name/cron-jobs/:jobId/run` | Run cron job immediately |
|
|
429
|
+
|
|
430
|
+
### Runs
|
|
431
|
+
|
|
432
|
+
| Method | Path | Description |
|
|
433
|
+
|--------|------|-------------|
|
|
434
|
+
| POST | `/api/v1/runs` | Submit a run to Hermes Agent (returns 202) |
|
|
435
|
+
| GET | `/api/v1/runs/:runId/events` | Subscribe to run event stream (SSE proxy) |
|
|
436
|
+
| POST | `/api/v1/runs/:runId/cancel` | Cancel a run |
|
|
437
|
+
|
|
438
|
+
**POST `/api/v1/runs`** Body:
|
|
439
|
+
|
|
440
|
+
```json
|
|
441
|
+
{
|
|
442
|
+
"input": "Please organise my ~/Downloads folder",
|
|
443
|
+
"profile": "default",
|
|
444
|
+
"instructions": "optional system instructions",
|
|
445
|
+
"session_id": "optional session ID",
|
|
446
|
+
"conversation_history": []
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Response (202):
|
|
451
|
+
|
|
452
|
+
```json
|
|
453
|
+
{
|
|
454
|
+
"run_id": "run_xxx",
|
|
455
|
+
"fallback": false
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Updates
|
|
460
|
+
|
|
461
|
+
#### Hermes Agent
|
|
462
|
+
|
|
463
|
+
| Method | Path | Description |
|
|
464
|
+
|--------|------|-------------|
|
|
465
|
+
| GET | `/api/v1/hermes/update-check` | Check for a new Hermes Agent version |
|
|
466
|
+
| GET | `/api/v1/hermes/update/status` | Query Hermes update progress |
|
|
467
|
+
| POST | `/api/v1/hermes/update` | Trigger Hermes Agent update |
|
|
468
|
+
| GET | `/api/v1/hermes/update/events` | Update progress SSE stream |
|
|
469
|
+
|
|
470
|
+
#### Link itself
|
|
471
|
+
|
|
472
|
+
| Method | Path | Description |
|
|
473
|
+
|--------|------|-------------|
|
|
474
|
+
| GET | `/api/v1/link/update-check` | Check for a new Link version |
|
|
475
|
+
| GET | `/api/v1/link/update/status` | Query Link update progress |
|
|
476
|
+
| POST | `/api/v1/link/update` | Trigger Link self-update (`{"version":"0.3.0"}`) |
|
|
477
|
+
| GET | `/api/v1/link/update/events` | Update progress SSE stream |
|
|
478
|
+
|
|
479
|
+
### System
|
|
480
|
+
|
|
481
|
+
| Method | Path | Description |
|
|
482
|
+
|--------|------|-------------|
|
|
483
|
+
| GET | `/api/v1/system/status` | System details (version, autostart state, network info) |
|
|
484
|
+
| GET | `/api/v1/system/version` | Link version only |
|
|
485
|
+
| POST | `/api/v1/system/autostart/enable` | Enable autostart on login |
|
|
486
|
+
| POST | `/api/v1/system/autostart/disable` | Disable autostart |
|
|
487
|
+
| GET | `/api/v1/system/logs` | Recent Link logs |
|
|
488
|
+
| GET | `/api/v1/system/logs/gateway` | Recent gateway logs |
|
|
489
|
+
| GET | `/api/v1/system/updates` | Available updates (Hermes + Link combined) |
|
|
490
|
+
| POST | `/api/v1/system/updates/dismiss` | Dismiss current update notification |
|
|
491
|
+
|
|
492
|
+
### Error Response Format
|
|
493
|
+
|
|
494
|
+
All errors return:
|
|
495
|
+
|
|
496
|
+
```json
|
|
497
|
+
{
|
|
498
|
+
"ok": false,
|
|
499
|
+
"error": {
|
|
500
|
+
"code": "error_code",
|
|
501
|
+
"message": "Human readable message"
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Common error codes:
|
|
507
|
+
|
|
508
|
+
| code | HTTP | Description |
|
|
509
|
+
|------|------|-------------|
|
|
510
|
+
| `auth_required` | 401 | No Authorization header |
|
|
511
|
+
| `device_access_token_invalid` | 401 | access_token expired or invalid |
|
|
512
|
+
| `auth_invalid` | 401 | connect_token invalid or already used |
|
|
513
|
+
| `pairing_session_not_found` | 404 | Pairing session not found |
|
|
514
|
+
| `pairing_session_expired` | 404 | Pairing session expired |
|
|
515
|
+
| `pairing_claim_mismatch` | 409 | Pairing token mismatch |
|
|
516
|
+
| `link_not_paired` | 409 | Service has not been assigned a link_id yet |
|
|
517
|
+
|
|
518
|
+
## Examples
|
|
519
|
+
|
|
520
|
+
### Full pairing and usage script
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
#!/bin/bash
|
|
524
|
+
BASE="http://localhost:18642"
|
|
525
|
+
|
|
526
|
+
# Step 1: Generate a connect token
|
|
527
|
+
CONNECT=$(hermeslink pair 2>&1 | grep "Connect token:" | awk '{print $NF}')
|
|
528
|
+
echo "Connect token: $CONNECT"
|
|
529
|
+
|
|
530
|
+
# Step 2: Exchange for access_token and refresh_token
|
|
531
|
+
RESP=$(curl -s -X POST "$BASE/api/v1/auth/device-session" \
|
|
532
|
+
-H "Authorization: Bearer $CONNECT" \
|
|
533
|
+
-H "Content-Type: application/json" \
|
|
534
|
+
-d '{"device_label":"my-script","device_platform":"cli"}')
|
|
535
|
+
|
|
536
|
+
ACCESS=$(echo $RESP | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token']['token'])")
|
|
537
|
+
REFRESH=$(echo $RESP | python3 -c "import json,sys; print(json.load(sys.stdin)['refresh_token']['token'])")
|
|
538
|
+
echo "Access: $ACCESS"
|
|
539
|
+
echo "Refresh: $REFRESH"
|
|
540
|
+
|
|
541
|
+
# Step 3: Check status
|
|
542
|
+
curl -s "$BASE/api/v1/status" -H "Authorization: Bearer $ACCESS" | python3 -m json.tool
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Refresh token
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
curl -s -X POST http://localhost:18642/api/v1/auth/refresh \
|
|
549
|
+
-H "Content-Type: application/json" \
|
|
550
|
+
-d "{\"refresh_token\":\"$REFRESH\"}"
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### List devices
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
curl -s http://localhost:18642/api/v1/devices \
|
|
557
|
+
-H "Authorization: Bearer $ACCESS"
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### List conversations
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
curl -s "http://localhost:18642/api/v1/conversations?limit=10" \
|
|
564
|
+
-H "Authorization: Bearer $ACCESS"
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Autostart
|
|
568
|
+
|
|
569
|
+
- **macOS**: via launchd (`~/Library/LaunchAgents/com.hermes.link.plist`)
|
|
570
|
+
- **Linux**: via systemd user service or XDG autostart
|
|
571
|
+
- **Windows**: via the Startup folder
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
hermeslink autostart on
|
|
575
|
+
hermeslink autostart off
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Runtime Files
|
|
579
|
+
|
|
580
|
+
All files are stored under `~/.hermeslink/`:
|
|
581
|
+
|
|
582
|
+
| Path | Description |
|
|
583
|
+
|------|-------------|
|
|
584
|
+
| `config.json` | User configuration |
|
|
585
|
+
| `identity.json` | Device identity (ed25519 key pair + link_id) |
|
|
586
|
+
| `credentials.json` | Access tokens for paired devices |
|
|
587
|
+
| `app-connect-tokens.json` | Pending pairing tokens (5-minute TTL) |
|
|
588
|
+
| `conversations/` | Conversation data |
|
|
589
|
+
| `blobs/` | File attachments |
|
|
590
|
+
| `pairing/` | Pairing sessions |
|
|
591
|
+
| `link.db` | SQLite database (usage statistics) |
|
|
592
|
+
| `logs/` | Log files |
|
|
593
|
+
|
|
594
|
+
Uninstalling the npm package does not delete this directory — reinstalling reuses the same link_id.
|
|
595
|
+
|
|
596
|
+
## Environment Variables
|
|
597
|
+
|
|
598
|
+
| Variable | Description |
|
|
599
|
+
|----------|-------------|
|
|
600
|
+
| `HERMESLINK_HOME` | Override the runtime directory (default: `~/.hermeslink`) |
|
|
601
|
+
| `HERMESLINK_LOG_LEVEL` | Override log level |
|
|
602
|
+
| `HERMESLINK_LANG` | Override language (`en` / `zh-CN`) |
|
|
603
|
+
| `HERMES_BIN` | Path to the `hermes` binary (default: `hermes`) |
|
|
604
|
+
| `HERMESLINK_LISTEN_HOST` | HTTP listen address (default: `0.0.0.0`) |
|
|
605
|
+
|
|
606
|
+
## Development
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
# Install dependencies and build
|
|
610
|
+
npm install
|
|
611
|
+
npm run build
|
|
612
|
+
|
|
613
|
+
# Run in foreground (for debugging)
|
|
614
|
+
npm run dev:run
|
|
615
|
+
# or
|
|
616
|
+
node dist/cli/index.js daemon --foreground
|
|
617
|
+
|
|
618
|
+
# Watch mode (auto-rebuild on source changes, restart manually)
|
|
619
|
+
npm run dev:watch # terminal 1: watch and rebuild
|
|
620
|
+
node dist/cli/index.js daemon --foreground # terminal 2: run the service
|
|
621
|
+
|
|
622
|
+
# TypeScript type check
|
|
623
|
+
npm run check
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
After starting, visit `http://localhost:18642/api/v1/bootstrap` to verify the service is running.
|
|
627
|
+
|
|
628
|
+
## License
|
|
629
|
+
|
|
630
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# hermes-link
|
|
4
|
+
|
|
5
|
+
**Hermes Agent 本地接入层**
|
|
6
|
+
|
|
7
|
+
为 [Hermes Agent](https://github.com/nousresearch/hermes-agent) 提供完整的客户端 API、多设备鉴权与对话管理,支持局域网 / 公网直连。
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@bulolo/hermes-link)
|
|
10
|
+
[](https://nodejs.org/)
|
|
11
|
+
[](#)
|
|
12
|
+
[](#)
|
|
13
|
+
|
|
14
|
+
简体中文 | [English](./README.en.md)
|
|
15
|
+
|
|
16
|
+
[npm 包](https://www.npmjs.com/package/@bulolo/hermes-link)
|
|
17
|
+
|
|
18
|
+
<p>
|
|
19
|
+
<a href="https://github.com/bulolo/HermesLink">
|
|
20
|
+
<img src="https://img.shields.io/badge/⭐_Star-项目-yellow?style=for-the-badge&logo=github" alt="Star 项目"/>
|
|
21
|
+
</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
**如果这个项目对你有帮助,请点击右上角 ⭐ Star 支持一下,这是对开发者最大的鼓励!**
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
---
|
|
4
29
|
|
|
5
30
|
## 概述
|
|
6
31
|
|
|
@@ -9,7 +34,7 @@ Hermes Link 是一个运行在本机的后台 HTTP 服务,默认监听 `http:/
|
|
|
9
34
|
所有 API 请求分为两类:
|
|
10
35
|
|
|
11
36
|
- **无需鉴权**:`/pair`、`/api/v1/bootstrap`
|
|
12
|
-
- **需要 Bearer Token**:其余接口均需 `Authorization: Bearer
|
|
37
|
+
- **需要 Bearer Token**:其余接口均需 `Authorization: Bearer hlat_xxx`,通过配对流程获取
|
|
13
38
|
|
|
14
39
|
## 为什么需要 HermesLink?
|
|
15
40
|
|
|
@@ -190,7 +215,7 @@ curl -s -X POST http://localhost:18642/api/v1/auth/device-session \
|
|
|
190
215
|
|
|
191
216
|
---
|
|
192
217
|
|
|
193
|
-
> **Token 有效期**:access_token
|
|
218
|
+
> **Token 有效期**:access_token 2 小时,refresh_token 90 天。access_token 过期后用 refresh_token 换新,无需重新配对。
|
|
194
219
|
|
|
195
220
|
## 命令一览
|
|
196
221
|
|
|
@@ -223,15 +248,15 @@ hermeslink config set log-level debug # 日志级别:debug / info / wa
|
|
|
223
248
|
|
|
224
249
|
## API 参考
|
|
225
250
|
|
|
226
|
-
服务默认监听 `http://0.0.0.0:18642`。所有需要鉴权的接口均使用 Bearer Token(`
|
|
251
|
+
服务默认监听 `http://0.0.0.0:18642`。所有需要鉴权的接口均使用 Bearer Token(`hlat_` 前缀)。
|
|
227
252
|
|
|
228
253
|
### Token 说明
|
|
229
254
|
|
|
230
255
|
| Token | 前缀 | 有效期 | 用途 |
|
|
231
256
|
|-------|------|--------|------|
|
|
232
257
|
| Connect Token | 无前缀(base64url)| 5 分钟,一次性 | 兑换 access_token |
|
|
233
|
-
| Access Token | `
|
|
234
|
-
| Refresh Token | `
|
|
258
|
+
| Access Token | `hlat_` | 2 小时 | 所有 API 请求 |
|
|
259
|
+
| Refresh Token | `hlrt_` | 90 天 | 刷新 access_token |
|
|
235
260
|
|
|
236
261
|
### 无需鉴权
|
|
237
262
|
|
|
@@ -242,7 +267,7 @@ hermeslink config set log-level debug # 日志级别:debug / info / wa
|
|
|
242
267
|
|
|
243
268
|
### 认证 / 设备
|
|
244
269
|
|
|
245
|
-
所有以下接口需要 Header:`Authorization: Bearer
|
|
270
|
+
所有以下接口需要 Header:`Authorization: Bearer hlat_xxx`
|
|
246
271
|
|
|
247
272
|
| 方法 | 路径 | 说明 |
|
|
248
273
|
|------|------|------|
|
|
@@ -251,7 +276,7 @@ hermeslink config set log-level debug # 日志级别:debug / info / wa
|
|
|
251
276
|
| POST | `/api/v1/auth/refresh` | 用 refresh_token 换新 access_token |
|
|
252
277
|
| POST | `/api/v1/auth/logout` | 撤销 refresh_token |
|
|
253
278
|
|
|
254
|
-
**POST `/api/v1/auth/device-session`** 的 Authorization 头需放 **connect_token**(不是
|
|
279
|
+
**POST `/api/v1/auth/device-session`** 的 Authorization 头需放 **connect_token**(不是 hlat_),Body:
|
|
255
280
|
|
|
256
281
|
```json
|
|
257
282
|
{
|
|
@@ -267,15 +292,15 @@ hermeslink config set log-level debug # 日志级别:debug / info / wa
|
|
|
267
292
|
{
|
|
268
293
|
"ok": true,
|
|
269
294
|
"device": { "device_id": "dev_xxx", "label": "我的设备", "platform": "ios" },
|
|
270
|
-
"access_token": { "token": "
|
|
271
|
-
"refresh_token": { "token": "
|
|
295
|
+
"access_token": { "token": "hlat_xxx", "expires_at": "2026-05-08T13:00:00Z" },
|
|
296
|
+
"refresh_token": { "token": "hlrt_xxx", "expires_at": "2026-08-06T12:00:00Z" }
|
|
272
297
|
}
|
|
273
298
|
```
|
|
274
299
|
|
|
275
300
|
**POST `/api/v1/auth/refresh`** Body:
|
|
276
301
|
|
|
277
302
|
```json
|
|
278
|
-
{ "refresh_token": "
|
|
303
|
+
{ "refresh_token": "hlrt_xxx" }
|
|
279
304
|
```
|
|
280
305
|
|
|
281
306
|
### 配对
|
|
@@ -780,7 +780,7 @@ async function dismissUpdate(paths) {
|
|
|
780
780
|
|
|
781
781
|
// src/security/credentials.ts
|
|
782
782
|
import { randomBytes, randomUUID as randomUUID2, timingSafeEqual, createHash } from "crypto";
|
|
783
|
-
var ACCESS_TOKEN_TTL_MS =
|
|
783
|
+
var ACCESS_TOKEN_TTL_MS = 2 * 60 * 60 * 1e3;
|
|
784
784
|
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
785
785
|
var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
786
786
|
function credentialsPath(paths) {
|
|
@@ -841,8 +841,8 @@ function formatDeviceListItem(device) {
|
|
|
841
841
|
};
|
|
842
842
|
}
|
|
843
843
|
async function rotateDeviceSession(store, device, now, paths) {
|
|
844
|
-
const accessToken = randomToken("
|
|
845
|
-
const refreshToken = randomToken("
|
|
844
|
+
const accessToken = randomToken("hlat_");
|
|
845
|
+
const refreshToken = randomToken("hlrt_");
|
|
846
846
|
device.access_token_hash = sha256(accessToken);
|
|
847
847
|
device.access_expires_at = new Date(now.getTime() + ACCESS_TOKEN_TTL_MS).toISOString();
|
|
848
848
|
device.refresh_token_hash = sha256(refreshToken);
|
|
@@ -1106,7 +1106,7 @@ async function authenticateRequest(ctx, paths = resolveRuntimePaths()) {
|
|
|
1106
1106
|
if (device) {
|
|
1107
1107
|
return { kind: "device", device };
|
|
1108
1108
|
}
|
|
1109
|
-
if (token.startsWith("
|
|
1109
|
+
if (token.startsWith("hlat_")) {
|
|
1110
1110
|
throw new LinkHttpError(401, "device_access_token_invalid", "Device access token is invalid or expired");
|
|
1111
1111
|
}
|
|
1112
1112
|
const localToken = await consumeAppConnectToken(token, paths);
|
|
@@ -8734,7 +8734,7 @@ async function buildPairingPage(options) {
|
|
|
8734
8734
|
showStatus('success', '\u914D\u5BF9\u6210\u529F\uFF01');
|
|
8735
8735
|
const results = document.getElementById('results');
|
|
8736
8736
|
results.innerHTML = \`
|
|
8737
|
-
<div class="result-row"><span class="tag">access_token \xB7
|
|
8737
|
+
<div class="result-row"><span class="tag">access_token \xB7 2h</span><div class="mono" onclick="copyText(this)">\${data.access_token.token}</div></div>
|
|
8738
8738
|
<div class="result-row" style="margin-top:0.5rem"><span class="tag">refresh_token \xB7 90days</span><div class="mono" onclick="copyText(this)">\${data.refresh_token.token}</div></div>
|
|
8739
8739
|
\`;
|
|
8740
8740
|
} else {
|
package/dist/cli/index.js
CHANGED
package/dist/http/app.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bulolo/hermes-link",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Local companion service and CLI for Hermes Agent, enabling mobile and LAN access",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -74,4 +74,4 @@
|
|
|
74
74
|
"typescript": "^5.7.2",
|
|
75
75
|
"vitest": "^2.1.8"
|
|
76
76
|
}
|
|
77
|
-
}
|
|
77
|
+
}
|