@hermespilot/link 0.1.1 → 0.1.3

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
@@ -22,11 +22,17 @@ hermeslink --version
22
22
  hermeslink status
23
23
  hermeslink pair
24
24
  hermeslink start
25
+ hermeslink stop
26
+ hermeslink autostart on
27
+ hermeslink autostart off
25
28
  hermeslink doctor
29
+ hermeslink logs
26
30
  ```
27
31
 
28
32
  `hermeslink pair` requires HermesPilot Server and Relay to be available. The terminal side does not ask for a HermesPilot account; the App must be logged in before it scans or claims a pairing session.
29
33
 
34
+ After a successful QR claim, `hermeslink pair` starts Hermes Link in the background and enables boot autostart. Boot autostart does not configure launchd/systemd restart policies; if the user stops Hermes Link, the operating system should not automatically relaunch it until the next login/boot autostart cycle.
35
+
30
36
  CLI output follows the current system language when it is Chinese or English. You can override it for a single command with `HERMESLINK_LANG=zh-CN` or `HERMESLINK_LANG=en`.
31
37
 
32
38
  ## Runtime data
@@ -38,3 +44,5 @@ Hermes Link keeps its local identity and runtime state under:
38
44
  ```
39
45
 
40
46
  Uninstalling the npm package does not remove this directory, so the same Link ID can be reused after reinstalling.
47
+
48
+ Service logs are written as rotated JSONL files under `~/.hermeslink/logs/hermeslink.log`. A paired App can read the same service log stream through `GET /api/v1/logs` using the normal Link access token.
@@ -4,7 +4,7 @@ import Router from "@koa/router";
4
4
  import { Readable } from "stream";
5
5
 
6
6
  // src/constants.ts
7
- var LINK_VERSION = "0.1.1";
7
+ var LINK_VERSION = "0.1.3";
8
8
  var LINK_COMMAND = "hermeslink";
9
9
  var LINK_DEFAULT_PORT = 52379;
10
10
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -270,7 +270,7 @@ async function cancelHermesRun(runId, options = {}) {
270
270
  }
271
271
  fallbackRuns.delete(runId);
272
272
  }
