@bulolo/hermes-link 0.2.9 → 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 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
+ [![npm](https://img.shields.io/npm/v/@bulolo/hermes-link?color=cb3837&logo=npm)](https://www.npmjs.com/package/@bulolo/hermes-link)
10
+ [![Node](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
11
+ [![License](https://img.shields.io/badge/License-MIT-blue)](#)
12
+ [![Platform](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)](#)
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