@hermespilot/link 0.4.4 → 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.4";
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,
@@ -20659,6 +20736,8 @@ import { mkdir as mkdir12, rm as rm9, writeFile as writeFile3 } from "fs/promise
20659
20736
 
20660
20737
  // src/relay/control-client.ts
20661
20738
  import WebSocket from "ws";
20739
+ var RELAY_SSE_BATCH_FLUSH_INTERVAL_MS = 50;
20740
+ var RELAY_SSE_BATCH_FLUSH_BYTES = 2 * 1024;
20662
20741
  function connectRelayControl(options) {
20663
20742
  const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
20664
20743
  wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
@@ -20794,6 +20873,7 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
20794
20873
  }
20795
20874
  const abortController = new AbortController();
20796
20875
  abortControllers.set(frame.id, abortController);
20876
+ let sseBatcher = null;
20797
20877
  try {
20798
20878
  const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
20799
20879
  method: frame.method,
@@ -20805,14 +20885,16 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
20805
20885
  const contentType = response.headers.get("content-type") ?? "";
20806
20886
  if (response.body && contentType.includes("text/event-stream")) {
20807
20887
  socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
20888
+ sseBatcher = createRelayStreamChunkBatcher(socket, frame.id);
20808
20889
  const reader = response.body.getReader();
20809
20890
  while (true) {
20810
20891
  const next = await reader.read();
20811
20892
  if (next.done) {
20812
20893
  break;
20813
20894
  }
20814
- socket.send(JSON.stringify({ type: "http.stream.chunk", id: frame.id, bodyBase64: Buffer.from(next.value).toString("base64") }));
20895
+ sseBatcher.push(next.value);
20815
20896
  }
20897
+ sseBatcher.flush();
20816
20898
  socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
20817
20899
  return;
20818
20900
  }
@@ -20822,15 +20904,73 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
20822
20904
  if (abortController.signal.aborted || isAbortError2(error)) {
20823
20905
  return;
20824
20906
  }
20907
+ sseBatcher?.flush();
20825
20908
  const message = error instanceof Error ? error.message : "Relay request failed";
20826
20909
  socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
20827
20910
  } finally {
20911
+ sseBatcher?.dispose();
20828
20912
  abortControllers.delete(frame.id);
20829
20913
  }
20830
20914
  }
20831
20915
  function isAbortError2(error) {
20832
20916
  return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
20833
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
+ }
20834
20974
 
20835
20975
  // src/runtime/system-info.ts
20836
20976
  import { execFileSync } from "child_process";
package/dist/cli/index.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  startDaemonProcess,
37
37
  startLinkService,
38
38
  stopDaemonProcess
39
- } from "../chunk-2CHGHWCY.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-2CHGHWCY.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.4",
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",