@hermespilot/link 0.4.3 → 0.4.5

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.
@@ -3602,17 +3602,6 @@ function isEnvValueConfigured(value) {
3602
3602
  }
3603
3603
  async function readHermesApiServerEnvOverrides(profileName) {
3604
3604
  const values = await readHermesEnvFile(profileName);
3605
- for (const key of [
3606
- "API_SERVER_ENABLED",
3607
- "API_SERVER_HOST",
3608
- "API_SERVER_PORT",
3609
- "API_SERVER_KEY"
3610
- ]) {
3611
- const value = process.env[key];
3612
- if (typeof value === "string" && value.trim()) {
3613
- values[key] = value;
3614
- }
3615
- }
3616
3605
  const port = Number.parseInt(values.API_SERVER_PORT ?? "", 10);
3617
3606
  return {
3618
3607
  enabled: parseEnvBoolean(values.API_SERVER_ENABLED),
@@ -3996,7 +3985,7 @@ import os2 from "os";
3996
3985
  import path5 from "path";
3997
3986
 
3998
3987
  // src/constants.ts
3999
- var LINK_VERSION = "0.4.3";
3988
+ var LINK_VERSION = "0.4.5";
4000
3989
  var LINK_COMMAND = "hermeslink";
4001
3990
  var LINK_DEFAULT_PORT = 52379;
4002
3991
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4496,6 +4485,7 @@ var DASHBOARD_STATUS_URL = "http://127.0.0.1:9119/api/status";
4496
4485
  var DASHBOARD_STATUS_TIMEOUT_MS = 1500;
4497
4486
  var DEFAULT_VERSION_CACHE_TTL_MS = 6e4;
4498
4487
  var MAX_VERSION_LOG_OUTPUT_LENGTH = 1200;
4488
+ var HERMES_GATEWAY_ENV_BLOCKLIST_PREFIXES = ["API_SERVER_"];
4499
4489
  var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
4500
4490
  var hermesVersionCache = /* @__PURE__ */ new Map();
4501
4491
  var hermesVersionInFlight = /* @__PURE__ */ new Map();
@@ -4720,7 +4710,7 @@ async function startHermesGateway(paths, profileName, logger) {
4720
4710
  try {
4721
4711
  const child = spawn(resolveHermesBin(), gatewayRunArgs(profileName), {
4722
4712
  detached: true,
4723
- env: process.env,
4713
+ env: buildHermesGatewayChildEnv(),
4724
4714
  stdio: ["ignore", "pipe", "pipe"],
4725
4715
  windowsHide: true
4726
4716
  });
@@ -4786,6 +4776,7 @@ async function restartHermesGatewayServiceIfAvailable(options) {
4786
4776
  }
4787
4777
  try {
4788
4778
  await execFileAsync2(resolveHermesBin(), ["gateway", "restart"], {
4779
+ env: buildHermesGatewayChildEnv(),
4789
4780
  timeout: options.timeoutMs ?? DEFAULT_START_TIMEOUT_MS,
4790
4781
  windowsHide: true
4791
4782
  });
@@ -4840,6 +4831,17 @@ function gatewayRunArgs(profileName) {
4840
4831
  function formatHermesGatewayRunCommand(profileName) {
4841
4832
  return `${resolveHermesBin()} ${gatewayRunArgs(profileName).join(" ")}`;
4842
4833
  }
4834
+ function buildHermesGatewayChildEnv() {
4835
+ const env = { ...process.env };
4836
+ for (const key of Object.keys(env)) {
4837
+ if (HERMES_GATEWAY_ENV_BLOCKLIST_PREFIXES.some(
4838
+ (prefix) => key.startsWith(prefix)
4839
+ )) {
4840
+ delete env[key];
4841
+ }
4842
+ }
4843
+ return env;
4844
+ }
4843
4845
  async function fileExists(filePath) {
4844
4846
  return access(filePath).then(() => true).catch(() => false);
4845
4847
  }
@@ -4875,6 +4877,10 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
4875
4877
  }
4876
4878
  const issue = describeDetailedHealthIssue(record);
4877
4879
  const terminal = isTerminalDetailedHealth(record);
4880
+ const authIssue = issue ? null : await probeHermesApiServerAuth(resolvedConfig, fetcher);
4881
+ if (authIssue) {
4882
+ return authIssue;
4883
+ }
4878
4884
  return {
4879
4885
  healthy: !issue,
4880
4886
  terminal,
@@ -4911,20 +4917,9 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
4911
4917
  };
4912
4918
  }
4913
4919
  if (resolvedConfig.key) {
4914
- const authProbe = await fetchWithTimeout(
4915
- `http://127.0.0.1:${resolvedConfig.port}/v1/models`,
4916
- {
4917
- method: "GET",
4918
- headers: authHeaders(resolvedConfig)
4919
- },
4920
- fetcher
4921
- );
4922
- if (authProbe?.status === 401) {
4923
- return {
4924
- healthy: false,
4925
- authInvalid: true,
4926
- issue: "Hermes API Server \u8FD4\u56DE 401\uFF0C\u5F53\u524D\u7AEF\u53E3\u53EF\u80FD\u5C5E\u4E8E\u53E6\u4E00\u4E2A Profile\uFF0C\u6216 API Server key \u5DF2\u53D8\u66F4\u3002"
4927
- };
4920
+ const authIssue = await probeHermesApiServerAuth(resolvedConfig, fetcher);
4921
+ if (authIssue) {
4922
+ return authIssue;
4928
4923
  }
4929
4924
  }
4930
4925
  return { healthy: true };
@@ -4941,6 +4936,39 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
4941
4936
  issue: describePortHealthFailure(resolvedConfig.port)
4942
4937
  };
4943
4938
  }
4939
+ async function probeHermesApiServerAuth(config, fetcher) {
4940
+ if (!config.key) {
4941
+ return null;
4942
+ }
4943
+ const response = await fetchWithTimeout(
4944
+ `http://127.0.0.1:${config.port}/v1/models`,
4945
+ {
4946
+ method: "GET",
4947
+ headers: authHeaders(config)
4948
+ },
4949
+ fetcher
4950
+ );
4951
+ if (!response) {
4952
+ return {
4953
+ healthy: false,
4954
+ issue: "Hermes API Server \u9274\u6743\u63A2\u6D4B\u65E0\u54CD\u5E94\uFF1A/v1/models \u6CA1\u6709\u5728\u8D85\u65F6\u65F6\u95F4\u5185\u8FD4\u56DE\u3002"
4955
+ };
4956
+ }
4957
+ if (response?.status === 401) {
4958
+ return {
4959
+ healthy: false,
4960
+ authInvalid: true,
4961
+ issue: "Hermes API Server \u8FD4\u56DE 401\uFF0C\u5F53\u524D\u7AEF\u53E3\u53EF\u80FD\u5C5E\u4E8E\u53E6\u4E00\u4E2A Profile\uFF0C\u6216 API Server key \u5DF2\u53D8\u66F4\u3002"
4962
+ };
4963
+ }
4964
+ if (response && !response.ok) {
4965
+ return {
4966
+ healthy: false,
4967
+ issue: `Hermes API Server \u9274\u6743\u63A2\u6D4B\u5931\u8D25\uFF1A/v1/models \u8FD4\u56DE HTTP ${response.status}\u3002`
4968
+ };
4969
+ }
4970
+ return null;
4971
+ }
4944
4972
  function fetchWithTimeout(input, init, fetcher) {
4945
4973
  const controller = new AbortController();
4946
4974
  const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
@@ -8979,24 +9007,15 @@ var ConversationQueryCoordinator = class {
8979
9007
  async listConversationPage(options = {}) {
8980
9008
  const limit = normalizeConversationListPageLimit(options.limit);
8981
9009
  const cursor = decodeConversationListCursor(options.cursor);
8982
- const indexedPage = await listConversationStatsPage(this.deps.paths, {
9010
+ return this.listIndexedConversationPage({
8983
9011
  limit,
8984
- cursor
8985
- });
8986
- if (indexedPage.records.length === 0) {
8987
- return this.listConversationPageFromStore({ limit, cursor });
8988
- }
8989
- const summaries = await this.summarizeIndexedConversations(
8990
- indexedPage.records
8991
- );
8992
- return {
8993
- conversations: summaries,
8994
- page: {
9012
+ cursor,
9013
+ fallback: () => this.listConversationPageFromStore({ limit, cursor }),
9014
+ listPage: (pageCursor) => listConversationStatsPage(this.deps.paths, {
8995
9015
  limit,
8996
- has_more: indexedPage.hasMore,
8997
- next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
8998
- }
8999
- };
9016
+ cursor: pageCursor
9017
+ })
9018
+ });
9000
9019
  }
9001
9020
  async searchConversationPage(options = {}) {
9002
9021
  const query = normalizeConversationSearchQuery(options.query);
@@ -9005,20 +9024,72 @@ var ConversationQueryCoordinator = class {
9005
9024
  }
9006
9025
  const limit = normalizeConversationListPageLimit(options.limit);
9007
9026
  const cursor = decodeConversationListCursor(options.cursor);
9008
- const indexedPage = await searchConversationStatsPage(this.deps.paths, {
9027
+ return this.listIndexedConversationPage({
9009
9028
  limit,
9010
9029
  cursor,
9011
- query
9030
+ listPage: (pageCursor) => searchConversationStatsPage(this.deps.paths, {
9031
+ limit,
9032
+ cursor: pageCursor,
9033
+ query
9034
+ })
9012
9035
  });
9013
- const summaries = await this.summarizeIndexedConversations(
9014
- indexedPage.records
9015
- );
9036
+ }
9037
+ async listIndexedConversationPage(input) {
9038
+ const collected = [];
9039
+ const seenConversationIds = /* @__PURE__ */ new Set();
9040
+ let scanCursor = input.cursor;
9041
+ let usedIndex = false;
9042
+ while (collected.length <= input.limit) {
9043
+ const indexedPage = await input.listPage(scanCursor);
9044
+ if (indexedPage.records.length === 0) {
9045
+ if (!usedIndex && input.fallback) {
9046
+ return input.fallback();
9047
+ }
9048
+ return this.buildConversationListPage({
9049
+ limit: input.limit,
9050
+ conversations: collected,
9051
+ hasMore: false
9052
+ });
9053
+ }
9054
+ usedIndex = true;
9055
+ const summaries = await this.summarizeIndexedConversations(
9056
+ indexedPage.records
9057
+ );
9058
+ for (const summary of summaries) {
9059
+ if (!seenConversationIds.add(summary.id)) {
9060
+ continue;
9061
+ }
9062
+ collected.push(summary);
9063
+ if (collected.length > input.limit) {
9064
+ break;
9065
+ }
9066
+ }
9067
+ if (collected.length > input.limit) {
9068
+ break;
9069
+ }
9070
+ if (!indexedPage.hasMore) {
9071
+ return this.buildConversationListPage({
9072
+ limit: input.limit,
9073
+ conversations: collected,
9074
+ hasMore: false
9075
+ });
9076
+ }
9077
+ scanCursor = conversationListCursorFromRecord(indexedPage.records.at(-1));
9078
+ }
9079
+ return this.buildConversationListPage({
9080
+ limit: input.limit,
9081
+ conversations: collected,
9082
+ hasMore: true
9083
+ });
9084
+ }
9085
+ buildConversationListPage(input) {
9086
+ const conversations = input.conversations.slice(0, input.limit);
9016
9087
  return {
9017
- conversations: summaries,
9088
+ conversations,
9018
9089
  page: {
9019
- limit,
9020
- has_more: indexedPage.hasMore,
9021
- next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
9090
+ limit: input.limit,
9091
+ has_more: input.hasMore,
9092
+ next_cursor: input.hasMore && conversations.length > 0 ? encodeConversationListCursorFromSummary(conversations.at(-1)) : null
9022
9093
  }
9023
9094
  };
9024
9095
  }
@@ -9177,6 +9248,12 @@ function encodeConversationListCursor(record) {
9177
9248
  "utf8"
9178
9249
  ).toString("base64url");
9179
9250
  }
9251
+ function conversationListCursorFromRecord(record) {
9252
+ return {
9253
+ updatedAt: record.updatedAt,
9254
+ conversationId: record.conversationId
9255
+ };
9256
+ }
9180
9257
  function encodeConversationListCursorFromSummary(summary) {
9181
9258
  return encodeConversationListCursor({
9182
9259
  conversationId: summary.id,
@@ -15305,6 +15382,24 @@ function readHeader(ctx, name) {
15305
15382
  const value = ctx.get(name).trim();
15306
15383
  return value ? value : null;
15307
15384
  }
15385
+ function readUploadFilenameHeader(ctx) {
15386
+ const encoded = readHeader(ctx, "x-filename-base64") ?? readHeader(ctx, "x-filename-b64");
15387
+ if (encoded) {
15388
+ const decoded = decodeBase64Utf8Header(encoded);
15389
+ if (decoded) {
15390
+ return decoded;
15391
+ }
15392
+ }
15393
+ return readHeader(ctx, "x-filename");
15394
+ }
15395
+ function decodeBase64Utf8Header(value) {
15396
+ const trimmed = value.trim();
15397
+ if (!trimmed || !/^[A-Za-z0-9+/]+={0,2}$/u.test(trimmed)) {
15398
+ return null;
15399
+ }
15400
+ const decoded = Buffer.from(trimmed, "base64").toString("utf8").trim();
15401
+ return decoded || null;
15402
+ }
15308
15403
  var CONVERSATION_HISTORY_REPLAY_FIELDS = [
15309
15404
  "tool_call_id",
15310
15405
  "tool_calls",
@@ -15797,7 +15892,7 @@ function registerConversationRoutes(router, options) {
15797
15892
  }
15798
15893
  const blob = await conversations.writeBlob(ctx.params.conversationId, {
15799
15894
  bytes,
15800
- filename: readHeader(ctx, "x-filename") ?? void 0,
15895
+ filename: readUploadFilenameHeader(ctx) ?? void 0,
15801
15896
  mime: ctx.get("content-type") || void 0
15802
15897
  });
15803
15898
  ctx.status = 201;
@@ -20641,6 +20736,8 @@ import { mkdir as mkdir12, rm as rm9, writeFile as writeFile3 } from "fs/promise
20641
20736
 
20642
20737
  // src/relay/control-client.ts
20643
20738
  import WebSocket from "ws";
20739
+ var RELAY_SSE_BATCH_FLUSH_INTERVAL_MS = 50;
20740
+ var RELAY_SSE_BATCH_FLUSH_BYTES = 2 * 1024;
20644
20741
  function connectRelayControl(options) {
20645
20742
  const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
20646
20743
  wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
@@ -20776,6 +20873,7 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
20776
20873
  }
20777
20874
  const abortController = new AbortController();
20778
20875
  abortControllers.set(frame.id, abortController);
20876
+ let sseBatcher = null;
20779
20877
  try {
20780
20878
  const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
20781
20879
  method: frame.method,
@@ -20787,26 +20885,92 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
20787
20885
  const contentType = response.headers.get("content-type") ?? "";
20788
20886
  if (response.body && contentType.includes("text/event-stream")) {
20789
20887
  socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
20888
+ sseBatcher = createRelayStreamChunkBatcher(socket, frame.id);
20790
20889
  const reader = response.body.getReader();
20791
20890
  while (true) {
20792
20891
  const next = await reader.read();
20793
20892
  if (next.done) {
20794
20893
  break;
20795
20894
  }
20796
- socket.send(JSON.stringify({ type: "http.stream.chunk", id: frame.id, bodyBase64: Buffer.from(next.value).toString("base64") }));
20895
+ sseBatcher.push(next.value);
20797
20896
  }
20897
+ sseBatcher.flush();
20798
20898
  socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
20799
20899
  return;
20800
20900
  }
20801
20901
  const body = Buffer.from(await response.arrayBuffer()).toString("base64");
20802
20902
  socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
20803
20903
  } catch (error) {
20904
+ if (abortController.signal.aborted || isAbortError2(error)) {
20905
+ return;
20906
+ }
20907
+ sseBatcher?.flush();
20804
20908
  const message = error instanceof Error ? error.message : "Relay request failed";
20805
20909
  socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
20806
20910
  } finally {
20911
+ sseBatcher?.dispose();
20807
20912
  abortControllers.delete(frame.id);
20808
20913
  }
20809
20914
  }
20915
+ function isAbortError2(error) {
20916
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
20917
+ }
20918
+ function createRelayStreamChunkBatcher(socket, id) {
20919
+ let chunks = [];
20920
+ let totalBytes = 0;
20921
+ let flushTimer = null;
20922
+ const clearFlushTimer = () => {
20923
+ if (flushTimer == null) {
20924
+ return;
20925
+ }
20926
+ clearTimeout(flushTimer);
20927
+ flushTimer = null;
20928
+ };
20929
+ const flush = () => {
20930
+ clearFlushTimer();
20931
+ if (totalBytes <= 0) {
20932
+ return;
20933
+ }
20934
+ const bodyBase64 = Buffer.concat(chunks, totalBytes).toString("base64");
20935
+ chunks = [];
20936
+ totalBytes = 0;
20937
+ if (socket.readyState !== WebSocket.OPEN) {
20938
+ return;
20939
+ }
20940
+ socket.send(JSON.stringify({ type: "http.stream.chunk", id, bodyBase64 }));
20941
+ };
20942
+ const scheduleFlush = () => {
20943
+ if (flushTimer != null) {
20944
+ return;
20945
+ }
20946
+ flushTimer = setTimeout(() => {
20947
+ flushTimer = null;
20948
+ flush();
20949
+ }, RELAY_SSE_BATCH_FLUSH_INTERVAL_MS);
20950
+ flushTimer.unref?.();
20951
+ };
20952
+ return {
20953
+ push(chunk) {
20954
+ if (chunk.byteLength <= 0) {
20955
+ return;
20956
+ }
20957
+ const buffer = Buffer.from(chunk);
20958
+ chunks.push(buffer);
20959
+ totalBytes += buffer.byteLength;
20960
+ if (totalBytes >= RELAY_SSE_BATCH_FLUSH_BYTES) {
20961
+ flush();
20962
+ return;
20963
+ }
20964
+ scheduleFlush();
20965
+ },
20966
+ flush,
20967
+ dispose() {
20968
+ clearFlushTimer();
20969
+ chunks = [];
20970
+ totalBytes = 0;
20971
+ }
20972
+ };
20973
+ }
20810
20974
 
20811
20975
  // src/runtime/system-info.ts
20812
20976
  import { execFileSync } from "child_process";
@@ -20827,8 +20991,8 @@ function readLinkSystemInfo() {
20827
20991
  function buildDefaultDisplayName(input) {
20828
20992
  const hostname = normalizeText(input.hostname);
20829
20993
  const osLabel = normalizeText(input.osLabel);
20830
- if (hostname && osLabel && hostname.toLowerCase() !== osLabel.toLowerCase()) {
20831
- return truncateText(`${hostname} - ${osLabel}`, 128);
20994
+ if (hostname) {
20995
+ return truncateText(hostname, 128);
20832
20996
  }
20833
20997
  return truncateText(hostname ?? osLabel ?? `Hermes Link ${input.platform}`, 128);
20834
20998
  }
@@ -21126,6 +21290,10 @@ function unique(values) {
21126
21290
 
21127
21291
  // src/link/network-report-state.ts
21128
21292
  var DEFAULT_AUTO_DAILY_LIMIT = 20;
21293
+ async function readNetworkReportState(paths) {
21294
+ const state = await readLinkState(paths);
21295
+ return normalizeNetworkReportState(state.networkReport);
21296
+ }
21129
21297
  async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
21130
21298
  const snapshot = normalizeNetworkSnapshot(snapshotInput);
21131
21299
  await updateNetworkReportState(paths, (current) => ({
@@ -21175,6 +21343,20 @@ async function reserveAutomaticNetworkReport(paths, snapshotInput, options = {})
21175
21343
  });
21176
21344
  return reservation;
21177
21345
  }
21346
+ async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
21347
+ const state = await readNetworkReportState(paths);
21348
+ return {
21349
+ ...snapshotInput,
21350
+ publicIpv4s: uniqueStrings([
21351
+ ...snapshotInput.publicIpv4s,
21352
+ ...state.lastReportedPublicIpv4s
21353
+ ]).slice(0, 2),
21354
+ publicIpv6s: uniqueStrings([
21355
+ ...snapshotInput.publicIpv6s,
21356
+ ...state.lastReportedPublicIpv6s
21357
+ ]).slice(0, 2)
21358
+ };
21359
+ }
21178
21360
  async function updateNetworkReportState(paths, update) {
21179
21361
  const state = await readLinkState(paths);
21180
21362
  const next = {
@@ -21263,6 +21445,9 @@ function sameStringList(left, right) {
21263
21445
  }
21264
21446
  return left.every((value, index) => value === right[index]);
21265
21447
  }
21448
+ function uniqueStrings(values) {
21449
+ return [...new Set(values)];
21450
+ }
21266
21451
  function formatUtcDay(date) {
21267
21452
  return date.toISOString().slice(0, 10);
21268
21453
  }
@@ -21274,7 +21459,7 @@ async function reportLinkStatusToServer(options = {}) {
21274
21459
  if (!identity?.link_id) {
21275
21460
  return null;
21276
21461
  }
21277
- const routes = options.routes ?? await discoverRouteCandidates({
21462
+ const discoveredRoutes = options.routes ?? await discoverRouteCandidates({
21278
21463
  port: config.port,
21279
21464
  relayBaseUrl: config.relayBaseUrl,
21280
21465
  linkId: identity.link_id,
@@ -21284,6 +21469,7 @@ async function reportLinkStatusToServer(options = {}) {
21284
21469
  configuredLanHost: config.lanHost,
21285
21470
  fetchImpl: options.fetchImpl
21286
21471
  });
21472
+ const routes = await mergeLastReportedPublicRoutes(paths, discoveredRoutes);
21287
21473
  const systemInfo = readLinkSystemInfo();
21288
21474
  const payload = {
21289
21475
  type: "hermes_link_status_report",
@@ -21375,12 +21561,16 @@ function startLanIpMonitor(options) {
21375
21561
  running = false;
21376
21562
  }
21377
21563
  };
21378
- current = check({ forceReport: true, publishToRelay: true });
21564
+ current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
21379
21565
  const timer = setInterval(() => {
21380
- current = check();
21566
+ current = check({ observePublicRoute: false });
21381
21567
  }, options.intervalMs ?? DEFAULT_INTERVAL_MS);
21382
21568
  timer.unref?.();
21383
21569
  return {
21570
+ async refreshPublicRoutes() {
21571
+ current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
21572
+ await current;
21573
+ },
21384
21574
  async close() {
21385
21575
  closed = true;
21386
21576
  clearInterval(timer);
@@ -21396,16 +21586,17 @@ async function checkLanIpChange(options, context = {}) {
21396
21586
  if (!identity?.link_id) {
21397
21587
  return;
21398
21588
  }
21399
- const routes = await discoverRouteCandidates({
21589
+ const discoveredRoutes = await discoverRouteCandidates({
21400
21590
  port: config.port,
21401
21591
  relayBaseUrl: config.relayBaseUrl,
21402
21592
  linkId: identity.link_id,
21403
21593
  installId: identity.install_id,
21404
21594
  publicKeyPem: identity.public_key_pem,
21405
- observePublicRoute: true,
21595
+ observePublicRoute: context.observePublicRoute === true,
21406
21596
  configuredLanHost: config.lanHost,
21407
21597
  fetchImpl: options.fetchImpl
21408
21598
  });
21599
+ const routes = await mergeLastReportedPublicRoutes(options.paths, discoveredRoutes);
21409
21600
  if (context.publishToRelay) {
21410
21601
  options.onNetworkRoutes?.(routes);
21411
21602
  }
@@ -21515,6 +21706,7 @@ function startHermesSessionSyncScheduler(options) {
21515
21706
 
21516
21707
  // src/daemon/service.ts
21517
21708
  var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
21709
+ var RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS = 15 * 6e4;
21518
21710
  async function startLinkService(options = {}) {
21519
21711
  const paths = options.paths ?? resolveRuntimePaths();
21520
21712
  const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
@@ -21585,6 +21777,9 @@ async function startLinkService(options = {}) {
21585
21777
  logger
21586
21778
  });
21587
21779
  let relay = null;
21780
+ let lanIpMonitor = null;
21781
+ let hasSeenRelayConnected = false;
21782
+ let lastRelayReconnectPublicRouteRefreshAt = 0;
21588
21783
  if (identity?.link_id) {
21589
21784
  let resolveRelayReady = null;
21590
21785
  const relayReady = new Promise((resolve) => {
@@ -21600,6 +21795,12 @@ async function startLinkService(options = {}) {
21600
21795
  onStatus: (status) => {
21601
21796
  void logger.info("relay_status", status);
21602
21797
  if (status.state === "connected") {
21798
+ const now = Date.now();
21799
+ if (hasSeenRelayConnected && lanIpMonitor && now - lastRelayReconnectPublicRouteRefreshAt >= RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS) {
21800
+ lastRelayReconnectPublicRouteRefreshAt = now;
21801
+ void lanIpMonitor.refreshPublicRoutes();
21802
+ }
21803
+ hasSeenRelayConnected = true;
21603
21804
  resolveRelayReady?.(true);
21604
21805
  resolveRelayReady = null;
21605
21806
  } else if (status.state === "failed") {
@@ -21618,7 +21819,7 @@ async function startLinkService(options = {}) {
21618
21819
  } else {
21619
21820
  void logger.info("relay_skipped", { reason: "link_not_paired" });
21620
21821
  }
21621
- const lanIpMonitor = startLanIpMonitor({
21822
+ lanIpMonitor = startLanIpMonitor({
21622
21823
  paths,
21623
21824
  logger,
21624
21825
  intervalMs: options.lanIpMonitorIntervalMs,
@@ -21638,7 +21839,7 @@ async function startLinkService(options = {}) {
21638
21839
  await Promise.all([
21639
21840
  scheduler.close(),
21640
21841
  hermesSessionSyncScheduler.close(),
21641
- lanIpMonitor.close(),
21842
+ lanIpMonitor?.close(),
21642
21843
  hermesSessionSync.catch(() => void 0)
21643
21844
  ]);
21644
21845
  await logger.info("service_stopped");
package/dist/cli/index.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  startDaemonProcess,
37
37
  startLinkService,
38
38
  stopDaemonProcess
39
- } from "../chunk-MFRSQUSE.js";
39
+ } from "../chunk-UANE2YHT.js";
40
40
 
41
41
  // src/cli/index.ts
42
42
  import { Command } from "commander";
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-MFRSQUSE.js";
3
+ } from "../chunk-UANE2YHT.js";
4
4
  export {
5
5
  createApp
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hermespilot/link",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",