273
- async function callHermesApi(path5, init, options) {
273
+ async function callHermesApi(path6, init, options) {
274
274
  const config = await readHermesApiServerConfig();
275
275
  if (!config.port || !config.key) {
276
276
  return new Response(null, { status: 503 });
@@ -280,7 +280,7 @@ async function callHermesApi(path5, init, options) {
280
280
  headers.set("accept", headers.get("accept") ?? "application/json");
281
281
  headers.set("x-api-key", config.key);
282
282
  headers.set("authorization", `Bearer ${config.key}`);
283
- return await fetcher(`http://127.0.0.1:${config.port}${path5}`, {
283
+ return await fetcher(`http://127.0.0.1:${config.port}${path6}`, {
284
284
  ...init,
285
285
  headers
286
286
  }).catch(() => new Response(null, { status: 503 }));
@@ -672,33 +672,48 @@ function readErrorMessage(payload) {
672
672
 
673
673
  // src/topology/network.ts
674
674
  import os4 from "os";
675
+ 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;
676
+ var MAX_LAN_IPS = 4;
677
+ var MAX_PUBLIC_IPV4S = 2;
678
+ var MAX_PUBLIC_IPV6S = 2;
675
679
  async function discoverRouteCandidates(options) {
676
680
  const lanIps = discoverLanIps();
677
681
  const publicIps = options.relayBootstrapToken ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
682
+ const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
683
+ const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
678
684
  const preferredUrls = [
679
685
  ...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
680
- ...publicIps.publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
681
- ...publicIps.publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
686
+ ...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
687
+ ...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
682
688
  `${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/links/${options.linkId}`
683
689
  ];
684
690
  return {
685
691
  lanIps,
686
- publicIpv4s: publicIps.publicIpv4s,
687
- publicIpv6s: publicIps.publicIpv6s,
692
+ publicIpv4s,
693
+ publicIpv6s,
688
694
  preferredUrls
689
695
  };
690
696
  }
691
697
  function discoverLanIps() {
698
+ return discoverLanIpsFromInterfaces(os4.networkInterfaces());
699
+ }
700
+ function discoverLanIpsFromInterfaces(interfaces) {
692
701
  const result = /* @__PURE__ */ new Set();
693
- const interfaces = os4.networkInterfaces();
694
- for (const items of Object.values(interfaces)) {
702
+ const candidates = [];
703
+ for (const [name, items] of Object.entries(interfaces)) {
704
+ if (shouldIgnoreInterface(name)) {
705
+ continue;
706
+ }
695
707
  for (const item of items ?? []) {
696
- if (!item.internal && item.address && (item.family === "IPv4" || item.family === "IPv6")) {
697
- result.add(item.address);
708
+ if (!item.internal && item.address && item.family === "IPv4" && isUsableLanIpv4(item.address, item.netmask)) {
709
+ candidates.push({ name, address: item.address });
698
710
  }
699
711
  }
700
712
  }
701
- return [...result];
713
+ for (const candidate of candidates.sort(compareLanCandidate)) {
714
+ result.add(candidate.address);
715
+ }
716
+ return [...result].slice(0, MAX_LAN_IPS);
702
717
  }
703
718
  async function observePublicRoute(options) {
704
719
  const fetcher = options.fetchImpl ?? fetch;
@@ -723,8 +738,8 @@ async function observePublicRoute(options) {
723
738
  typeof observed?.ip === "string" ? observed.ip : null
724
739
  ].filter((value) => Boolean(value));
725
740
  return {
726
- publicIpv4s: unique(values.filter((ip) => !ip.includes(":"))),
727
- publicIpv6s: unique(values.filter((ip) => ip.includes(":")))
741
+ publicIpv4s: unique(values.filter(isUsablePublicIpv4)),
742
+ publicIpv6s: unique(values.filter(isUsablePublicIpv6))
728
743
  };
729
744
  }
730
745
  function readIpRecord(value) {
@@ -737,6 +752,81 @@ function readIpRecord(value) {
737
752
  function buildDirectUrl(ip, port) {
738
753
  return `http://${ip.includes(":") ? `[${ip}]` : ip}:${port}`;
739
754
  }
755
+ function shouldIgnoreInterface(name) {
756
+ return !name.trim() || VIRTUAL_INTERFACE_NAME_PATTERN.test(name);
757
+ }
758
+ function compareLanCandidate(left, right) {
759
+ const priority = interfacePriority(left.name) - interfacePriority(right.name);
760
+ return priority || left.name.localeCompare(right.name) || left.address.localeCompare(right.address);
761
+ }
762
+ function interfacePriority(name) {
763
+ if (/^(en|eth|wlan|wi-fi|wifi)/iu.test(name)) {
764
+ return 0;
765
+ }
766
+ return 1;
767
+ }
768
+ function isUsableLanIpv4(address, netmask) {
769
+ return isPrivateIpv4(address) && !isNetworkOrBroadcastIpv4Address(address, netmask);
770
+ }
771
+ function isUsablePublicIpv4(address) {
772
+ return isValidIpv4(address) && !isSpecialIpv4(address);
773
+ }
774
+ function isUsablePublicIpv6(address) {
775
+ const normalized = address.toLowerCase();
776
+ return normalized.includes(":") && !normalized.startsWith("fe80:") && !normalized.startsWith("fc") && !normalized.startsWith("fd") && !normalized.startsWith("ff") && normalized !== "::" && normalized !== "::1";
777
+ }
778
+ function isPrivateIpv4(address) {
779
+ const parts = parseIpv4Segments(address);
780
+ if (!parts) {
781
+ return false;
782
+ }
783
+ const [first, second] = parts;
784
+ return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
785
+ }
786
+ function isSpecialIpv4(address) {
787
+ const parts = parseIpv4Segments(address);
788
+ if (!parts) {
789
+ return true;
790
+ }
791
+ const [first, second, third, fourth] = parts;
792
+ return first === 0 || first === 10 || first === 127 || first >= 224 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 192 && second === 0 && third === 2 || first === 198 && (second === 18 || second === 19) || first === 198 && second === 51 && third === 100 || first === 203 && second === 0 && third === 113 || first === 255 && second === 255 && third === 255 && fourth === 255;
793
+ }
794
+ function isNetworkOrBroadcastIpv4Address(address, netmask) {
795
+ const addressParts = parseIpv4Segments(address);
796
+ const netmaskParts = netmask ? parseIpv4Segments(netmask) : null;
797
+ if (!addressParts) {
798
+ return true;
799
+ }
800
+ if (!netmaskParts) {
801
+ const last = addressParts[3];
802
+ return last === 0 || last === 255;
803
+ }
804
+ const addressInt = ipv4SegmentsToInt(addressParts);
805
+ const netmaskInt = ipv4SegmentsToInt(netmaskParts);
806
+ const hostMask = ~netmaskInt >>> 0;
807
+ if (hostMask === 0) {
808
+ return false;
809
+ }
810
+ const networkInt = addressInt & netmaskInt;
811
+ const broadcastInt = (networkInt | hostMask) >>> 0;
812
+ return addressInt === networkInt || addressInt === broadcastInt;
813
+ }
814
+ function isValidIpv4(address) {
815
+ return Boolean(parseIpv4Segments(address));
816
+ }
817
+ function parseIpv4Segments(address) {
818
+ if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(address)) {
819
+ return null;
820
+ }
821
+ const parts = address.split(".").map((part) => Number.parseInt(part, 10));
822
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
823
+ return null;
824
+ }
825
+ return parts;
826
+ }
827
+ function ipv4SegmentsToInt(parts) {
828
+ return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
829
+ }
740
830
  function unique(values) {
741
831
  return [...new Set(values)];
742
832
  }
@@ -783,7 +873,7 @@ async function preparePairing(paths = resolveRuntimePaths()) {
783
873
  display_name: defaultDisplayName(),
784
874
  session_id: created.sessionId,
785
875
  code: created.code,
786
- preferred_urls: routes.preferredUrls
876
+ preferred_urls: qrPreferredUrls(routes)
787
877
  };
788
878
  return {
789
879
  sessionId: created.sessionId,
@@ -840,8 +930,8 @@ async function loadRequiredIdentity(paths) {
840
930
  }
841
931
  return identity;
842
932
  }
843
- async function postServerJson(serverBaseUrl, path5, body) {
844
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path5}`, {
933
+ async function postServerJson(serverBaseUrl, path6, body) {
934
+ const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path6}`, {
845
935
  method: "POST",
846
936
  headers: {
847
937
  accept: "application/json",
@@ -851,8 +941,8 @@ async function postServerJson(serverBaseUrl, path5, body) {
851
941
  });
852
942
  return readJsonResponse2(response);
853
943
  }
854
- async function patchServerJson(serverBaseUrl, path5, token, body) {
855
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path5}`, {
944
+ async function patchServerJson(serverBaseUrl, path6, token, body) {
945
+ const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path6}`, {
856
946
  method: "PATCH",
857
947
  headers: {
858
948
  accept: "application/json",
@@ -885,6 +975,9 @@ function readErrorMessage2(payload) {
885
975
  function defaultDisplayName() {
886
976
  return `Hermes Link ${process.platform}`;
887
977
  }
978
+ function qrPreferredUrls(routes) {
979
+ return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
980
+ }
888
981
  function formatDevice(device) {
889
982
  return {
890
983
  id: device.id,
@@ -955,11 +1048,217 @@ function base64UrlToBase64(value) {
955
1048
  return normalized + "=".repeat((4 - normalized.length % 4) % 4);
956
1049
  }
957
1050
 
1051
+ // src/runtime/logger.ts
1052
+ import { appendFile, mkdir as mkdir5, open as open2, readFile as readFile3, rename as rename3, rm as rm3, stat as stat2 } from "fs/promises";
1053
+ import path5 from "path";
1054
+ var DEFAULT_LOG_FILE = "hermeslink.log";
1055
+ var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
1056
+ var DEFAULT_MAX_FILES = 5;
1057
+ var DEFAULT_READ_LIMIT = 200;
1058
+ var MAX_READ_LIMIT = 1e3;
1059
+ var DEFAULT_MAX_BYTES_PER_FILE = 512 * 1024;
1060
+ var FileLogger = class {
1061
+ filePath;
1062
+ paths;
1063
+ maxFileBytes;
1064
+ maxFiles;
1065
+ now;
1066
+ queue = Promise.resolve();
1067
+ constructor(options = {}) {
1068
+ this.paths = options.paths ?? resolveRuntimePaths();
1069
+ this.filePath = getLinkLogFile(this.paths, options.fileName);
1070
+ this.maxFileBytes = Math.max(256, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
1071
+ this.maxFiles = Math.max(0, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
1072
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1073
+ }
1074
+ debug(message, fields) {
1075
+ return this.write("debug", message, fields);
1076
+ }
1077
+ info(message, fields) {
1078
+ return this.write("info", message, fields);
1079
+ }
1080
+ warn(message, fields) {
1081
+ return this.write("warn", message, fields);
1082
+ }
1083
+ error(message, fields) {
1084
+ return this.write("error", message, fields);
1085
+ }
1086
+ write(level, message, fields) {
1087
+ const entry = {
1088
+ ts: this.now().toISOString(),
1089
+ level,
1090
+ message,
1091
+ ...fields ? { fields: sanitizeFields(fields) } : {}
1092
+ };
1093
+ const next = this.queue.then(() => this.appendEntry(entry)).catch(() => void 0);
1094
+ this.queue = next;
1095
+ return next;
1096
+ }
1097
+ flush() {
1098
+ return this.queue;
1099
+ }
1100
+ async appendEntry(entry) {
1101
+ await mkdir5(this.paths.logsDir, { recursive: true, mode: 448 });
1102
+ const line = `${JSON.stringify(entry)}
1103
+ `;
1104
+ await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
1105
+ await appendFile(this.filePath, line, { mode: 384 });
1106
+ }
1107
+ async rotateIfNeeded(nextBytes) {
1108
+ const current = await stat2(this.filePath).catch(() => null);
1109
+ if (!current || current.size === 0 || current.size + nextBytes <= this.maxFileBytes) {
1110
+ return;
1111
+ }
1112
+ if (this.maxFiles === 0) {
1113
+ await rm3(this.filePath, { force: true }).catch(() => void 0);
1114
+ return;
1115
+ }
1116
+ await rm3(rotatedLogFile(this.filePath, this.maxFiles), { force: true }).catch(() => void 0);
1117
+ for (let index = this.maxFiles - 1; index >= 1; index -= 1) {
1118
+ await moveIfExists(rotatedLogFile(this.filePath, index), rotatedLogFile(this.filePath, index + 1));
1119
+ }
1120
+ await moveIfExists(this.filePath, rotatedLogFile(this.filePath, 1));
1121
+ }
1122
+ };
1123
+ function createFileLogger(options = {}) {
1124
+ return new FileLogger(options);
1125
+ }
1126
+ function getLinkLogFile(paths = resolveRuntimePaths(), fileName = DEFAULT_LOG_FILE) {
1127
+ return path5.join(paths.logsDir, fileName);
1128
+ }
1129
+ async function readRecentLogEntries(options = {}) {
1130
+ const paths = options.paths ?? resolveRuntimePaths();
1131
+ const filePath = getLinkLogFile(paths, options.fileName);
1132
+ const limit = clampLimit(options.limit);
1133
+ const maxFiles = Math.max(0, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
1134
+ const maxBytesPerFile = Math.max(1024, Math.floor(options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE));
1135
+ const files = [filePath, ...Array.from({ length: maxFiles }, (_, index) => rotatedLogFile(filePath, index + 1))];
1136
+ const entries = [];
1137
+ for (const file of files) {
1138
+ const raw = await readTail(file, maxBytesPerFile);
1139
+ if (!raw) {
1140
+ continue;
1141
+ }
1142
+ const lines = raw.split(/\r?\n/u).filter(Boolean);
1143
+ for (let index = lines.length - 1; index >= 0 && entries.length < limit; index -= 1) {
1144
+ const entry = parseLogLine(lines[index]);
1145
+ if (entry) {
1146
+ entries.push(entry);
1147
+ }
1148
+ }
1149
+ if (entries.length >= limit) {
1150
+ break;
1151
+ }
1152
+ }
1153
+ return entries.reverse();
1154
+ }
1155
+ function clampLimit(value) {
1156
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1157
+ return DEFAULT_READ_LIMIT;
1158
+ }
1159
+ return Math.min(MAX_READ_LIMIT, Math.max(1, Math.floor(value)));
1160
+ }
1161
+ function sanitizeFields(fields) {
1162
+ return sanitizeObject(fields, 0);
1163
+ }
1164
+ function sanitizeValue(value, depth) {
1165
+ if (value === null || typeof value === "boolean") {
1166
+ return value;
1167
+ }
1168
+ if (typeof value === "number") {
1169
+ return Number.isFinite(value) ? value : null;
1170
+ }
1171
+ if (typeof value === "string") {
1172
+ return value.length > 2e3 ? `${value.slice(0, 2e3)}...` : value;
1173
+ }
1174
+ if (Array.isArray(value)) {
1175
+ if (depth >= 3) {
1176
+ return "[array]";
1177
+ }
1178
+ return value.slice(0, 20).map((item) => sanitizeValue(item, depth + 1));
1179
+ }
1180
+ if (typeof value === "object" && value !== null) {
1181
+ if (depth >= 3) {
1182
+ return "[object]";
1183
+ }
1184
+ return sanitizeObject(value, depth + 1);
1185
+ }
1186
+ return String(value);
1187
+ }
1188
+ function sanitizeObject(value, depth) {
1189
+ const result = {};
1190
+ for (const [key, child] of Object.entries(value).slice(0, 50)) {
1191
+ if (isSensitiveKey(key)) {
1192
+ result[key] = "[redacted]";
1193
+ continue;
1194
+ }
1195
+ result[key] = sanitizeValue(child, depth);
1196
+ }
1197
+ return result;
1198
+ }
1199
+ function isSensitiveKey(key) {
1200
+ return /(authorization|cookie|token|secret|password|private[_-]?key|api[_-]?key)/iu.test(key);
1201
+ }
1202
+ function parseLogLine(line) {
1203
+ try {
1204
+ const value = JSON.parse(line);
1205
+ if (!value || typeof value.ts !== "string" || !isLogLevel(value.level) || typeof value.message !== "string") {
1206
+ return null;
1207
+ }
1208
+ return {
1209
+ ts: value.ts,
1210
+ level: value.level,
1211
+ message: value.message,
1212
+ ...value.fields && typeof value.fields === "object" ? { fields: value.fields } : {}
1213
+ };
1214
+ } catch {
1215
+ return null;
1216
+ }
1217
+ }
1218
+ function isLogLevel(value) {
1219
+ return value === "debug" || value === "info" || value === "warn" || value === "error";
1220
+ }
1221
+ async function readTail(filePath, maxBytes) {
1222
+ const info = await stat2(filePath).catch(() => null);
1223
+ if (!info || info.size <= 0) {
1224
+ return null;
1225
+ }
1226
+ if (info.size <= maxBytes) {
1227
+ return await readFile3(filePath, "utf8").catch(() => null);
1228
+ }
1229
+ const handle = await open2(filePath, "r").catch(() => null);
1230
+ if (!handle) {
1231
+ return null;
1232
+ }
1233
+ try {
1234
+ const length = Math.min(info.size, maxBytes);
1235
+ const buffer = Buffer.alloc(length);
1236
+ await handle.read(buffer, 0, length, info.size - length);
1237
+ return buffer.toString("utf8");
1238
+ } finally {
1239
+ await handle.close();
1240
+ }
1241
+ }
1242
+ async function moveIfExists(from, to) {
1243
+ await rm3(to, { force: true }).catch(() => void 0);
1244
+ await rename3(from, to).catch((error) => {
1245
+ if (error.code !== "ENOENT") {
1246
+ throw error;
1247
+ }
1248
+ });
1249
+ }
1250
+ function rotatedLogFile(filePath, index) {
1251
+ return `${filePath}.${index}`;
1252
+ }
1253
+
958
1254
  // src/http/app.ts
959
- async function createApp() {
1255
+ async function createApp(options = {}) {
1256
+ const paths = options.paths ?? resolveRuntimePaths();
1257
+ const logger = options.logger ?? createFileLogger({ paths });
960
1258
  const app = new Koa();
961
1259
  const router = new Router();
962
1260
  app.use(async (ctx, next) => {
1261
+ const startedAt = Date.now();
963
1262
  try {
964
1263
  await next();
965
1264
  } catch (error) {
@@ -973,10 +1272,24 @@ async function createApp() {
973
1272
  message: error instanceof Error ? error.message : "Internal error"
974
1273
  }
975
1274
  };
1275
+ void logger.write(status >= 500 ? "error" : "warn", "http_request_failed", {
1276
+ method: ctx.method,
1277
+ path: ctx.path,
1278
+ status,
1279
+ code: isLinkHttpError(error) ? error.code : status === 400 ? "invalid_profile_name" : "internal_error",
1280
+ error: error instanceof Error ? error.message : String(error)
1281
+ });
1282
+ } finally {
1283
+ void logger.info("http_request", {
1284
+ method: ctx.method,
1285
+ path: ctx.path,
1286
+ status: ctx.status,
1287
+ duration_ms: Date.now() - startedAt
1288
+ });
976
1289
  }
977
1290
  });
978
1291
  router.get("/api/v1/bootstrap", async (ctx) => {
979
- const [identity, config] = await Promise.all([loadIdentity(), loadConfig()]);
1292
+ const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
980
1293
  const routes = identity?.link_id ? await discoverRouteCandidates({
981
1294
  port: config.port,
982
1295
  relayBaseUrl: config.relayBaseUrl,
@@ -998,7 +1311,8 @@ async function createApp() {
998
1311
  runs: true,
999
1312
  sse: true,
1000
1313
  relay: true,
1001
- profiles: true
1314
+ profiles: true,
1315
+ logs: true
1002
1316
  }
1003
1317
  };
1004
1318
  });
@@ -1009,16 +1323,28 @@ async function createApp() {
1009
1323
  if (!sessionId || !claimToken) {
1010
1324
  throw new LinkHttpError(400, "pairing_claim_invalid", "session_id and claim_token are required");
1011
1325
  }
1012
- ctx.body = await claimPairing({
1326
+ const claimed = await claimPairing({
1013
1327
  sessionId,
1014
1328
  claimToken,
1015
1329
  deviceLabel: readString2(body, "device_label") ?? readString2(body, "deviceLabel") ?? "HermesPilot App",
1016
- devicePlatform: readString2(body, "device_platform") ?? readString2(body, "devicePlatform") ?? "unknown"
1330
+ devicePlatform: readString2(body, "device_platform") ?? readString2(body, "devicePlatform") ?? "unknown",
1331
+ paths
1332
+ });
1333
+ ctx.body = claimed;
1334
+ void logger.info("pairing_claimed", {
1335
+ device_id: claimed.device.device_id,
1336
+ device_platform: claimed.device.platform
1017
1337
  });
1338
+ if (options.onPairingClaimed) {
1339
+ const timer = setTimeout(() => {
1340
+ void options.onPairingClaimed?.();
1341
+ }, 250);
1342
+ timer.unref?.();
1343
+ }
1018
1344
  });
1019
1345
  router.get("/api/v1/auth/me", async (ctx) => {
1020
- const auth = await authenticateRequest(ctx);
1021
- const identity = await loadRequiredIdentity2();
1346
+ const auth = await authenticateRequest(ctx, paths);
1347
+ const identity = await loadRequiredIdentity2(paths);
1022
1348
  ctx.body = {
1023
1349
  ok: true,
1024
1350
  auth: { kind: auth.kind, account_id: auth.accountId ?? null },
@@ -1041,7 +1367,7 @@ async function createApp() {
1041
1367
  if (!refreshToken) {
1042
1368
  throw new LinkHttpError(400, "refresh_token_required", "refresh_token is required");
1043
1369
  }
1044
- const session = await refreshDeviceSession(refreshToken);
1370
+ const session = await refreshDeviceSession(refreshToken, paths);
1045
1371
  ctx.body = {
1046
1372
  ok: true,
1047
1373
  device: session.device,
@@ -1059,13 +1385,13 @@ async function createApp() {
1059
1385
  const body = await readJsonBody(ctx.req);
1060
1386
  const refreshToken = readString2(body, "refresh_token") ?? readString2(body, "refreshToken");
1061
1387
  if (refreshToken) {
1062
- await revokeDeviceRefreshToken(refreshToken);
1388
+ await revokeDeviceRefreshToken(refreshToken, paths);
1063
1389
  }
1064
1390
  ctx.body = { ok: true };
1065
1391
  });
1066
1392
  router.get("/api/v1/status", async (ctx) => {
1067
- await authenticateRequest(ctx);
1068
- const [identity, config] = await Promise.all([loadIdentity(), loadConfig()]);
1393
+ await authenticateRequest(ctx, paths);
1394
+ const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
1069
1395
  ctx.body = {
1070
1396
  ok: true,
1071
1397
  version: LINK_VERSION,
@@ -1074,12 +1400,23 @@ async function createApp() {
1074
1400
  port: config.port
1075
1401
  };
1076
1402
  });
1403
+ router.get("/api/v1/logs", async (ctx) => {
1404
+ await authenticateRequest(ctx, paths);
1405
+ ctx.set("cache-control", "no-store");
1406
+ ctx.body = {
1407
+ ok: true,
1408
+ logs: await readRecentLogEntries({
1409
+ paths,
1410
+ limit: readLimit(ctx.query.limit)
1411
+ })
1412
+ };
1413
+ });
1077
1414
  router.get("/api/v1/models", async (ctx) => {
1078
- await authenticateRequest(ctx);
1415
+ await authenticateRequest(ctx, paths);
1079
1416
  ctx.body = await listHermesModels();
1080
1417
  });
1081
1418
  router.post("/api/v1/runs", async (ctx) => {
1082
- await authenticateRequest(ctx);
1419
+ await authenticateRequest(ctx, paths);
1083
1420
  const body = await readJsonBody(ctx.req);
1084
1421
  const input = readString2(body, "input");
1085
1422
  if (!input) {
@@ -1093,7 +1430,7 @@ async function createApp() {
1093
1430
  });
1094
1431
  });
1095
1432
  router.get("/api/v1/runs/:runId/events", async (ctx) => {
1096
- await authenticateRequest(ctx);
1433
+ await authenticateRequest(ctx, paths);
1097
1434
  const response = await streamHermesRunEvents(ctx.params.runId);
1098
1435
  ctx.status = response.status;
1099
1436
  for (const [key, value] of response.headers.entries()) {
@@ -1112,12 +1449,12 @@ async function createApp() {
1112
1449
  }
1113
1450
  });
1114
1451
  router.post("/api/v1/runs/:runId/cancel", async (ctx) => {
1115
- await authenticateRequest(ctx);
1452
+ await authenticateRequest(ctx, paths);
1116
1453
  await cancelHermesRun(ctx.params.runId);
1117
1454
  ctx.body = { ok: true };
1118
1455
  });
1119
1456
  router.get("/api/v1/profiles", async (ctx) => {
1120
- await authenticateRequest(ctx);
1457
+ await authenticateRequest(ctx, paths);
1121
1458
  ctx.set("cache-control", "no-store");
1122
1459
  ctx.body = {
1123
1460
  ok: true,
@@ -1125,7 +1462,7 @@ async function createApp() {
1125
1462
  };
1126
1463
  });
1127
1464
  router.get("/api/v1/profiles/:name/status", async (ctx) => {
1128
- await authenticateRequest(ctx);
1465
+ await authenticateRequest(ctx, paths);
1129
1466
  ctx.set("cache-control", "no-store");
1130
1467
  ctx.body = {
1131
1468
  ok: true,
@@ -1133,7 +1470,7 @@ async function createApp() {
1133
1470
  };
1134
1471
  });
1135
1472
  router.post("/api/v1/profiles", async (ctx) => {
1136
- await authenticateRequest(ctx);
1473
+ await authenticateRequest(ctx, paths);
1137
1474
  const body = await readJsonBody(ctx.req);
1138
1475
  const name = readProfileName(body);
1139
1476
  ctx.status = 201;
@@ -1143,14 +1480,14 @@ async function createApp() {
1143
1480
  };
1144
1481
  });
1145
1482
  router.post("/api/v1/profiles/:name/use", async (ctx) => {
1146
- await authenticateRequest(ctx);
1483
+ await authenticateRequest(ctx, paths);
1147
1484
  ctx.body = {
1148
1485
  ok: true,
1149
1486
  profile: await useHermesProfile(ctx.params.name)
1150
1487
  };
1151
1488
  });
1152
1489
  router.patch("/api/v1/profiles/:name", async (ctx) => {
1153
- await authenticateRequest(ctx);
1490
+ await authenticateRequest(ctx, paths);
1154
1491
  const body = await readJsonBody(ctx.req);
1155
1492
  const name = readProfileName(body);
1156
1493
  ctx.body = {
@@ -1159,7 +1496,7 @@ async function createApp() {
1159
1496
  };
1160
1497
  });
1161
1498
  router.delete("/api/v1/profiles/:name", async (ctx) => {
1162
- await authenticateRequest(ctx);
1499
+ await authenticateRequest(ctx, paths);
1163
1500
  await deleteHermesProfile(ctx.params.name);
1164
1501
  ctx.status = 204;
1165
1502
  });
@@ -1183,24 +1520,24 @@ function readProfileName(body) {
1183
1520
  }
1184
1521
  return body.name;
1185
1522
  }
1186
- async function authenticateRequest(ctx) {
1523
+ async function authenticateRequest(ctx, paths) {
1187
1524
  const token = readBearerToken(ctx.get("authorization"));
1188
1525
  if (!token) {
1189
1526
  throw new LinkHttpError(401, "auth_required", "Authorization bearer token is required");
1190
1527
  }
1191
- const device = await authenticateDeviceAccessToken(token);
1528
+ const device = await authenticateDeviceAccessToken(token, paths);
1192
1529
  if (device) {
1193
1530
  return { kind: "device", device };
1194
1531
  }
1195
- const [identity, config] = await Promise.all([loadRequiredIdentity2(), loadConfig()]);
1532
+ const [identity, config] = await Promise.all([loadRequiredIdentity2(paths), loadConfig(paths)]);
1196
1533
  const claims = await verifyAppConnectToken(token, {
1197
1534
  config,
1198
1535
  linkId: identity.link_id
1199
1536
  });
1200
1537
  return { kind: "app-connect", accountId: claims.sub };
1201
1538
  }
1202
- async function loadRequiredIdentity2() {
1203
- const identity = await loadIdentity();
1539
+ async function loadRequiredIdentity2(paths) {
1540
+ const identity = await loadIdentity(paths);
1204
1541
  if (!identity?.link_id) {
1205
1542
  throw new LinkHttpError(409, "link_not_paired", "Hermes Link is not paired");
1206
1543
  }
@@ -1218,6 +1555,14 @@ function readString2(body, key) {
1218
1555
  const value = body[key];
1219
1556
  return typeof value === "string" && value.trim() ? value.trim() : null;
1220
1557
  }
1558
+ function readLimit(value) {
1559
+ const raw = Array.isArray(value) ? value[0] : value;
1560
+ if (typeof raw !== "string") {
1561
+ return void 0;
1562
+ }
1563
+ const parsed = Number.parseInt(raw, 10);
1564
+ return Number.isFinite(parsed) ? parsed : void 0;
1565
+ }
1221
1566
  function readConversationHistory(value) {
1222
1567
  if (!Array.isArray(value)) {
1223
1568
  return [];
@@ -1252,6 +1597,8 @@ export {
1252
1597
  ensureIdentity,
1253
1598
  getIdentityStatus,
1254
1599
  preparePairing,
1600
+ createFileLogger,
1601
+ getLinkLogFile,
1255
1602
  createApp
1256
1603
  };
1257
- //# sourceMappingURL=chunk-4CDHEW3J.js.map
1604
+ //# sourceMappingURL=chunk-7M3UZCA7.js.map