@hermespilot/link 0.1.2 → 0.1.4

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.2";
7
+ var LINK_VERSION = "0.1.4";
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 }));
@@ -930,8 +930,8 @@ async function loadRequiredIdentity(paths) {
930
930
  }
931
931
  return identity;
932
932
  }
933
- async function postServerJson(serverBaseUrl, path5, body) {
934
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path5}`, {
933
+ async function postServerJson(serverBaseUrl, path6, body) {
934
+ const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path6}`, {
935
935
  method: "POST",
936
936
  headers: {
937
937
  accept: "application/json",
@@ -941,8 +941,8 @@ async function postServerJson(serverBaseUrl, path5, body) {
941
941
  });
942
942
  return readJsonResponse2(response);
943
943
  }
944
- async function patchServerJson(serverBaseUrl, path5, token, body) {
945
- 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}`, {
946
946
  method: "PATCH",
947
947
  headers: {
948
948
  accept: "application/json",
@@ -1048,11 +1048,217 @@ function base64UrlToBase64(value) {
1048
1048
  return normalized + "=".repeat((4 - normalized.length % 4) % 4);
1049
1049
  }
1050
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
+
1051
1254
  // src/http/app.ts
1052
- async function createApp() {
1255
+ async function createApp(options = {}) {
1256
+ const paths = options.paths ?? resolveRuntimePaths();
1257
+ const logger = options.logger ?? createFileLogger({ paths });
1053
1258
  const app = new Koa();
1054
1259
  const router = new Router();
1055
1260
  app.use(async (ctx, next) => {
1261
+ const startedAt = Date.now();
1056
1262
  try {
1057
1263
  await next();
1058
1264
  } catch (error) {
@@ -1066,10 +1272,24 @@ async function createApp() {
1066
1272
  message: error instanceof Error ? error.message : "Internal error"
1067
1273
  }
1068
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
+ });
1069
1289
  }
1070
1290
  });
1071
1291
  router.get("/api/v1/bootstrap", async (ctx) => {
1072
- const [identity, config] = await Promise.all([loadIdentity(), loadConfig()]);
1292
+ const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
1073
1293
  const routes = identity?.link_id ? await discoverRouteCandidates({
1074
1294
  port: config.port,
1075
1295
  relayBaseUrl: config.relayBaseUrl,
@@ -1091,7 +1311,8 @@ async function createApp() {
1091
1311
  runs: true,
1092
1312
  sse: true,
1093
1313
  relay: true,
1094
- profiles: true
1314
+ profiles: true,
1315
+ logs: true
1095
1316
  }
1096
1317
  };
1097
1318
  });
@@ -1102,16 +1323,28 @@ async function createApp() {
1102
1323
  if (!sessionId || !claimToken) {
1103
1324
  throw new LinkHttpError(400, "pairing_claim_invalid", "session_id and claim_token are required");
1104
1325
  }
1105
- ctx.body = await claimPairing({
1326
+ const claimed = await claimPairing({
1106
1327
  sessionId,
1107
1328
  claimToken,
1108
1329
  deviceLabel: readString2(body, "device_label") ?? readString2(body, "deviceLabel") ?? "HermesPilot App",
1109
- devicePlatform: readString2(body, "device_platform") ?? readString2(body, "devicePlatform") ?? "unknown"
1330
+ devicePlatform: readString2(body, "device_platform") ?? readString2(body, "devicePlatform") ?? "unknown",
1331
+ paths
1110
1332
  });
1333
+ ctx.body = claimed;
1334
+ void logger.info("pairing_claimed", {
1335
+ device_id: claimed.device.device_id,
1336
+ device_platform: claimed.device.platform
1337
+ });
1338
+ if (options.onPairingClaimed) {
1339
+ const timer = setTimeout(() => {
1340
+ void options.onPairingClaimed?.();
1341
+ }, 250);
1342
+ timer.unref?.();
1343
+ }
1111
1344
  });
1112
1345
  router.get("/api/v1/auth/me", async (ctx) => {
1113
- const auth = await authenticateRequest(ctx);
1114
- const identity = await loadRequiredIdentity2();
1346
+ const auth = await authenticateRequest(ctx, paths);
1347
+ const identity = await loadRequiredIdentity2(paths);
1115
1348
  ctx.body = {
1116
1349
  ok: true,
1117
1350
  auth: { kind: auth.kind, account_id: auth.accountId ?? null },
@@ -1134,7 +1367,7 @@ async function createApp() {
1134
1367
  if (!refreshToken) {
1135
1368
  throw new LinkHttpError(400, "refresh_token_required", "refresh_token is required");
1136
1369
  }
1137
- const session = await refreshDeviceSession(refreshToken);
1370
+ const session = await refreshDeviceSession(refreshToken, paths);
1138
1371
  ctx.body = {
1139
1372
  ok: true,
1140
1373
  device: session.device,
@@ -1152,13 +1385,13 @@ async function createApp() {
1152
1385
  const body = await readJsonBody(ctx.req);
1153
1386
  const refreshToken = readString2(body, "refresh_token") ?? readString2(body, "refreshToken");
1154
1387
  if (refreshToken) {
1155
- await revokeDeviceRefreshToken(refreshToken);
1388
+ await revokeDeviceRefreshToken(refreshToken, paths);
1156
1389
  }
1157
1390
  ctx.body = { ok: true };
1158
1391
  });
1159
1392
  router.get("/api/v1/status", async (ctx) => {
1160
- await authenticateRequest(ctx);
1161
- const [identity, config] = await Promise.all([loadIdentity(), loadConfig()]);
1393
+ await authenticateRequest(ctx, paths);
1394
+ const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
1162
1395
  ctx.body = {
1163
1396
  ok: true,
1164
1397
  version: LINK_VERSION,
@@ -1167,12 +1400,23 @@ async function createApp() {
1167
1400
  port: config.port
1168
1401
  };
1169
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
+ });
1170
1414
  router.get("/api/v1/models", async (ctx) => {
1171
- await authenticateRequest(ctx);
1415
+ await authenticateRequest(ctx, paths);
1172
1416
  ctx.body = await listHermesModels();
1173
1417
  });
1174
1418
  router.post("/api/v1/runs", async (ctx) => {
1175
- await authenticateRequest(ctx);
1419
+ await authenticateRequest(ctx, paths);
1176
1420
  const body = await readJsonBody(ctx.req);
1177
1421
  const input = readString2(body, "input");
1178
1422
  if (!input) {
@@ -1186,7 +1430,7 @@ async function createApp() {
1186
1430
  });
1187
1431
  });
1188
1432
  router.get("/api/v1/runs/:runId/events", async (ctx) => {
1189
- await authenticateRequest(ctx);
1433
+ await authenticateRequest(ctx, paths);
1190
1434
  const response = await streamHermesRunEvents(ctx.params.runId);
1191
1435
  ctx.status = response.status;
1192
1436
  for (const [key, value] of response.headers.entries()) {
@@ -1205,12 +1449,12 @@ async function createApp() {
1205
1449
  }
1206
1450
  });
1207
1451
  router.post("/api/v1/runs/:runId/cancel", async (ctx) => {
1208
- await authenticateRequest(ctx);
1452
+ await authenticateRequest(ctx, paths);
1209
1453
  await cancelHermesRun(ctx.params.runId);
1210
1454
  ctx.body = { ok: true };
1211
1455
  });
1212
1456
  router.get("/api/v1/profiles", async (ctx) => {
1213
- await authenticateRequest(ctx);
1457
+ await authenticateRequest(ctx, paths);
1214
1458
  ctx.set("cache-control", "no-store");
1215
1459
  ctx.body = {
1216
1460
  ok: true,
@@ -1218,7 +1462,7 @@ async function createApp() {
1218
1462
  };
1219
1463
  });
1220
1464
  router.get("/api/v1/profiles/:name/status", async (ctx) => {
1221
- await authenticateRequest(ctx);
1465
+ await authenticateRequest(ctx, paths);
1222
1466
  ctx.set("cache-control", "no-store");
1223
1467
  ctx.body = {
1224
1468
  ok: true,
@@ -1226,7 +1470,7 @@ async function createApp() {
1226
1470
  };
1227
1471
  });
1228
1472
  router.post("/api/v1/profiles", async (ctx) => {
1229
- await authenticateRequest(ctx);
1473
+ await authenticateRequest(ctx, paths);
1230
1474
  const body = await readJsonBody(ctx.req);
1231
1475
  const name = readProfileName(body);
1232
1476
  ctx.status = 201;
@@ -1236,14 +1480,14 @@ async function createApp() {
1236
1480
  };
1237
1481
  });
1238
1482
  router.post("/api/v1/profiles/:name/use", async (ctx) => {
1239
- await authenticateRequest(ctx);
1483
+ await authenticateRequest(ctx, paths);
1240
1484
  ctx.body = {
1241
1485
  ok: true,
1242
1486
  profile: await useHermesProfile(ctx.params.name)
1243
1487
  };
1244
1488
  });
1245
1489
  router.patch("/api/v1/profiles/:name", async (ctx) => {
1246
- await authenticateRequest(ctx);
1490
+ await authenticateRequest(ctx, paths);
1247
1491
  const body = await readJsonBody(ctx.req);
1248
1492
  const name = readProfileName(body);
1249
1493
  ctx.body = {
@@ -1252,7 +1496,7 @@ async function createApp() {
1252
1496
  };
1253
1497
  });
1254
1498
  router.delete("/api/v1/profiles/:name", async (ctx) => {
1255
- await authenticateRequest(ctx);
1499
+ await authenticateRequest(ctx, paths);
1256
1500
  await deleteHermesProfile(ctx.params.name);
1257
1501
  ctx.status = 204;
1258
1502
  });
@@ -1276,24 +1520,24 @@ function readProfileName(body) {
1276
1520
  }
1277
1521
  return body.name;
1278
1522
  }
1279
- async function authenticateRequest(ctx) {
1523
+ async function authenticateRequest(ctx, paths) {
1280
1524
  const token = readBearerToken(ctx.get("authorization"));
1281
1525
  if (!token) {
1282
1526
  throw new LinkHttpError(401, "auth_required", "Authorization bearer token is required");
1283
1527
  }
1284
- const device = await authenticateDeviceAccessToken(token);
1528
+ const device = await authenticateDeviceAccessToken(token, paths);
1285
1529
  if (device) {
1286
1530
  return { kind: "device", device };
1287
1531
  }
1288
- const [identity, config] = await Promise.all([loadRequiredIdentity2(), loadConfig()]);
1532
+ const [identity, config] = await Promise.all([loadRequiredIdentity2(paths), loadConfig(paths)]);
1289
1533
  const claims = await verifyAppConnectToken(token, {
1290
1534
  config,
1291
1535
  linkId: identity.link_id
1292
1536
  });
1293
1537
  return { kind: "app-connect", accountId: claims.sub };
1294
1538
  }
1295
- async function loadRequiredIdentity2() {
1296
- const identity = await loadIdentity();
1539
+ async function loadRequiredIdentity2(paths) {
1540
+ const identity = await loadIdentity(paths);
1297
1541
  if (!identity?.link_id) {
1298
1542
  throw new LinkHttpError(409, "link_not_paired", "Hermes Link is not paired");
1299
1543
  }
@@ -1311,6 +1555,14 @@ function readString2(body, key) {
1311
1555
  const value = body[key];
1312
1556
  return typeof value === "string" && value.trim() ? value.trim() : null;
1313
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
+ }
1314
1566
  function readConversationHistory(value) {
1315
1567
  if (!Array.isArray(value)) {
1316
1568
  return [];
@@ -1345,6 +1597,8 @@ export {
1345
1597
  ensureIdentity,
1346
1598
  getIdentityStatus,
1347
1599
  preparePairing,
1600
+ createFileLogger,
1601
+ getLinkLogFile,
1348
1602
  createApp
1349
1603
  };
1350
- //# sourceMappingURL=chunk-E2BRK5JT.js.map
1604
+ //# sourceMappingURL=chunk-T35GPRKF.js.map