@hermespilot/link 0.3.0 → 0.3.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.
@@ -3724,7 +3724,7 @@ import os2 from "os";
3724
3724
  import path5 from "path";
3725
3725
 
3726
3726
  // src/constants.ts
3727
- var LINK_VERSION = "0.3.0";
3727
+ var LINK_VERSION = "0.3.2";
3728
3728
  var LINK_COMMAND = "hermeslink";
3729
3729
  var LINK_DEFAULT_PORT = 52379;
3730
3730
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4091,7 +4091,8 @@ import { execFile } from "child_process";
4091
4091
  import { promisify } from "util";
4092
4092
  var execFileAsync = promisify(execFile);
4093
4093
  async function deleteHermesSession(sessionId, profileName = "default") {
4094
- if (!sessionId.trim()) {
4094
+ const normalizedSessionId = sessionId.trim();
4095
+ if (!normalizedSessionId) {
4095
4096
  throw new LinkHttpError(
4096
4097
  400,
4097
4098
  "hermes_session_id_required",
@@ -4099,15 +4100,31 @@ async function deleteHermesSession(sessionId, profileName = "default") {
4099
4100
  );
4100
4101
  }
4101
4102
  try {
4102
- await execFileAsync(
4103
+ const output = await execFileAsync(
4103
4104
  resolveHermesBin(),
4104
- [...profileArgs(profileName), "sessions", "delete", sessionId, "--yes"],
4105
+ [
4106
+ ...profileArgs(profileName),
4107
+ "sessions",
4108
+ "delete",
4109
+ normalizedSessionId,
4110
+ "--yes"
4111
+ ],
4105
4112
  {
4106
4113
  timeout: 1e4,
4107
4114
  windowsHide: true
4108
4115
  }
4109
4116
  );
4117
+ return {
4118
+ session_id: normalizedSessionId,
4119
+ status: readSessionDeleteStatus(output.stdout, output.stderr)
4120
+ };
4110
4121
  } catch (error) {
4122
+ if (isSessionNotFoundOutput(readExecErrorOutput(error))) {
4123
+ return {
4124
+ session_id: normalizedSessionId,
4125
+ status: "not_found"
4126
+ };
4127
+ }
4111
4128
  throw new LinkHttpError(
4112
4129
  502,
4113
4130
  "hermes_session_delete_failed",
@@ -4158,6 +4175,29 @@ async function renameHermesSession(sessionId, title, profileName = "default") {
4158
4175
  function resolveHermesBin() {
4159
4176
  return process.env.HERMES_BIN?.trim() || "hermes";
4160
4177
  }
4178
+ function readSessionDeleteStatus(stdout, stderr) {
4179
+ const output = `${stdout.toString()}
4180
+ ${stderr.toString()}`;
4181
+ if (isSessionNotFoundOutput(output)) {
4182
+ return "not_found";
4183
+ }
4184
+ if (/deleted session\b/i.test(output)) {
4185
+ return "deleted";
4186
+ }
4187
+ return "unknown";
4188
+ }
4189
+ function isSessionNotFoundOutput(output) {
4190
+ return /\bsession\b[\s\S]*\bnot found\b/i.test(output);
4191
+ }
4192
+ function readExecErrorOutput(error) {
4193
+ if (typeof error !== "object" || error === null) {
4194
+ return "";
4195
+ }
4196
+ const stdout = "stdout" in error && error.stdout != null ? String(error.stdout) : "";
4197
+ const stderr = "stderr" in error && error.stderr != null ? String(error.stderr) : "";
4198
+ return `${stdout}
4199
+ ${stderr}`;
4200
+ }
4161
4201
  function profileArgs(profileName) {
4162
4202
  const normalized = profileName.trim() || "default";
4163
4203
  return normalized === "default" ? [] : ["-p", normalized];
@@ -6327,6 +6367,51 @@ function safePathSegment(value, fallback) {
6327
6367
  return safe.length > 0 ? safe.slice(0, 120) : fallback;
6328
6368
  }
6329
6369
 
6370
+ // src/conversations/conversation-session-ids.ts
6371
+ function normalizeHermesSessionIds(values) {
6372
+ const seen = /* @__PURE__ */ new Set();
6373
+ for (const value of values) {
6374
+ const sessionId = value?.trim();
6375
+ if (!sessionId || seen.has(sessionId)) {
6376
+ continue;
6377
+ }
6378
+ seen.add(sessionId);
6379
+ }
6380
+ return [...seen];
6381
+ }
6382
+ function addHermesSessionIdToManifest(manifest, sessionId) {
6383
+ const normalizedSessionId = sessionId.trim();
6384
+ if (!normalizedSessionId) {
6385
+ return manifest;
6386
+ }
6387
+ const hermesSessionIds = normalizeHermesSessionIds([
6388
+ ...manifest.hermes_session_ids ?? [],
6389
+ manifest.hermes_session_id,
6390
+ normalizedSessionId
6391
+ ]);
6392
+ if (manifest.hermes_session_id === normalizedSessionId && arraysEqual(manifest.hermes_session_ids ?? [], hermesSessionIds)) {
6393
+ return manifest;
6394
+ }
6395
+ return {
6396
+ ...manifest,
6397
+ hermes_session_id: normalizedSessionId,
6398
+ hermes_session_ids: hermesSessionIds
6399
+ };
6400
+ }
6401
+ function collectHermesSessionIds(manifest, snapshot) {
6402
+ return normalizeHermesSessionIds([
6403
+ manifest.hermes_session_id,
6404
+ ...manifest.hermes_session_ids ?? [],
6405
+ ...snapshot.runs.map((run) => run.hermes_session_id)
6406
+ ]);
6407
+ }
6408
+ function arraysEqual(left, right) {
6409
+ if (left.length !== right.length) {
6410
+ return false;
6411
+ }
6412
+ return left.every((item, index) => item === right[index]);
6413
+ }
6414
+
6330
6415
  // src/conversations/conversation-maintenance.ts
6331
6416
  var MAX_UPLOADED_BLOB_BYTES = 50 * 1024 * 1024;
6332
6417
  var ConversationMaintenanceCoordinator = class {
@@ -6447,10 +6532,18 @@ var ConversationMaintenanceCoordinator = class {
6447
6532
  ...collectBlobIds(snapshot),
6448
6533
  ...await this.listConversationBlobIds(conversationId)
6449
6534
  ]);
6450
- await deleteHermesSession(
6451
- manifest.hermes_session_id,
6535
+ const hermesSessionIds = collectHermesSessionIds(manifest, snapshot);
6536
+ const hermesDeleteResults = await this.deleteHermesSessions(
6537
+ hermesSessionIds,
6452
6538
  manifest.profile_name_snapshot ?? manifest.profile ?? "default"
6453
6539
  );
6540
+ if (snapshot.runs.length > 0 && hermesDeleteResults.every((result) => result.status === "not_found")) {
6541
+ throw new LinkHttpError(
6542
+ 502,
6543
+ "hermes_session_delete_not_confirmed",
6544
+ "Hermes session deletion was not confirmed"
6545
+ );
6546
+ }
6454
6547
  const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
6455
6548
  const stats = buildConversationStats(
6456
6549
  {
@@ -6464,6 +6557,7 @@ var ConversationMaintenanceCoordinator = class {
6464
6557
  const next = {
6465
6558
  ...manifest,
6466
6559
  status: "deleted_soft",
6560
+ hermes_session_ids: hermesSessionIds,
6467
6561
  updated_at: deletedAt,
6468
6562
  deleted_at: deletedAt,
6469
6563
  stats
@@ -6473,7 +6567,9 @@ var ConversationMaintenanceCoordinator = class {
6473
6567
  type: "conversation.deleted",
6474
6568
  payload: {
6475
6569
  deleted_at: deletedAt,
6476
- hermes_session_id: manifest.hermes_session_id
6570
+ hermes_session_id: manifest.hermes_session_id,
6571
+ hermes_session_ids: hermesSessionIds,
6572
+ hermes_delete_results: hermesDeleteResults
6477
6573
  }
6478
6574
  });
6479
6575
  await this.deps.store.writeSnapshot(conversationId, emptySnapshot2());
@@ -6491,9 +6587,17 @@ var ConversationMaintenanceCoordinator = class {
6491
6587
  return {
6492
6588
  conversation_id: conversationId,
6493
6589
  hermes_deleted: true,
6590
+ hermes_session_ids: hermesSessionIds,
6494
6591
  deleted_at: deletedAt
6495
6592
  };
6496
6593
  }
6594
+ async deleteHermesSessions(sessionIds, profileName) {
6595
+ const results = [];
6596
+ for (const sessionId of sessionIds) {
6597
+ results.push(await deleteHermesSession(sessionId, profileName));
6598
+ }
6599
+ return results;
6600
+ }
6497
6601
  async pruneConversationBlobReferences(conversationId, blobIds) {
6498
6602
  for (const blobId of blobIds) {
6499
6603
  try {
@@ -7640,8 +7744,12 @@ var ConversationOrchestrationCoordinator = class {
7640
7744
  const resetAt = (/* @__PURE__ */ new Date()).toISOString();
7641
7745
  const previousSessionId = input.manifest.hermes_session_id;
7642
7746
  const nextSessionId = freshHermesSessionId(input.manifest.id);
7747
+ const nextManifest = addHermesSessionIdToManifest(
7748
+ input.manifest,
7749
+ nextSessionId
7750
+ );
7643
7751
  await this.deps.store.writeManifest({
7644
- ...input.manifest,
7752
+ ...nextManifest,
7645
7753
  hermes_session_id: nextSessionId,
7646
7754
  command_state: {
7647
7755
  ...input.manifest.command_state,
@@ -9787,9 +9895,11 @@ var ConversationRunLifecycle = class {
9787
9895
  this.deps.paths,
9788
9896
  run.profile
9789
9897
  ).catch(() => void 0) ?? run.hermes_session_id;
9790
- await this.updateRun(conversationId, runId, {
9791
- hermes_session_id: hermesSessionId
9792
- });
9898
+ await this.rememberRunHermesSessionId(
9899
+ conversationId,
9900
+ runId,
9901
+ hermesSessionId
9902
+ );
9793
9903
  const conversationHistory = await buildConversationHistory({
9794
9904
  paths: this.deps.paths,
9795
9905
  profileName: run.profile,
@@ -9859,9 +9969,11 @@ var ConversationRunLifecycle = class {
9859
9969
  );
9860
9970
  const responseSessionId = response.headers.get("x-hermes-session-id")?.trim();
9861
9971
  if (responseSessionId) {
9862
- await this.updateRun(conversationId, runId, {
9863
- hermes_session_id: responseSessionId
9864
- });
9972
+ await this.rememberRunHermesSessionId(
9973
+ conversationId,
9974
+ runId,
9975
+ responseSessionId
9976
+ );
9865
9977
  }
9866
9978
  for await (const rawEvent of parseSseResponse(response)) {
9867
9979
  if (controller.signal.aborted) {
@@ -10047,6 +10159,29 @@ ${attachmentLines.join("\n")}`
10047
10159
  Object.assign(run, patch);
10048
10160
  await this.deps.writeSnapshot(conversationId, snapshot);
10049
10161
  }
10162
+ async rememberRunHermesSessionId(conversationId, runId, sessionId) {
10163
+ const normalizedSessionId = sessionId.trim();
10164
+ if (!normalizedSessionId) {
10165
+ return;
10166
+ }
10167
+ return this.deps.withConversationLock(conversationId, async () => {
10168
+ const snapshot = await this.deps.readSnapshot(conversationId);
10169
+ const run = snapshot.runs.find((item) => item.id === runId);
10170
+ if (!run) {
10171
+ return;
10172
+ }
10173
+ run.hermes_session_id = normalizedSessionId;
10174
+ await this.deps.writeSnapshot(conversationId, snapshot);
10175
+ const manifest = await this.deps.readActiveManifest(conversationId);
10176
+ const nextManifest = addHermesSessionIdToManifest(
10177
+ manifest,
10178
+ normalizedSessionId
10179
+ );
10180
+ if (nextManifest !== manifest) {
10181
+ await this.deps.writeManifest(nextManifest);
10182
+ }
10183
+ });
10184
+ }
10050
10185
  async runHasAssistantOutput(conversationId, runId) {
10051
10186
  const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
10052
10187
  const run = snapshot?.runs.find((item) => item.id === runId);
@@ -10984,6 +11119,7 @@ var ConversationService = class {
10984
11119
  statsOverride
10985
11120
  ),
10986
11121
  readActiveManifest: (conversationId) => this.store.readActiveManifest(conversationId),
11122
+ writeManifest: (manifest) => this.store.writeManifest(manifest),
10987
11123
  isConversationActive: (conversationId) => this.store.isConversationActive(conversationId),
10988
11124
  writeBlob: (conversationId, input) => this.maintenance.writeBlob(conversationId, input),
10989
11125
  syncCronDeliveries: () => this.syncCronDeliveries(),
@@ -11089,6 +11225,7 @@ var ConversationService = class {
11089
11225
  title_source: isDefaultConversationTitle(title) ? "default" : "hermes",
11090
11226
  status: "active",
11091
11227
  hermes_session_id: `hp_${id}`,
11228
+ hermes_session_ids: [`hp_${id}`],
11092
11229
  profile_uid: profile.profileUid,
11093
11230
  profile_name_snapshot: profile.profileName,
11094
11231
  profile: profile.profileName,
@@ -17495,6 +17632,7 @@ function connectRelayControl(options) {
17495
17632
  let retryTimer = null;
17496
17633
  let abortControllers = /* @__PURE__ */ new Map();
17497
17634
  let fatalRelayRejection = null;
17635
+ let latestNetworkRoutes = null;
17498
17636
  const connect = () => {
17499
17637
  options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
17500
17638
  fatalRelayRejection = null;
@@ -17506,6 +17644,10 @@ function connectRelayControl(options) {
17506
17644
  socket.on("open", () => {
17507
17645
  reconnectAttempts = 0;
17508
17646
  options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
17647
+ const currentSocket = socket;
17648
+ if (currentSocket && latestNetworkRoutes) {
17649
+ sendNetworkRoutes(currentSocket, options.linkId, latestNetworkRoutes);
17650
+ }
17509
17651
  });
17510
17652
  socket.on("message", (raw) => {
17511
17653
  if (!socket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
@@ -17553,6 +17695,12 @@ function connectRelayControl(options) {
17553
17695
  };
17554
17696
  connect();
17555
17697
  return {
17698
+ publishNetworkRoutes(routes) {
17699
+ latestNetworkRoutes = routes;
17700
+ if (socket?.readyState === WebSocket.OPEN) {
17701
+ sendNetworkRoutes(socket, options.linkId, routes);
17702
+ }
17703
+ },
17556
17704
  close() {
17557
17705
  closedByUser = true;
17558
17706
  if (retryTimer) {
@@ -17564,6 +17712,19 @@ function connectRelayControl(options) {
17564
17712
  }
17565
17713
  };
17566
17714
  }
17715
+ function sendNetworkRoutes(socket, linkId, routes) {
17716
+ socket.send(JSON.stringify({
17717
+ type: "network.routes",
17718
+ id: `routes_${Date.now().toString(36)}`,
17719
+ payload: {
17720
+ link_id: linkId,
17721
+ lan_ips: routes.lanIps,
17722
+ public_ipv4s: routes.publicIpv4s,
17723
+ public_ipv6s: routes.publicIpv6s,
17724
+ observed_at: (/* @__PURE__ */ new Date()).toISOString()
17725
+ }
17726
+ }));
17727
+ }
17567
17728
  function resolveFatalRelayRejection(message) {
17568
17729
  if (!/Unexpected server response:\s*(400|401|403|426)\b/u.test(message)) {
17569
17730
  return null;
@@ -17787,7 +17948,7 @@ async function discoverRouteCandidates(options) {
17787
17948
  const environment = detectRuntimeEnvironment();
17788
17949
  const configuredLanHost = normalizeLanHost(options.configuredLanHost);
17789
17950
  const lanIps = configuredLanHost ? [configuredLanHost] : environment.lanAutoDiscoveryUsable ? discoverLanIps() : [];
17790
- const publicIps = options.relayBootstrapToken ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
17951
+ const publicIps = options.relayBootstrapToken || options.observePublicRoute ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
17791
17952
  const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
17792
17953
  const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
17793
17954
  const preferredUrls = [
@@ -17830,8 +17991,8 @@ async function observePublicRoute(options) {
17830
17991
  const response = await fetcher(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/public-route/observe`, {
17831
17992
  method: "POST",
17832
17993
  headers: {
17833
- authorization: `Bearer ${options.relayBootstrapToken}`,
17834
- "content-type": "application/json"
17994
+ "content-type": "application/json",
17995
+ ...options.relayBootstrapToken ? { authorization: `Bearer ${options.relayBootstrapToken}` } : {}
17835
17996
  },
17836
17997
  body: JSON.stringify({
17837
17998
  install_id: options.installId,
@@ -17943,25 +18104,32 @@ function unique(values) {
17943
18104
 
17944
18105
  // src/link/network-report-state.ts
17945
18106
  var DEFAULT_AUTO_DAILY_LIMIT = 20;
17946
- async function markNetworkStatusReported(paths, lanIps, reportedAt = /* @__PURE__ */ new Date()) {
18107
+ async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
18108
+ const snapshot = normalizeNetworkSnapshot(snapshotInput);
17947
18109
  await updateNetworkReportState(paths, (current) => ({
17948
18110
  ...current,
17949
- lastReportedLanIps: normalizeLanIps(lanIps),
18111
+ lastReportedLanIps: snapshot.lanIps,
18112
+ lastReportedPublicIpv4s: snapshot.publicIpv4s,
18113
+ lastReportedPublicIpv6s: snapshot.publicIpv6s,
17950
18114
  lastReportedAt: reportedAt.toISOString(),
17951
- lastAutoAttempt: current.lastAutoAttempt ? { ...current.lastAutoAttempt, success: true } : null
18115
+ lastAutoAttempt: current.lastAutoAttempt ? { ...current.lastAutoAttempt, ...snapshot, success: true } : null
17952
18116
  }));
17953
18117
  }
17954
- async function reserveAutomaticNetworkReport(paths, lanIps, options = {}) {
17955
- const snapshot = normalizeLanIps(lanIps);
18118
+ async function reserveAutomaticNetworkReport(paths, snapshotInput, options = {}) {
18119
+ const snapshot = normalizeNetworkSnapshot(snapshotInput);
17956
18120
  const now = options.now ?? /* @__PURE__ */ new Date();
17957
18121
  const dailyLimit = Math.max(0, Math.floor(options.dailyLimit ?? DEFAULT_AUTO_DAILY_LIMIT));
17958
18122
  let reservation = { allowed: false, reason: "unchanged" };
17959
18123
  await updateNetworkReportState(paths, (current) => {
17960
- if (sameLanIps(current.lastReportedLanIps, snapshot)) {
17961
- reservation = { allowed: false, reason: "unchanged" };
17962
- return current;
18124
+ if (sameNetworkSnapshot(readReportedSnapshot(current), snapshot)) {
18125
+ const lastReportedAt = current.lastReportedAt ? Date.parse(current.lastReportedAt) : Number.NaN;
18126
+ const unchangedMinIntervalMs = Math.max(0, Math.floor(options.unchangedMinIntervalMs ?? 0));
18127
+ if (!options.force || Number.isFinite(lastReportedAt) && now.getTime() - lastReportedAt < unchangedMinIntervalMs) {
18128
+ reservation = { allowed: false, reason: "unchanged" };
18129
+ return current;
18130
+ }
17963
18131
  }
17964
- if (current.lastAutoAttempt && !current.lastAutoAttempt.success && sameLanIps(current.lastAutoAttempt.lanIps, snapshot)) {
18132
+ if (current.lastAutoAttempt && !current.lastAutoAttempt.success && sameNetworkSnapshot(readAttemptSnapshot(current.lastAutoAttempt), snapshot)) {
17965
18133
  reservation = { allowed: false, reason: "failed_snapshot_not_retried" };
17966
18134
  return current;
17967
18135
  }
@@ -17977,7 +18145,7 @@ async function reserveAutomaticNetworkReport(paths, lanIps, options = {}) {
17977
18145
  autoQuotaDay: quotaDay,
17978
18146
  autoReportsToday: reportsToday + 1,
17979
18147
  lastAutoAttempt: {
17980
- lanIps: snapshot,
18148
+ ...snapshot,
17981
18149
  attemptedAt: now.toISOString(),
17982
18150
  success: false
17983
18151
  }
@@ -18001,6 +18169,8 @@ function normalizeNetworkReportState(value) {
18001
18169
  const record = value && typeof value === "object" ? value : {};
18002
18170
  return {
18003
18171
  lastReportedLanIps: normalizeLanIps(record.lastReportedLanIps),
18172
+ lastReportedPublicIpv4s: normalizeLanIps(record.lastReportedPublicIpv4s),
18173
+ lastReportedPublicIpv6s: normalizeLanIps(record.lastReportedPublicIpv6s),
18004
18174
  lastReportedAt: typeof record.lastReportedAt === "string" ? record.lastReportedAt : null,
18005
18175
  autoQuotaDay: typeof record.autoQuotaDay === "string" ? record.autoQuotaDay : null,
18006
18176
  autoReportsToday: Number.isFinite(record.autoReportsToday) ? Math.max(0, Math.floor(record.autoReportsToday ?? 0)) : 0,
@@ -18017,10 +18187,41 @@ function normalizeAttempt(value) {
18017
18187
  }
18018
18188
  return {
18019
18189
  lanIps: normalizeLanIps(record.lanIps),
18190
+ publicIpv4s: normalizeLanIps(record.publicIpv4s),
18191
+ publicIpv6s: normalizeLanIps(record.publicIpv6s),
18020
18192
  attemptedAt: record.attemptedAt,
18021
18193
  success: record.success === true
18022
18194
  };
18023
18195
  }
18196
+ function normalizeNetworkSnapshot(value) {
18197
+ if (Array.isArray(value)) {
18198
+ return {
18199
+ lanIps: normalizeLanIps(value),
18200
+ publicIpv4s: [],
18201
+ publicIpv6s: []
18202
+ };
18203
+ }
18204
+ const record = value && typeof value === "object" ? value : {};
18205
+ return {
18206
+ lanIps: normalizeLanIps(record.lanIps),
18207
+ publicIpv4s: normalizeLanIps(record.publicIpv4s),
18208
+ publicIpv6s: normalizeLanIps(record.publicIpv6s)
18209
+ };
18210
+ }
18211
+ function readReportedSnapshot(state) {
18212
+ return {
18213
+ lanIps: state.lastReportedLanIps,
18214
+ publicIpv4s: state.lastReportedPublicIpv4s,
18215
+ publicIpv6s: state.lastReportedPublicIpv6s
18216
+ };
18217
+ }
18218
+ function readAttemptSnapshot(attempt) {
18219
+ return {
18220
+ lanIps: attempt.lanIps,
18221
+ publicIpv4s: attempt.publicIpv4s,
18222
+ publicIpv6s: attempt.publicIpv6s
18223
+ };
18224
+ }
18024
18225
  function normalizeLanIps(value) {
18025
18226
  if (!Array.isArray(value)) {
18026
18227
  return [];
@@ -18031,7 +18232,10 @@ function normalizeLanIps(value) {
18031
18232
  )
18032
18233
  ];
18033
18234
  }
18034
- function sameLanIps(left, right) {
18235
+ function sameNetworkSnapshot(left, right) {
18236
+ return sameStringList(left.lanIps, right.lanIps) && sameStringList(left.publicIpv4s, right.publicIpv4s) && sameStringList(left.publicIpv6s, right.publicIpv6s);
18237
+ }
18238
+ function sameStringList(left, right) {
18035
18239
  if (left.length !== right.length) {
18036
18240
  return false;
18037
18241
  }
@@ -18048,12 +18252,13 @@ async function reportLinkStatusToServer(options = {}) {
18048
18252
  if (!identity?.link_id) {
18049
18253
  return null;
18050
18254
  }
18051
- const routes = await discoverRouteCandidates({
18255
+ const routes = options.routes ?? await discoverRouteCandidates({
18052
18256
  port: config.port,
18053
18257
  relayBaseUrl: config.relayBaseUrl,
18054
18258
  linkId: identity.link_id,
18055
18259
  installId: identity.install_id,
18056
18260
  publicKeyPem: identity.public_key_pem,
18261
+ observePublicRoute: true,
18057
18262
  configuredLanHost: config.lanHost,
18058
18263
  fetchImpl: options.fetchImpl
18059
18264
  });
@@ -18093,7 +18298,7 @@ async function reportLinkStatusToServer(options = {}) {
18093
18298
  const message = readErrorMessage3(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
18094
18299
  throw new LinkHttpError(response.status, "server_request_failed", message);
18095
18300
  }
18096
- await markNetworkStatusReported(paths, routes.lanIps);
18301
+ await markNetworkStatusReported(paths, routes);
18097
18302
  return body;
18098
18303
  }
18099
18304
  function canonicalJson(value) {
@@ -18128,16 +18333,17 @@ function readErrorMessage3(payload) {
18128
18333
  // src/daemon/lan-ip-monitor.ts
18129
18334
  var DEFAULT_INTERVAL_MS = 5 * 6e4;
18130
18335
  var DEFAULT_DAILY_REPORT_LIMIT = 20;
18336
+ var DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS = 15 * 6e4;
18131
18337
  function startLanIpMonitor(options) {
18132
18338
  let running = false;
18133
18339
  let closed = false;
18134
- const check = async () => {
18340
+ const check = async (context = {}) => {
18135
18341
  if (running || closed) {
18136
18342
  return;
18137
18343
  }
18138
18344
  running = true;
18139
18345
  try {
18140
- await checkLanIpChange(options);
18346
+ await checkLanIpChange(options, context);
18141
18347
  } catch (error) {
18142
18348
  void options.logger.warn("lan_ip_monitor_failed", {
18143
18349
  error: error instanceof Error ? error.message : String(error)
@@ -18146,7 +18352,7 @@ function startLanIpMonitor(options) {
18146
18352
  running = false;
18147
18353
  }
18148
18354
  };
18149
- void check();
18355
+ void check({ forceReport: true, publishToRelay: true });
18150
18356
  const timer = setInterval(() => {
18151
18357
  void check();
18152
18358
  }, options.intervalMs ?? DEFAULT_INTERVAL_MS);
@@ -18158,7 +18364,7 @@ function startLanIpMonitor(options) {
18158
18364
  }
18159
18365
  };
18160
18366
  }
18161
- async function checkLanIpChange(options) {
18367
+ async function checkLanIpChange(options, context = {}) {
18162
18368
  const [identity, config] = await Promise.all([
18163
18369
  loadIdentity(options.paths),
18164
18370
  loadConfig(options.paths)
@@ -18172,14 +18378,25 @@ async function checkLanIpChange(options) {
18172
18378
  linkId: identity.link_id,
18173
18379
  installId: identity.install_id,
18174
18380
  publicKeyPem: identity.public_key_pem,
18381
+ observePublicRoute: true,
18175
18382
  configuredLanHost: config.lanHost,
18176
18383
  fetchImpl: options.fetchImpl
18177
18384
  });
18178
- const reservation = await reserveAutomaticNetworkReport(options.paths, routes.lanIps, {
18179
- dailyLimit: options.dailyReportLimit ?? DEFAULT_DAILY_REPORT_LIMIT
18385
+ if (context.publishToRelay) {
18386
+ options.onNetworkRoutes?.(routes);
18387
+ }
18388
+ const reservation = await reserveAutomaticNetworkReport(options.paths, routes, {
18389
+ dailyLimit: options.dailyReportLimit ?? DEFAULT_DAILY_REPORT_LIMIT,
18390
+ force: context.forceReport === true,
18391
+ unchangedMinIntervalMs: options.startupReportMinIntervalMs ?? DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS
18180
18392
  });
18181
18393
  if (!reservation.allowed) {
18182
- const logFields = { lan_ips: routes.lanIps, reason: reservation.reason };
18394
+ const logFields = {
18395
+ lan_ips: routes.lanIps,
18396
+ public_ipv4s: routes.publicIpv4s,
18397
+ public_ipv6s: routes.publicIpv6s,
18398
+ reason: reservation.reason
18399
+ };
18183
18400
  if (reservation.reason === "daily_limit_reached") {
18184
18401
  void options.logger.warn("lan_ip_report_skipped", logFields);
18185
18402
  } else {
@@ -18190,12 +18407,16 @@ async function checkLanIpChange(options) {
18190
18407
  try {
18191
18408
  const result = await reportLinkStatusToServer({
18192
18409
  paths: options.paths,
18193
- fetchImpl: options.fetchImpl
18410
+ fetchImpl: options.fetchImpl,
18411
+ routes
18194
18412
  });
18195
18413
  if (result) {
18414
+ options.onNetworkRoutes?.(routes);
18196
18415
  void options.logger.info("lan_ip_change_reported", {
18197
18416
  link_id: result.linkId,
18198
- lan_ips: routes.lanIps
18417
+ lan_ips: routes.lanIps,
18418
+ public_ipv4s: routes.publicIpv4s,
18419
+ public_ipv6s: routes.publicIpv6s
18199
18420
  });
18200
18421
  }
18201
18422
  } catch (error) {
@@ -18287,13 +18508,6 @@ async function startLinkService(options = {}) {
18287
18508
  conversations,
18288
18509
  logger
18289
18510
  });
18290
- const lanIpMonitor = startLanIpMonitor({
18291
- paths,
18292
- logger,
18293
- intervalMs: options.lanIpMonitorIntervalMs,
18294
- dailyReportLimit: options.lanIpMonitorDailyReportLimit,
18295
- fetchImpl: options.lanIpMonitorFetchImpl
18296
- });
18297
18511
  let relay = null;
18298
18512
  if (identity?.link_id) {
18299
18513
  relay = connectRelayControl({
@@ -18310,6 +18524,16 @@ async function startLinkService(options = {}) {
18310
18524
  } else {
18311
18525
  void logger.info("relay_skipped", { reason: "link_not_paired" });
18312
18526
  }
18527
+ const lanIpMonitor = startLanIpMonitor({
18528
+ paths,
18529
+ logger,
18530
+ intervalMs: options.lanIpMonitorIntervalMs,
18531
+ dailyReportLimit: options.lanIpMonitorDailyReportLimit,
18532
+ fetchImpl: options.lanIpMonitorFetchImpl,
18533
+ onNetworkRoutes: (routes) => {
18534
+ relay?.publishNetworkRoutes(routes);
18535
+ }
18536
+ });
18313
18537
  if (options.writePidFile) {
18314
18538
  await writePidFile(paths);
18315
18539
  }
@@ -20492,7 +20716,6 @@ export {
20492
20716
  resolveRuntimePaths,
20493
20717
  getLinkLogFile,
20494
20718
  ensureHermesApiServerAvailable,
20495
- readHermesVersion,
20496
20719
  loadConfig,
20497
20720
  saveConfig,
20498
20721
  normalizeLanHost,
package/dist/cli/index.js CHANGED
@@ -20,7 +20,6 @@ import {
20
20
  preparePairing,
21
21
  probeLocalLinkService,
22
22
  readHermesApiServerConfig,
23
- readHermesVersion,
24
23
  readPairingClaim,
25
24
  reportLinkStatusToServer,
26
25
  resolveHermesConfigPath,
@@ -31,7 +30,7 @@ import {
31
30
  startDaemonProcess,
32
31
  startLinkService,
33
32
  stopDaemonProcess
34
- } from "../chunk-45RR27KS.js";
33
+ } from "../chunk-PEHQ3AED.js";
35
34
 
36
35
  // src/cli/index.ts
37
36
  import { Command } from "commander";
@@ -510,21 +509,6 @@ async function assertPairingPreflightReady(options = {}) {
510
509
  if (failures.length > 0) {
511
510
  throwPairingPreflightError(failures);
512
511
  }
513
- const readVersion = options.readHermesVersion ?? readHermesVersion;
514
- try {
515
- await readVersion();
516
- } catch (error) {
517
- throwPairingPreflightError([
518
- {
519
- code: "hermes_cli_unavailable",
520
- zh: "\u6CA1\u6709\u627E\u5230\u53EF\u7528\u7684 Hermes Agent CLI\u3002Link \u9700\u8981\u548C Hermes \u5728\u540C\u4E00\u4E2A\u7CFB\u7EDF\u73AF\u5883\u91CC\uFF0C\u624D\u80FD\u5728\u914D\u5BF9\u65F6\u542F\u52A8\u548C\u68C0\u6D4B Hermes Gateway\u3002",
521
- en: "Hermes Agent CLI was not found. Link needs Hermes to be available in the same system environment so it can start and check Hermes Gateway during pairing.",
522
- actionZh: "\u8BF7\u628A Hermes Agent \u5B89\u88C5\u5728\u8FD9\u53F0\u5BBF\u4E3B\u673A\u4E0A\uFF0C\u6216\u628A `HERMES_BIN` \u6307\u5411 Link \u53EF\u4EE5\u76F4\u63A5\u6267\u884C\u7684 Hermes CLI\u3002\u5F53\u524D\u5982\u679C Hermes \u53EA\u5728\u53E6\u4E00\u4E2A Docker\u3001WSL \u6216\u865A\u62DF\u673A\u73AF\u5883\u91CC\uFF0C\u5BBF\u4E3B\u673A Link \u4E0D\u80FD\u76F4\u63A5\u7528\u5B83\u5B8C\u6210\u914D\u5BF9\u524D\u68C0\u67E5\u3002",
523
- actionEn: "Install Hermes Agent on this host, or point `HERMES_BIN` to a Hermes CLI that Link can run directly in this system environment. If Hermes only exists inside another Docker, WSL, or VM environment, host Link cannot use it for pairing preflight.",
524
- detail: error instanceof Error ? error.message : String(error)
525
- }
526
- ]);
527
- }
528
512
  const apiServerConfig = await readHermesApiServerConfig(profileName, configPath);
529
513
  if (apiServerConfig.enabled !== true) {
530
514
  throwPairingPreflightError([
@@ -237,12 +237,14 @@ interface ConversationEvent {
237
237
  interface DeleteConversationResult {
238
238
  conversation_id: string;
239
239
  hermes_deleted: boolean;
240
+ hermes_session_ids?: string[];
240
241
  deleted_at: string;
241
242
  }
242
243
  interface BulkDeleteConversationResult {
243
244
  conversation_id: string;
244
245
  status: 'deleted' | 'failed';
245
246
  hermes_deleted?: boolean;
247
+ hermes_session_ids?: string[];
246
248
  deleted_at?: string;
247
249
  error?: {
248
250
  code: string;
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-45RR27KS.js";
3
+ } from "../chunk-PEHQ3AED.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.3.0",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",