@bulolo/hermes-link 0.2.0 → 0.2.2

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 CHANGED
@@ -1,103 +1,120 @@
1
1
  # @bulolo/hermes-link
2
2
 
3
- Local companion service and CLI for connecting [Hermes Agent](https://github.com/nousresearch/hermes-agent) to your devices through zhiji.
3
+ 本地伴随服务,为 [Hermes Agent](https://github.com/nousresearch/hermes-agent) 提供移动端接入能力,支持局域网直连。
4
4
 
5
- ## Requirements
5
+ ## 环境要求
6
6
 
7
7
  - Node.js >= 20
8
- - A running [Hermes Agent](https://github.com/nousresearch/hermes-agent) installation
8
+ - 已安装并运行的 [Hermes Agent](https://github.com/nousresearch/hermes-agent)
9
9
 
10
- ## Installation
10
+ ## 安装
11
11
 
12
12
  ```bash
13
- npm install -g @bulolo/hermes-link
13
+ npm install -g @bulolo/hermes-link --registry https://registry.npmjs.org
14
14
  ```
15
15
 
16
- ## Quick Start
16
+ ## 快速开始
17
17
 
18
18
  ```bash
19
- # Start the background daemon
19
+ # 启动后台服务
20
20
  hermeslink start
21
21
 
22
- # Check status
22
+ # 生成配对二维码(手机扫码连接)
23
+ hermeslink pair
24
+
25
+ # 查看状态
23
26
  hermeslink status
24
27
 
25
- # View logs
28
+ # 查看日志
26
29
  hermeslink logs
27
30
  ```
28
31
 
29
- ## Commands
30
-
31
- | Command | Description |
32
- |---------|-------------|
33
- | `hermeslink start` | Start the background daemon |
34
- | `hermeslink stop` | Stop the daemon |
35
- | `hermeslink restart` | Restart the daemon |
36
- | `hermeslink status` | Show daemon and service status |
37
- | `hermeslink pair` | Generate a pairing URL for your device |
38
- | `hermeslink config get` | Show current configuration |
39
- | `hermeslink config set <key> <value>` | Set a configuration value |
40
- | `hermeslink autostart` | Show autostart status |
41
- | `hermeslink autostart enable` | Enable autostart on login |
42
- | `hermeslink autostart disable` | Disable autostart |
43
- | `hermeslink logs` | Show recent log entries |
44
- | `hermeslink logs --gateway` | Show Hermes gateway logs |
45
- | `hermeslink version` | Print version |
46
-
47
- ## Configuration
32
+ ## 配对流程
33
+
34
+ ```
35
+ 1. 运行 hermeslink pair
36
+
37
+
38
+ 终端显示二维码
39
+
40
+
41
+ 手机 App 扫码
42
+
43
+
44
+ 直连本地服务 (局域网 / 127.0.0.1)
45
+ ```
46
+
47
+ 配对完成后,手机 App 获得访问令牌,后续请求直接连接本地服务,无需经过外部服务器。
48
+
49
+ ## 命令一览
50
+
51
+ | 命令 | 说明 |
52
+ |------|------|
53
+ | `hermeslink start` | 启动后台守护进程 |
54
+ | `hermeslink stop` | 停止守护进程 |
55
+ | `hermeslink restart` | 重启守护进程 |
56
+ | `hermeslink status` | 查看运行状态 |
57
+ | `hermeslink pair` | 生成配对二维码 |
58
+ | `hermeslink config get` | 查看当前配置 |
59
+ | `hermeslink config set <key> <value>` | 修改配置 |
60
+ | `hermeslink autostart enable` | 开机自启 |
61
+ | `hermeslink autostart disable` | 关闭自启 |
62
+ | `hermeslink logs` | 查看日志 |
63
+ | `hermeslink logs --gateway` | 查看 Hermes 网关日志 |
64
+ | `hermeslink version` | 查看版本 |
65
+
66
+ ## 配置
48
67
 
49
68
  ```bash
50
- hermeslink config set port 52379 # Change listen port
51
- hermeslink config set lan-host 192.168.1.10 # Set LAN IP manually
52
- hermeslink config set language zh-CN # Set language (auto/en/zh-CN)
53
- hermeslink config set log-level debug # Set log level (debug/info/warn/error)
69
+ hermeslink config set port 52379 # 修改监听端口
70
+ hermeslink config set lan-host 192.168.1.10 # 手动指定局域网 IP
71
+ hermeslink config set language zh-CN # 语言 (auto/en/zh-CN)
72
+ hermeslink config set log-level debug # 日志级别 (debug/info/warn/error)
54
73
  ```
55
74
 
56
- Config is stored at `~/.hermeslink/config.json`.
75
+ 配置文件位于 `~/.hermeslink/config.json`。
57
76
 
58
- ## How It Works
77
+ ## 工作原理
59
78
 
60
79
  ```
61
- Hermes App (mobile)
62
-
63
-
64
- hermes-relay.catwiki.ai ──────────────────┐
65
- WebSocket tunnel
66
- hermeslink (this service)
67
-
68
-
69
- Hermes Agent (local)
70
- localhost:8642
80
+ 手机 App
81
+
82
+ └──→ hermeslink (本地, 端口 52379)
83
+
84
+ └──→ Hermes Agent (localhost:8642)
71
85
  ```
72
86
 
73
- `hermeslink` runs a local HTTP server (default port `52379`) and maintains a persistent WebSocket connection to the relay server. The mobile app connects through the relay and proxies requests to your local Hermes Agent.
87
+ `hermeslink` 在本地运行一个 HTTP 服务,手机 App 通过局域网直接访问。对话、文件、指令均在本地处理,数据不经过外部服务器。
74
88
 
75
- ## Runtime Files
89
+ ## 运行时文件
76
90
 
77
- All runtime files are stored in `~/.hermeslink/`:
91
+ 所有文件存储于 `~/.hermeslink/`:
78
92
 
79
- | Path | Description |
80
- |------|-------------|
81
- | `config.json` | User configuration |
82
- | `identity.json` | Device identity (ed25519 keypair) |
83
- | `link.db` | SQLite database (stats, usage) |
84
- | `logs/` | Log files |
85
- | `daemon.pid` | Daemon PID file |
93
+ | 路径 | 说明 |
94
+ |------|------|
95
+ | `config.json` | 用户配置 |
96
+ | `identity.json` | 设备身份(ed25519 密钥对)|
97
+ | `credentials.json` | 已配对设备的访问令牌 |
98
+ | `conversations/` | 对话数据 |
99
+ | `blobs/` | 文件附件 |
100
+ | `pairing/` | 配对会话 |
101
+ | `link.db` | SQLite 数据库(统计信息)|
102
+ | `logs/` | 日志文件 |
86
103
 
87
- ## Environment Variables
104
+ ## 环境变量
88
105
 
89
- | Variable | Description |
90
- |----------|-------------|
91
- | `HERMESLINK_HOME` | Override runtime directory (default `~/.hermeslink`) |
92
- | `HERMESLINK_LOG_LEVEL` | Override log level |
93
- | `HERMESLINK_LANG` | Override language |
94
- | `HERMES_BIN` | Path to `hermes` binary (default: `hermes`) |
106
+ | 变量 | 说明 |
107
+ |------|------|
108
+ | `HERMESLINK_HOME` | 覆盖运行时目录(默认 `~/.hermeslink`)|
109
+ | `HERMESLINK_LOG_LEVEL` | 覆盖日志级别 |
110
+ | `HERMESLINK_LANG` | 覆盖语言 |
111
+ | `HERMES_BIN` | `hermes` 二进制路径(默认 `hermes`)|
95
112
 
96
- ## Autostart
113
+ ## 开机自启
97
114
 
98
- On macOS, autostart is managed via launchd (`~/Library/LaunchAgents/com.hermes.link.plist`).
99
- On Linux, via systemd user service or XDG autostart.
100
- On Windows, via the Startup folder.
115
+ - **macOS**:通过 launchd(`~/Library/LaunchAgents/com.hermes.link.plist`)
116
+ - **Linux**:通过 systemd 用户服务或 XDG autostart
117
+ - **Windows**:通过 Startup 文件夹
101
118
 
102
119
  ```bash
103
120
  hermeslink autostart enable
@@ -12,8 +12,11 @@ import path2 from "path";
12
12
  import pino from "pino";
13
13
 
14
14
  // src/constants.ts
15
+ import { createRequire } from "module";
16
+ var _require = createRequire(import.meta.url);
17
+ var _pkg = _require("../package.json");
15
18
  var LINK_COMMAND = "hermeslink";
16
- var LINK_VERSION = "0.1.0";
19
+ var LINK_VERSION = _pkg.version;
17
20
  var LINK_DEFAULT_PORT = 52379;
18
21
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
19
22
  var DEFAULT_LOG_FILE = "hermeslink.log";
@@ -797,9 +800,6 @@ async function dismissUpdate(paths) {
797
800
  await updateLinkState({ updateDismissedAt: (/* @__PURE__ */ new Date()).toISOString() }, runtimePaths);
798
801
  }
799
802
 
800
- // src/http/auth.ts
801
- import { createPublicKey, verify } from "crypto";
802
-
803
803
  // src/security/credentials.ts
804
804
  import { randomBytes, randomUUID as randomUUID2, timingSafeEqual, createHash } from "crypto";
805
805
  var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
@@ -1072,133 +1072,50 @@ async function renameDeviceById(deviceId, label, paths = resolveRuntimePaths())
1072
1072
  return formatDeviceListItem(device);
1073
1073
  }
1074
1074
 
1075
- // src/identity/identity.ts
1076
- import { generateKeyPairSync, randomUUID as randomUUID3, sign } from "crypto";
1077
- import { chmod as chmod2, mkdir as mkdir4 } from "fs/promises";
1078
- import { z } from "zod";
1079
- var linkIdentitySchema = z.object({
1080
- install_id: z.string().min(1),
1081
- link_id: z.string().min(1).nullable().optional(),
1082
- public_key_pem: z.string().min(1),
1083
- private_key_pem: z.string().min(1),
1084
- created_at: z.string().min(1),
1085
- updated_at: z.string().min(1)
1086
- });
1087
- async function loadIdentity(paths = resolveRuntimePaths()) {
1088
- const value = await readJsonFile(paths.identityFile);
1089
- if (value === null) {
1090
- return null;
1091
- }
1092
- return linkIdentitySchema.parse(value);
1093
- }
1094
- async function ensureIdentity(paths = resolveRuntimePaths()) {
1095
- const existing = await loadIdentity(paths);
1096
- if (existing) {
1097
- return existing;
1098
- }
1099
- await mkdir4(paths.homeDir, { recursive: true, mode: 448 });
1100
- await chmod2(paths.homeDir, 448).catch(() => void 0);
1101
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
1102
- const now = (/* @__PURE__ */ new Date()).toISOString();
1103
- const identity = {
1104
- install_id: `install_${randomUUID3().replaceAll("-", "")}`,
1105
- link_id: null,
1106
- public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
1107
- private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
1108
- created_at: now,
1109
- updated_at: now
1110
- };
1111
- await writeJsonFile(paths.identityFile, identity);
1112
- return identity;
1075
+ // src/security/app-connect-token.ts
1076
+ import crypto from "crypto";
1077
+ import path6 from "path";
1078
+ var TOKENS_FILE = "app-connect-tokens.json";
1079
+ var TOKEN_EXPIRY_MS = 5 * 60 * 1e3;
1080
+ function tokensFilePath(paths) {
1081
+ return path6.join(paths.homeDir, TOKENS_FILE);
1082
+ }
1083
+ async function readTokens(paths) {
1084
+ const raw = await readJsonFile(tokensFilePath(paths));
1085
+ if (!Array.isArray(raw)) return [];
1086
+ const now = /* @__PURE__ */ new Date();
1087
+ return raw.filter(isValidToken).filter((t) => new Date(t.expiresAt) > now);
1113
1088
  }
1114
- async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
1115
- const identity = await ensureIdentity(paths);
1116
- const next = {
1117
- ...identity,
1118
- link_id: linkId,
1119
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
1120
- };
1121
- await writeJsonFile(paths.identityFile, next);
1122
- return next;
1089
+ function isValidToken(value) {
1090
+ if (!value || typeof value !== "object") return false;
1091
+ const t = value;
1092
+ return typeof t.token === "string" && typeof t.createdAt === "string" && typeof t.expiresAt === "string";
1123
1093
  }
1124
- function signRelayNonce(identity, nonce) {
1125
- return signIdentityPayload(identity, nonce);
1094
+ async function saveTokens(tokens, paths) {
1095
+ await writeJsonFile(tokensFilePath(paths), tokens);
1126
1096
  }
1127
- function signIdentityPayload(identity, payload) {
1128
- const signature = sign(null, Buffer.from(payload, "utf8"), identity.private_key_pem);
1129
- return signature.toString("base64url");
1130
- }
1131
-
1132
- // src/config/config.ts
1133
- var defaultLinkConfig = {
1134
- port: LINK_DEFAULT_PORT,
1135
- lanHost: null,
1136
- serverBaseUrl: "https://hermes-server.catwiki.ai",
1137
- relayBaseUrl: "https://hermes-relay.catwiki.ai",
1138
- appConnectTokenIssuer: "https://hermes-server.catwiki.ai",
1139
- appConnectTokenAudience: "hermes-link",
1140
- language: "auto",
1141
- logLevel: "warn"
1142
- };
1143
- async function loadConfig(paths = resolveRuntimePaths()) {
1144
- const existing = await readJsonFile(paths.configFile);
1145
- const language = normalizeConfiguredLanguage(existing?.language);
1146
- const lanHost = normalizeLanHost(existing?.lanHost);
1147
- const logLevel = normalizeLogLevel(existing?.logLevel ?? process.env.HERMESLINK_LOG_LEVEL);
1148
- return {
1149
- ...defaultLinkConfig,
1150
- ...existing ?? {},
1151
- language,
1152
- lanHost,
1153
- logLevel
1154
- };
1155
- }
1156
- async function saveConfig(patch, paths = resolveRuntimePaths()) {
1157
- const current = await loadConfig(paths);
1158
- const next = {
1159
- ...current,
1160
- ...patch,
1161
- logLevel: patch.logLevel === void 0 ? current.logLevel : normalizeLogLevel(patch.logLevel)
1097
+ async function generateAppConnectToken(paths) {
1098
+ const runtimePaths = paths ?? resolveRuntimePaths();
1099
+ const now = /* @__PURE__ */ new Date();
1100
+ const token = {
1101
+ token: crypto.randomBytes(32).toString("base64url"),
1102
+ createdAt: now.toISOString(),
1103
+ usedAt: null,
1104
+ expiresAt: new Date(now.getTime() + TOKEN_EXPIRY_MS).toISOString()
1162
1105
  };
1163
- await writeJsonFile(paths.configFile, next);
1164
- return next;
1165
- }
1166
- function normalizeConfiguredLanguage(language) {
1167
- if (language === "zh-CN" || language === "en" || language === "auto") {
1168
- return language;
1169
- }
1170
- return defaultLinkConfig.language;
1171
- }
1172
- function normalizeLogLevel(level) {
1173
- if (level === "debug" || level === "info" || level === "warn" || level === "error") {
1174
- return level;
1175
- }
1176
- return defaultLinkConfig.logLevel;
1177
- }
1178
- function normalizeLanHost(value) {
1179
- if (value === null || value === void 0) {
1180
- return null;
1181
- }
1182
- if (typeof value !== "string") {
1183
- return null;
1184
- }
1185
- const host = value.trim().replace(/^\[/u, "").replace(/\]$/u, "");
1186
- if (!host) {
1187
- return null;
1188
- }
1189
- if (!isUsableLanIpv4(host)) {
1190
- return null;
1191
- }
1192
- return host;
1106
+ const tokens = await readTokens(runtimePaths);
1107
+ tokens.push(token);
1108
+ await saveTokens(tokens, runtimePaths);
1109
+ return token;
1193
1110
  }
1194
- function isUsableLanIpv4(value) {
1195
- const parts = value.split(".").map((part) => Number.parseInt(part, 10));
1196
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
1197
- return false;
1198
- }
1199
- const [first, second, , fourth] = parts;
1200
- const privateRange = first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
1201
- return privateRange && fourth !== 0 && fourth !== 255;
1111
+ async function consumeAppConnectToken(tokenValue, paths) {
1112
+ const runtimePaths = paths ?? resolveRuntimePaths();
1113
+ const tokens = await readTokens(runtimePaths);
1114
+ const index = tokens.findIndex((t) => t.token === tokenValue && !t.usedAt);
1115
+ if (index === -1) return null;
1116
+ tokens[index].usedAt = (/* @__PURE__ */ new Date()).toISOString();
1117
+ await saveTokens(tokens, runtimePaths);
1118
+ return tokens[index];
1202
1119
  }
1203
1120
 
1204
1121
  // src/core/errors.ts
@@ -1213,7 +1130,6 @@ var LinkHttpError = class extends Error {
1213
1130
  };
1214
1131
 
1215
1132
  // src/http/auth.ts
1216
- var cachedJwks = null;
1217
1133
  async function authenticateRequest(ctx, paths = resolveRuntimePaths()) {
1218
1134
  const token = readBearerToken(ctx.get("authorization"));
1219
1135
  if (!token) {
@@ -1226,21 +1142,11 @@ async function authenticateRequest(ctx, paths = resolveRuntimePaths()) {
1226
1142
  if (token.startsWith("hpat_")) {
1227
1143
  throw new LinkHttpError(401, "device_access_token_invalid", "Device access token is invalid or expired");
1228
1144
  }
1229
- const [identity, config] = await Promise.all([loadRequiredIdentity(paths), loadConfig(paths)]);
1230
- const claims = await verifyAppConnectToken(token, { config, linkId: identity.link_id ?? null });
1231
- return {
1232
- kind: "app-connect",
1233
- accountId: typeof claims.sub === "string" ? claims.sub : null,
1234
- scopes: normalizeScopes(claims.scope),
1235
- appInstanceId: normalizeAppInstanceId2(claims.app_instance_id)
1236
- };
1237
- }
1238
- async function loadRequiredIdentity(paths) {
1239
- const identity = await loadIdentity(paths);
1240
- if (!identity?.link_id) {
1241
- throw new LinkHttpError(409, "link_not_paired", "Hermes Link is not paired");
1145
+ const localToken = await consumeAppConnectToken(token, paths);
1146
+ if (localToken) {
1147
+ return { kind: "app-connect", accountId: null, scopes: [], appInstanceId: null };
1242
1148
  }
1243
- return identity;
1149
+ throw new LinkHttpError(401, "auth_invalid", "Token is invalid or expired");
1244
1150
  }
1245
1151
  function readBearerToken(value) {
1246
1152
  const trimmed = value.trim();
@@ -1248,67 +1154,6 @@ function readBearerToken(value) {
1248
1154
  const token = trimmed.slice(7).trim();
1249
1155
  return token || null;
1250
1156
  }
1251
- function normalizeScopes(value) {
1252
- if (Array.isArray(value)) return value.filter((i) => typeof i === "string").map((i) => i.trim()).filter(Boolean);
1253
- if (typeof value === "string") return value.split(/\s+/u).map((i) => i.trim()).filter(Boolean);
1254
- return [];
1255
- }
1256
- function normalizeAppInstanceId2(value) {
1257
- if (typeof value !== "string") return null;
1258
- const trimmed = value.trim();
1259
- return /^appi_[A-Za-z0-9_-]{16,96}$/u.test(trimmed) ? trimmed : null;
1260
- }
1261
- async function verifyAppConnectToken(token, options) {
1262
- const segments = token.split(".");
1263
- if (segments.length !== 3) {
1264
- throw new LinkHttpError(401, "app_connect_token_invalid", "App connect token is malformed");
1265
- }
1266
- const [encodedHeader, encodedPayload, encodedSignature] = segments;
1267
- const header = decodeJwtPart(encodedHeader);
1268
- const payload = decodeJwtPart(encodedPayload);
1269
- if (header.alg !== "ES256" || header.typ !== "JWT") {
1270
- throw new LinkHttpError(401, "app_connect_token_invalid", "App connect token algorithm is unsupported");
1271
- }
1272
- if (payload.token_type !== "hermes_app_connect" || payload.iss !== options.config.appConnectTokenIssuer || payload.aud !== options.config.appConnectTokenAudience || payload.link_id !== options.linkId || !Number.isFinite(payload.exp) || payload.exp <= Math.floor(Date.now() / 1e3)) {
1273
- throw new LinkHttpError(401, "app_connect_token_invalid", "App connect token claims are invalid");
1274
- }
1275
- const jwks = await getJwks(options.config.serverBaseUrl);
1276
- const key = jwks.find((k) => k.kid === header.kid) ?? jwks[0];
1277
- if (!key) {
1278
- throw new LinkHttpError(503, "app_connect_jwks_unavailable", "App connect token key is unavailable");
1279
- }
1280
- const publicKey = createPublicKey({ key, format: "jwk" });
1281
- const ok = verify(
1282
- "sha256",
1283
- Buffer.from(`${encodedHeader}.${encodedPayload}`),
1284
- { key: publicKey, dsaEncoding: "ieee-p1363" },
1285
- Buffer.from(base64UrlToBase64(encodedSignature), "base64")
1286
- );
1287
- if (!ok) {
1288
- throw new LinkHttpError(401, "app_connect_token_invalid", "App connect token signature is invalid");
1289
- }
1290
- return payload;
1291
- }
1292
- async function getJwks(serverBaseUrl) {
1293
- if (cachedJwks && cachedJwks.expiresAt > Date.now()) return cachedJwks.keys;
1294
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}/api/v1/app-connect/jwks.json`, {
1295
- headers: { accept: "application/json" }
1296
- });
1297
- if (!response.ok) {
1298
- throw new LinkHttpError(503, "app_connect_jwks_unavailable", "Unable to load app connect JWKS");
1299
- }
1300
- const payload = await response.json();
1301
- const keys = Array.isArray(payload.keys) ? payload.keys : [];
1302
- cachedJwks = { keys, expiresAt: Date.now() + 5 * 60 * 1e3 };
1303
- return keys;
1304
- }
1305
- function decodeJwtPart(value) {
1306
- return JSON.parse(Buffer.from(base64UrlToBase64(value), "base64").toString("utf8"));
1307
- }
1308
- function base64UrlToBase64(value) {
1309
- const n = value.replace(/-/g, "+").replace(/_/g, "/");
1310
- return n + "=".repeat((4 - n.length % 4) % 4);
1311
- }
1312
1157
  function readAppInstanceIdHeader(ctx) {
1313
1158
  const value = ctx.get("x-hermes-app-instance-id").trim();
1314
1159
  return /^appi_[A-Za-z0-9_-]{16,96}$/u.test(value) ? value : null;
@@ -1513,6 +1358,135 @@ function createStatisticsRouter(options) {
1513
1358
  // src/http/routes/bootstrap.ts
1514
1359
  import Router3 from "@koa/router";
1515
1360
 
1361
+ // src/identity/identity.ts
1362
+ import { generateKeyPairSync, randomUUID as randomUUID3, sign } from "crypto";
1363
+ import { chmod as chmod2, mkdir as mkdir4 } from "fs/promises";
1364
+ import { z } from "zod";
1365
+ var linkIdentitySchema = z.object({
1366
+ install_id: z.string().min(1),
1367
+ link_id: z.string().min(1).nullable().optional(),
1368
+ public_key_pem: z.string().min(1),
1369
+ private_key_pem: z.string().min(1),
1370
+ created_at: z.string().min(1),
1371
+ updated_at: z.string().min(1)
1372
+ });
1373
+ async function loadIdentity(paths = resolveRuntimePaths()) {
1374
+ const value = await readJsonFile(paths.identityFile);
1375
+ if (value === null) {
1376
+ return null;
1377
+ }
1378
+ return linkIdentitySchema.parse(value);
1379
+ }
1380
+ async function ensureIdentity(paths = resolveRuntimePaths()) {
1381
+ const existing = await loadIdentity(paths);
1382
+ if (existing) {
1383
+ return existing;
1384
+ }
1385
+ await mkdir4(paths.homeDir, { recursive: true, mode: 448 });
1386
+ await chmod2(paths.homeDir, 448).catch(() => void 0);
1387
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
1388
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1389
+ const identity = {
1390
+ install_id: `install_${randomUUID3().replaceAll("-", "")}`,
1391
+ link_id: null,
1392
+ public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
1393
+ private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
1394
+ created_at: now,
1395
+ updated_at: now
1396
+ };
1397
+ await writeJsonFile(paths.identityFile, identity);
1398
+ return identity;
1399
+ }
1400
+ async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
1401
+ const identity = await ensureIdentity(paths);
1402
+ const next = {
1403
+ ...identity,
1404
+ link_id: linkId,
1405
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1406
+ };
1407
+ await writeJsonFile(paths.identityFile, next);
1408
+ return next;
1409
+ }
1410
+ function signRelayNonce(identity, nonce) {
1411
+ return signIdentityPayload(identity, nonce);
1412
+ }
1413
+ function signIdentityPayload(identity, payload) {
1414
+ const signature = sign(null, Buffer.from(payload, "utf8"), identity.private_key_pem);
1415
+ return signature.toString("base64url");
1416
+ }
1417
+
1418
+ // src/config/config.ts
1419
+ var defaultLinkConfig = {
1420
+ port: LINK_DEFAULT_PORT,
1421
+ lanHost: null,
1422
+ serverBaseUrl: "https://hermes-server.catwiki.ai",
1423
+ relayBaseUrl: "https://hermes-relay.catwiki.ai",
1424
+ appConnectTokenIssuer: "https://hermes-server.catwiki.ai",
1425
+ appConnectTokenAudience: "hermes-link",
1426
+ language: "auto",
1427
+ logLevel: "warn"
1428
+ };
1429
+ async function loadConfig(paths = resolveRuntimePaths()) {
1430
+ const existing = await readJsonFile(paths.configFile);
1431
+ const language = normalizeConfiguredLanguage(existing?.language);
1432
+ const lanHost = normalizeLanHost(existing?.lanHost);
1433
+ const logLevel = normalizeLogLevel(existing?.logLevel ?? process.env.HERMESLINK_LOG_LEVEL);
1434
+ return {
1435
+ ...defaultLinkConfig,
1436
+ ...existing ?? {},
1437
+ language,
1438
+ lanHost,
1439
+ logLevel
1440
+ };
1441
+ }
1442
+ async function saveConfig(patch, paths = resolveRuntimePaths()) {
1443
+ const current = await loadConfig(paths);
1444
+ const next = {
1445
+ ...current,
1446
+ ...patch,
1447
+ logLevel: patch.logLevel === void 0 ? current.logLevel : normalizeLogLevel(patch.logLevel)
1448
+ };
1449
+ await writeJsonFile(paths.configFile, next);
1450
+ return next;
1451
+ }
1452
+ function normalizeConfiguredLanguage(language) {
1453
+ if (language === "zh-CN" || language === "en" || language === "auto") {
1454
+ return language;
1455
+ }
1456
+ return defaultLinkConfig.language;
1457
+ }
1458
+ function normalizeLogLevel(level) {
1459
+ if (level === "debug" || level === "info" || level === "warn" || level === "error") {
1460
+ return level;
1461
+ }
1462
+ return defaultLinkConfig.logLevel;
1463
+ }
1464
+ function normalizeLanHost(value) {
1465
+ if (value === null || value === void 0) {
1466
+ return null;
1467
+ }
1468
+ if (typeof value !== "string") {
1469
+ return null;
1470
+ }
1471
+ const host = value.trim().replace(/^\[/u, "").replace(/\]$/u, "");
1472
+ if (!host) {
1473
+ return null;
1474
+ }
1475
+ if (!isUsableLanIpv4(host)) {
1476
+ return null;
1477
+ }
1478
+ return host;
1479
+ }
1480
+ function isUsableLanIpv4(value) {
1481
+ const parts = value.split(".").map((part) => Number.parseInt(part, 10));
1482
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
1483
+ return false;
1484
+ }
1485
+ const [first, second, , fourth] = parts;
1486
+ const privateRange = first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
1487
+ return privateRange && fourth !== 0 && fourth !== 255;
1488
+ }
1489
+
1516
1490
  // src/network/topology.ts
1517
1491
  import os5 from "os";
1518
1492
  var VIRTUAL_INTERFACE_NAME_PATTERN = /(docker|veth|vmnet|vmenet|vbox|virtualbox|vmware|tailscale|zerotier|wireguard|utun|virbr|hyper-v|vethernet|loopback|\blo\b|^lo\d*$|^br-|^bridge\d+$|^zt|^tun|^tap|awdl|llw|anpi|gif|stf|ipsec|ppp)/iu;
@@ -2237,7 +2211,7 @@ function createConversationsRouter(options) {
2237
2211
 
2238
2212
  // src/http/routes/pairing.ts
2239
2213
  import Router7 from "@koa/router";
2240
- import path6 from "path";
2214
+ import path7 from "path";
2241
2215
  function readString4(body, ...keys) {
2242
2216
  if (!body || typeof body !== "object") return null;
2243
2217
  for (const key of keys) {
@@ -2264,10 +2238,10 @@ async function readJsonBody4(req) {
2264
2238
  });
2265
2239
  }
2266
2240
  function pairingSessionPath(sessionId, paths) {
2267
- return path6.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
2241
+ return path7.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
2268
2242
  }
2269
2243
  function pairingClaimPath(sessionId, paths) {
2270
- return path6.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
2244
+ return path7.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
2271
2245
  }
2272
2246
  async function readPairingSession(sessionId, paths) {
2273
2247
  const record = await readJsonFile(pairingSessionPath(sessionId, paths));
@@ -2312,35 +2286,18 @@ function createPairingRouter(options) {
2312
2286
  if (!sessionId || !claimToken) {
2313
2287
  throw new LinkHttpError(400, "pairing_claim_invalid", "session_id and claim_token are required");
2314
2288
  }
2315
- const [identity, config, localSession] = await Promise.all([
2289
+ const [identity, localSession] = await Promise.all([
2316
2290
  loadIdentity(paths),
2317
- loadConfig(paths),
2318
2291
  readPairingSession(sessionId, paths)
2319
2292
  ]);
2320
2293
  if (!identity?.link_id) throw new LinkHttpError(409, "link_not_paired", "Hermes Link is not paired");
2321
2294
  if (!localSession) throw new LinkHttpError(404, "pairing_session_not_found", "Pairing session was not found");
2322
2295
  if (isPairingSessionExpired(localSession)) throw new LinkHttpError(404, "pairing_session_expired", "Pairing session has expired");
2323
2296
  if (localSession.link_id !== identity.link_id) throw new LinkHttpError(409, "pairing_claim_mismatch", "Pairing claim does not match this Link");
2324
- let verified;
2325
- try {
2326
- const resp = await fetch(`${config.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/link-pairings/${sessionId}/claim/verify`, {
2327
- method: "POST",
2328
- headers: { accept: "application/json", "content-type": "application/json" },
2329
- body: JSON.stringify({ claim_token: claimToken, app_instance_id: readString4(body, "app_instance_id", "appInstanceId") ?? void 0 })
2330
- });
2331
- if (!resp.ok) {
2332
- const errBody = await resp.json().catch(() => ({}));
2333
- throw new LinkHttpError(resp.status, "pairing_claim_verify_failed", String(errBody.message ?? "Pairing claim verification failed"));
2334
- }
2335
- verified = await resp.json();
2336
- } catch (err) {
2337
- if (err instanceof LinkHttpError) throw err;
2338
- throw new LinkHttpError(503, "pairing_server_unreachable", `Unable to verify pairing claim: ${err.message}`);
2297
+ if (localSession.code !== claimToken) {
2298
+ throw new LinkHttpError(409, "pairing_claim_mismatch", "Pairing claim token does not match");
2339
2299
  }
2340
- if (verified.ok !== true || verified.linkId !== identity.link_id) {
2341
- throw new LinkHttpError(409, "pairing_claim_mismatch", "Pairing claim does not match this Link");
2342
- }
2343
- const appInstanceId = typeof verified.appInstanceId === "string" ? verified.appInstanceId : null;
2300
+ const appInstanceId = readString4(body, "app_instance_id", "appInstanceId");
2344
2301
  const deviceLabel = readString4(body, "device_label", "deviceLabel") ?? "HermesPilot App";
2345
2302
  const devicePlatform = readString4(body, "device_platform", "devicePlatform") ?? "unknown";
2346
2303
  const session = await createDeviceSession(
@@ -2598,9 +2555,9 @@ function openSqliteDatabase(filePath, options = {}) {
2598
2555
 
2599
2556
  // src/storage/link-database.ts
2600
2557
  import { mkdir as mkdir5 } from "fs/promises";
2601
- import path7 from "path";
2558
+ import path8 from "path";
2602
2559
  async function initLinkDatabase(paths) {
2603
- await mkdir5(path7.dirname(paths.databaseFile), { recursive: true, mode: 448 });
2560
+ await mkdir5(path8.dirname(paths.databaseFile), { recursive: true, mode: 448 });
2604
2561
  const db = openDb(paths);
2605
2562
  try {
2606
2563
  db.exec(`
@@ -2696,35 +2653,35 @@ function openDb(paths) {
2696
2653
 
2697
2654
  // src/conversations/service.ts
2698
2655
  import { EventEmitter as EventEmitter2 } from "events";
2699
- import crypto3 from "crypto";
2656
+ import crypto4 from "crypto";
2700
2657
 
2701
2658
  // src/conversations/store.ts
2702
2659
  import { mkdir as mkdir6, readFile as readFile4, rm as rm4, writeFile as writeFile2 } from "fs/promises";
2703
- import path8 from "path";
2704
- import crypto from "crypto";
2660
+ import path9 from "path";
2661
+ import crypto2 from "crypto";
2705
2662
  function createConversationId() {
2706
- return `conv_${crypto.randomUUID().replaceAll("-", "")}`;
2663
+ return `conv_${crypto2.randomUUID().replaceAll("-", "")}`;
2707
2664
  }
2708
2665
  function createMessageId() {
2709
- return `msg_${crypto.randomUUID().replaceAll("-", "")}`;
2666
+ return `msg_${crypto2.randomUUID().replaceAll("-", "")}`;
2710
2667
  }
2711
2668
  function createRunId() {
2712
- return `run_${crypto.randomUUID().replaceAll("-", "")}`;
2669
+ return `run_${crypto2.randomUUID().replaceAll("-", "")}`;
2713
2670
  }
2714
2671
  function conversationDir(paths, conversationId) {
2715
- return path8.join(paths.conversationsDir, conversationId);
2672
+ return path9.join(paths.conversationsDir, conversationId);
2716
2673
  }
2717
2674
  function manifestPath(paths, conversationId) {
2718
- return path8.join(conversationDir(paths, conversationId), "manifest.json");
2675
+ return path9.join(conversationDir(paths, conversationId), "manifest.json");
2719
2676
  }
2720
2677
  function snapshotPath(paths, conversationId) {
2721
- return path8.join(conversationDir(paths, conversationId), "snapshot.json");
2678
+ return path9.join(conversationDir(paths, conversationId), "snapshot.json");
2722
2679
  }
2723
2680
  function eventsPath(paths, conversationId) {
2724
- return path8.join(conversationDir(paths, conversationId), "events.json");
2681
+ return path9.join(conversationDir(paths, conversationId), "events.json");
2725
2682
  }
2726
2683
  function blobPath(paths, blobId) {
2727
- return path8.join(paths.blobsDir, blobId);
2684
+ return path9.join(paths.blobsDir, blobId);
2728
2685
  }
2729
2686
  async function readJson(filePath, fallback) {
2730
2687
  try {
@@ -2735,7 +2692,7 @@ async function readJson(filePath, fallback) {
2735
2692
  }
2736
2693
  }
2737
2694
  async function writeJson(filePath, data) {
2738
- await mkdir6(path8.dirname(filePath), { recursive: true, mode: 448 });
2695
+ await mkdir6(path9.dirname(filePath), { recursive: true, mode: 448 });
2739
2696
  await writeFile2(filePath, JSON.stringify(data), { mode: 384 });
2740
2697
  }
2741
2698
  function assertValidConversationId(id) {
@@ -2792,8 +2749,8 @@ async function listConversationIds(paths) {
2792
2749
 
2793
2750
  // src/conversations/blobs.ts
2794
2751
  import { mkdir as mkdir7, readFile as readFile5, rm as rm5, writeFile as writeFile3 } from "fs/promises";
2795
- import path9 from "path";
2796
- import crypto2 from "crypto";
2752
+ import path10 from "path";
2753
+ import crypto3 from "crypto";
2797
2754
  var MAX_BLOB_UPLOAD_BYTES2 = 50 * 1024 * 1024;
2798
2755
  function blobManifestPath(paths, blobId) {
2799
2756
  return `${blobPath(paths, blobId)}.json`;
@@ -2801,7 +2758,7 @@ function blobManifestPath(paths, blobId) {
2801
2758
  function normalizeMime(mime, filename) {
2802
2759
  if (mime && mime !== "application/octet-stream") return mime;
2803
2760
  if (filename) {
2804
- const ext = path9.extname(filename).toLowerCase();
2761
+ const ext = path10.extname(filename).toLowerCase();
2805
2762
  const mimeMap = {
2806
2763
  ".jpg": "image/jpeg",
2807
2764
  ".jpeg": "image/jpeg",
@@ -2821,7 +2778,7 @@ function normalizeMime(mime, filename) {
2821
2778
  }
2822
2779
  function sanitizeFilename(filename, fallback) {
2823
2780
  if (!filename) return fallback;
2824
- return path9.basename(filename).replace(/[^\w.\-]/gu, "_").slice(0, 255) || fallback;
2781
+ return path10.basename(filename).replace(/[^\w.\-]/gu, "_").slice(0, 255) || fallback;
2825
2782
  }
2826
2783
  async function writeBlob(paths, conversationId, input) {
2827
2784
  assertValidConversationId(conversationId);
@@ -2831,9 +2788,9 @@ async function writeBlob(paths, conversationId, input) {
2831
2788
  if (input.bytes.byteLength > MAX_BLOB_UPLOAD_BYTES2) {
2832
2789
  throw new LinkHttpError(413, "blob_too_large", "Blob is too large");
2833
2790
  }
2834
- const id = `blob_${crypto2.randomUUID().replaceAll("-", "")}`;
2791
+ const id = `blob_${crypto3.randomUUID().replaceAll("-", "")}`;
2835
2792
  const filePath = blobPath(paths, id);
2836
- await mkdir7(path9.dirname(filePath), { recursive: true, mode: 448 });
2793
+ await mkdir7(path10.dirname(filePath), { recursive: true, mode: 448 });
2837
2794
  await writeFile3(filePath, input.bytes, { mode: 384 });
2838
2795
  const blob = {
2839
2796
  id,
@@ -3206,7 +3163,7 @@ var ConversationService = class extends EventEmitter2 {
3206
3163
  return { deleted_count: count };
3207
3164
  }
3208
3165
  async prepareClearAllConversationPlan() {
3209
- const planId = `plan_${crypto3.randomUUID().replaceAll("-", "")}`;
3166
+ const planId = `plan_${crypto4.randomUUID().replaceAll("-", "")}`;
3210
3167
  const ids = await listConversationIds(this.paths);
3211
3168
  const activeIds = [];
3212
3169
  for (const id of ids) {
@@ -3284,7 +3241,7 @@ import { promisify as promisify3 } from "util";
3284
3241
 
3285
3242
  // src/hermes/config.ts
3286
3243
  import os6 from "os";
3287
- import path10 from "path";
3244
+ import path11 from "path";
3288
3245
  import YAML from "yaml";
3289
3246
  var PROFILE_PERMISSION_TOOLSETS = [
3290
3247
  {
@@ -3414,19 +3371,19 @@ var profileApiServerPortAssignmentQueue = Promise.resolve();
3414
3371
  function resolveHermesProfilesDir() {
3415
3372
  const hermesHome = process.env.HERMES_HOME?.trim();
3416
3373
  if (hermesHome) {
3417
- return path10.join(
3418
- resolveDefaultHermesRoot(path10.resolve(hermesHome)),
3374
+ return path11.join(
3375
+ resolveDefaultHermesRoot(path11.resolve(hermesHome)),
3419
3376
  "profiles"
3420
3377
  );
3421
3378
  }
3422
- return path10.join(os6.homedir(), ".hermes", "profiles");
3379
+ return path11.join(os6.homedir(), ".hermes", "profiles");
3423
3380
  }
3424
3381
  function isValidProfileName(value) {
3425
3382
  return typeof value === "string" && /^[a-zA-Z0-9._-]{1,64}$/.test(value);
3426
3383
  }
3427
3384
  function resolveDefaultHermesRoot(hermesHome) {
3428
- if (path10.basename(path10.dirname(hermesHome)) === "profiles") {
3429
- return path10.dirname(path10.dirname(hermesHome));
3385
+ if (path11.basename(path11.dirname(hermesHome)) === "profiles") {
3386
+ return path11.dirname(path11.dirname(hermesHome));
3430
3387
  }
3431
3388
  return hermesHome;
3432
3389
  }
@@ -3638,7 +3595,6 @@ export {
3638
3595
  LINK_VERSION,
3639
3596
  DAEMON_LOG_FILE,
3640
3597
  resolveRuntimePaths,
3641
- readJsonFile,
3642
3598
  writeJsonFile,
3643
3599
  loadConfig,
3644
3600
  saveConfig,
@@ -3655,5 +3611,6 @@ export {
3655
3611
  readRecentLogEntries,
3656
3612
  readRecentGatewayLogEntries,
3657
3613
  bootstrapWithRelay,
3614
+ generateAppConnectToken,
3658
3615
  startLinkService
3659
3616
  };
package/dist/cli/index.js CHANGED
@@ -10,11 +10,11 @@ import {
10
10
  disableAutostart,
11
11
  enableAutostart,
12
12
  ensureIdentity,
13
+ generateAppConnectToken,
13
14
  getAutostartStatus,
14
15
  loadConfig,
15
16
  loadIdentity,
16
17
  normalizeLanHost,
17
- readJsonFile,
18
18
  readRecentGatewayLogEntries,
19
19
  readRecentLogEntries,
20
20
  resolveRuntimePaths,
@@ -22,10 +22,10 @@ import {
22
22
  saveConfig,
23
23
  startLinkService,
24
24
  writeJsonFile
25
- } from "../chunk-CCKWZNLX.js";
25
+ } from "../chunk-QL5SEM7J.js";
26
26
 
27
27
  // src/cli/index.ts
28
- import { mkdir as mkdir2 } from "fs/promises";
28
+ import { mkdir as mkdir3 } from "fs/promises";
29
29
  import qrcode from "qrcode-terminal";
30
30
 
31
31
  // src/daemon/process.ts
@@ -190,42 +190,9 @@ async function runDaemonSupervisor(options) {
190
190
  await cleanup();
191
191
  }
192
192
 
193
- // src/security/app-connect-token.ts
194
- import crypto from "crypto";
193
+ // src/pairing/preflight.ts
195
194
  import path2 from "path";
196
- var TOKENS_FILE = "app-connect-tokens.json";
197
- var TOKEN_EXPIRY_MS = 5 * 60 * 1e3;
198
- function tokensFilePath(paths) {
199
- return path2.join(paths.homeDir, TOKENS_FILE);
200
- }
201
- async function readTokens(paths) {
202
- const raw = await readJsonFile(tokensFilePath(paths));
203
- if (!Array.isArray(raw)) return [];
204
- const now = /* @__PURE__ */ new Date();
205
- return raw.filter(isValidToken).filter((t) => new Date(t.expiresAt) > now);
206
- }
207
- function isValidToken(value) {
208
- if (!value || typeof value !== "object") return false;
209
- const t = value;
210
- return typeof t.token === "string" && typeof t.createdAt === "string" && typeof t.expiresAt === "string";
211
- }
212
- async function saveTokens(tokens, paths) {
213
- await writeJsonFile(tokensFilePath(paths), tokens);
214
- }
215
- async function generateAppConnectToken(paths) {
216
- const runtimePaths = paths ?? resolveRuntimePaths();
217
- const now = /* @__PURE__ */ new Date();
218
- const token = {
219
- token: crypto.randomBytes(32).toString("base64url"),
220
- createdAt: now.toISOString(),
221
- usedAt: null,
222
- expiresAt: new Date(now.getTime() + TOKEN_EXPIRY_MS).toISOString()
223
- };
224
- const tokens = await readTokens(runtimePaths);
225
- tokens.push(token);
226
- await saveTokens(tokens, runtimePaths);
227
- return token;
228
- }
195
+ import { mkdir as mkdir2 } from "fs/promises";
229
196
 
230
197
  // src/runtime/browser.ts
231
198
  import { spawn as spawn2 } from "child_process";
@@ -261,28 +228,48 @@ async function spawnDetached(command2, args2) {
261
228
  }
262
229
 
263
230
  // src/pairing/preflight.ts
231
+ function pairingSessionPath(sessionId, paths) {
232
+ return path2.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
233
+ }
264
234
  async function runPairingPreflight(options) {
265
235
  const token = await generateAppConnectToken(options.paths);
266
- const baseUrl = (options.serverBaseUrl ?? options.config.serverBaseUrl).replace(/\/+$/u, "");
267
- const pairingUrl = buildPairingUrl(baseUrl, {
236
+ const localApiUrl = `http://127.0.0.1:${options.config.port}`;
237
+ const sessionId = `ps_${token.token.slice(0, 16)}`;
238
+ const session = {
239
+ session_id: sessionId,
240
+ code: token.token,
241
+ link_id: options.identity.link_id ?? "",
242
+ display_name: "Hermes Link",
243
+ local_api_url: localApiUrl,
244
+ server_base_url: options.config.serverBaseUrl,
245
+ relay_base_url: options.config.relayBaseUrl,
246
+ preferred_urls: [localApiUrl],
247
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
248
+ expires_at: token.expiresAt
249
+ };
250
+ await mkdir2(options.paths.pairingDir, { recursive: true, mode: 448 }).catch(() => void 0);
251
+ await writeJsonFile(pairingSessionPath(sessionId, options.paths), session);
252
+ const pairingUrl = buildPairingUrl({
268
253
  linkId: options.identity.link_id ?? "",
269
254
  installId: options.identity.install_id,
270
255
  connectToken: token.token,
271
- port: options.config.port
256
+ port: options.config.port,
257
+ localApiUrl
272
258
  });
273
259
  if (options.openBrowser !== false) {
274
260
  await openSystemBrowser(pairingUrl).catch(() => void 0);
275
261
  }
276
262
  return { pairingUrl, connectToken: token.token };
277
263
  }
278
- function buildPairingUrl(serverBaseUrl, params) {
264
+ function buildPairingUrl(params) {
279
265
  const qs = new URLSearchParams({
280
266
  link_id: params.linkId,
281
267
  install_id: params.installId,
282
268
  connect_token: params.connectToken,
283
- port: String(params.port)
269
+ port: String(params.port),
270
+ local_url: params.localApiUrl
284
271
  });
285
- return `${serverBaseUrl}/link/pair?${qs.toString()}`;
272
+ return `hermesapp://pair?${qs.toString()}`;
286
273
  }
287
274
 
288
275
  // src/cli/index.ts
@@ -308,7 +295,7 @@ async function main() {
308
295
  return;
309
296
  }
310
297
  const paths = resolveRuntimePaths();
311
- await mkdir2(paths.homeDir, { recursive: true, mode: 448 });
298
+ await mkdir3(paths.homeDir, { recursive: true, mode: 448 });
312
299
  switch (command) {
313
300
  case "start":
314
301
  await cmdStart(paths);
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startLinkService
3
- } from "../chunk-CCKWZNLX.js";
3
+ } from "../chunk-QL5SEM7J.js";
4
4
  export {
5
5
  startLinkService
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bulolo/hermes-link",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through zhiji",
5
5
  "license": "MIT",
6
6
  "type": "module",