@hermespilot/link 0.4.4 → 0.4.6

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.
@@ -2080,6 +2080,14 @@ async function ensureHermesApiServerConfig(profileName = "default", configPath =
2080
2080
  }
2081
2081
  return ensureHermesApiServerConfigUnlocked(profileName, configPath);
2082
2082
  }
2083
+ async function repairHermesApiServerConfig(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
2084
+ if (profileName !== "default") {
2085
+ return withProfileApiServerPortAssignmentLock(
2086
+ () => repairHermesApiServerConfigUnlocked(profileName, configPath)
2087
+ );
2088
+ }
2089
+ return repairHermesApiServerConfigUnlocked(profileName, configPath);
2090
+ }
2083
2091
  async function ensureHermesApiServerConfigUnlocked(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
2084
2092
  const existingRaw = await readFile2(configPath, "utf8").catch(
2085
2093
  (error) => {
@@ -2096,47 +2104,60 @@ async function ensureHermesApiServerConfigUnlocked(profileName = "default", conf
2096
2104
  const extra = ensureRecord(apiServer, "extra");
2097
2105
  const configOnly = readApiServerConfig(apiServer);
2098
2106
  const envOverrides = await readHermesApiServerEnvOverrides(profileName);
2099
- const before = applyEnvOverrides(configOnly, envOverrides, false);
2100
- const beforeKey = before.key?.trim() ? before.key : null;
2101
- const beforeEnabled = before.enabled === true;
2102
- const beforeHost = before.host?.trim() ? before.host : null;
2103
- const beforePort = typeof before.port === "number" && Number.isFinite(before.port) ? before.port : null;
2107
+ const configKey = configOnly.key?.trim() ? configOnly.key : null;
2108
+ const envKey = envOverrides.key?.trim() ? envOverrides.key : null;
2109
+ const configHost = configOnly.host?.trim() ? configOnly.host : null;
2110
+ const envHost = envOverrides.host?.trim() ? envOverrides.host : null;
2111
+ const configPort = readApiServerPort(configOnly.port);
2112
+ const envPort = readApiServerPort(envOverrides.port);
2113
+ const desiredHost = configHost ?? envHost ?? DEFAULT_HERMES_API_SERVER_HOST;
2114
+ const desiredPort = await resolveDesiredApiServerPort({
2115
+ profileName,
2116
+ configPort,
2117
+ envPort
2118
+ });
2119
+ const desiredKey = configKey ?? envKey ?? randomBytes(32).toString("base64url");
2104
2120
  let changed = false;
2105
2121
  let enabledAdded = false;
2106
2122
  let hostAdded = false;
2107
2123
  let portAdded = false;
2108
- let assignedPort = null;
2109
- if (!beforeEnabled) {
2124
+ if (apiServer.enabled !== true) {
2110
2125
  apiServer.enabled = true;
2111
2126
  enabledAdded = true;
2112
2127
  changed = true;
2113
2128
  }
2114
- if (!beforeHost) {
2115
- extra.host = DEFAULT_HERMES_API_SERVER_HOST;
2116
- hostAdded = true;
2129
+ if (configHost !== desiredHost) {
2130
+ extra.host = desiredHost;
2131
+ hostAdded = !configHost;
2117
2132
  changed = true;
2118
2133
  }
2119
- if (shouldAssignDedicatedProfileApiServerPort(profileName, configOnly.port)) {
2120
- assignedPort = await nextProfileApiServerPort(profileName);
2121
- extra.port = assignedPort;
2122
- portAdded = true;
2123
- changed = true;
2124
- } else if (!beforePort) {
2125
- assignedPort = DEFAULT_HERMES_API_SERVER_PORT;
2126
- extra.port = assignedPort;
2134
+ if (configPort !== desiredPort) {
2135
+ extra.port = desiredPort;
2127
2136
  portAdded = true;
2128
2137
  changed = true;
2129
2138
  }
2130
- if (!beforeKey) {
2131
- extra.key = randomBytes(32).toString("base64url");
2139
+ if (configKey !== desiredKey) {
2140
+ extra.key = desiredKey;
2132
2141
  changed = true;
2133
2142
  }
2134
- if (!changed) {
2143
+ const desiredApiServer = readApiServerConfig(apiServer, true);
2144
+ const backupPath = changed && existingRaw ? `${configPath}.bak.${Date.now()}` : null;
2145
+ if (backupPath && existingRaw !== null) {
2146
+ await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
2147
+ metadataSourcePath: configPath
2148
+ });
2149
+ }
2150
+ if (changed) {
2151
+ document.contents = document.createNode(config);
2152
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
2153
+ }
2154
+ const envChanged = await writeHermesApiServerEnv(profileName, desiredApiServer);
2155
+ if (!changed && !envChanged) {
2135
2156
  return {
2136
2157
  configPath,
2137
2158
  apiServer: applyEnvOverrides(
2138
2159
  readApiServerConfig(apiServer, true),
2139
- envOverrides,
2160
+ await readHermesApiServerEnvOverrides(profileName),
2140
2161
  true
2141
2162
  ),
2142
2163
  changed: false,
@@ -2148,36 +2169,79 @@ async function ensureHermesApiServerConfigUnlocked(profileName = "default", conf
2148
2169
  notice: null
2149
2170
  };
2150
2171
  }
2151
- const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
2152
- if (backupPath && existingRaw !== null) {
2153
- await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
2154
- metadataSourcePath: configPath
2155
- });
2156
- }
2157
- document.contents = document.createNode(config);
2158
- await atomicWriteFilePreservingMetadata(configPath, document.toString());
2159
2172
  return {
2160
2173
  configPath,
2161
2174
  apiServer: applyEnvOverrides(
2162
2175
  readApiServerConfig(apiServer, true),
2163
- envOverrides,
2176
+ await readHermesApiServerEnvOverrides(profileName),
2164
2177
  true
2165
2178
  ),
2166
2179
  changed: true,
2167
- keyAdded: !beforeKey,
2180
+ keyAdded: !configKey,
2168
2181
  enabledAdded,
2169
2182
  hostAdded,
2170
2183
  portAdded,
2171
2184
  backupPath,
2172
2185
  notice: buildNotice({
2173
- keyAdded: !beforeKey,
2186
+ keyAdded: !configKey,
2174
2187
  enabledAdded,
2175
2188
  hostAdded,
2176
2189
  portAdded,
2177
- port: assignedPort ?? beforePort ?? void 0
2190
+ port: desiredPort
2178
2191
  })
2179
2192
  };
2180
2193
  }
2194
+ async function repairHermesApiServerConfigUnlocked(profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
2195
+ const existingRaw = await readFile2(configPath, "utf8").catch(
2196
+ (error) => {
2197
+ if (isNodeError3(error, "ENOENT")) {
2198
+ return null;
2199
+ }
2200
+ throw error;
2201
+ }
2202
+ );
2203
+ const document = existingRaw ? YAML.parseDocument(existingRaw) : new YAML.Document({});
2204
+ const config = toRecord(document.toJSON());
2205
+ const platforms = ensureRecord(config, "platforms");
2206
+ const apiServer = ensureRecord(platforms, "api_server");
2207
+ const extra = ensureRecord(apiServer, "extra");
2208
+ const previous = applyEnvOverrides(
2209
+ readApiServerConfig(apiServer, true),
2210
+ await readHermesApiServerEnvOverrides(profileName),
2211
+ true
2212
+ );
2213
+ const freshPort = await nextProfileApiServerPort(profileName);
2214
+ const freshKey = randomBytes(32).toString("base64url");
2215
+ apiServer.enabled = true;
2216
+ extra.host = DEFAULT_HERMES_API_SERVER_HOST;
2217
+ extra.port = freshPort;
2218
+ extra.key = freshKey;
2219
+ const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
2220
+ if (backupPath && existingRaw !== null) {
2221
+ await atomicWriteFilePreservingMetadata(backupPath, existingRaw, {
2222
+ metadataSourcePath: configPath
2223
+ });
2224
+ }
2225
+ document.contents = document.createNode(config);
2226
+ await atomicWriteFilePreservingMetadata(configPath, document.toString());
2227
+ const apiServerConfig = readApiServerConfig(apiServer, true);
2228
+ await writeHermesApiServerEnv(profileName, apiServerConfig, { force: true });
2229
+ return {
2230
+ configPath,
2231
+ apiServer: applyEnvOverrides(
2232
+ apiServerConfig,
2233
+ await readHermesApiServerEnvOverrides(profileName),
2234
+ true
2235
+ ),
2236
+ changed: true,
2237
+ keyAdded: !previous.key,
2238
+ enabledAdded: previous.enabled !== true,
2239
+ hostAdded: previous.host !== DEFAULT_HERMES_API_SERVER_HOST,
2240
+ portAdded: previous.port !== freshPort,
2241
+ backupPath,
2242
+ notice: "\u5DF2\u4E3A Hermes API Server \u91CD\u65B0\u5206\u914D\u672C\u673A\u7AEF\u53E3\u5E76\u8F6E\u6362 key\uFF0C\u7528\u4E8E\u4FEE\u590D\u65E7 Gateway \u6216 key \u4E0D\u4E00\u81F4\u5BFC\u81F4\u7684 401\u3002"
2243
+ };
2244
+ }
2181
2245
  async function readHermesConfigDocument(configPath) {
2182
2246
  const existingRaw = await readFile2(configPath, "utf8").catch(
2183
2247
  (error) => {
@@ -3511,6 +3575,21 @@ function shouldAssignDedicatedProfileApiServerPort(profileName, configuredPort)
3511
3575
  }
3512
3576
  return configuredPort === void 0 || configuredPort === DEFAULT_HERMES_API_SERVER_PORT;
3513
3577
  }
3578
+ function readApiServerPort(value) {
3579
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
3580
+ }
3581
+ async function resolveDesiredApiServerPort(input) {
3582
+ if (shouldAssignDedicatedProfileApiServerPort(
3583
+ input.profileName,
3584
+ input.configPort ?? void 0
3585
+ )) {
3586
+ if (input.profileName !== "default" && input.envPort !== null && input.envPort !== DEFAULT_HERMES_API_SERVER_PORT) {
3587
+ return input.envPort;
3588
+ }
3589
+ return nextProfileApiServerPort(input.profileName);
3590
+ }
3591
+ return input.configPort ?? input.envPort ?? DEFAULT_HERMES_API_SERVER_PORT;
3592
+ }
3514
3593
  async function nextProfileApiServerPort(profileName) {
3515
3594
  const usedPorts = await readConfiguredApiServerPorts(profileName);
3516
3595
  for (let port = PROFILE_API_SERVER_PORT_START; port <= PROFILE_API_SERVER_PORT_END; port += 1) {
@@ -3602,17 +3681,6 @@ function isEnvValueConfigured(value) {
3602
3681
  }
3603
3682
  async function readHermesApiServerEnvOverrides(profileName) {
3604
3683
  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
3684
  const port = Number.parseInt(values.API_SERVER_PORT ?? "", 10);
3617
3685
  return {
3618
3686
  enabled: parseEnvBoolean(values.API_SERVER_ENABLED),
@@ -3681,14 +3749,82 @@ async function writeHermesEnvValue(profileName, key, value) {
3681
3749
  }
3682
3750
  await atomicWriteFilePreservingMetadata(envPath, nextRaw);
3683
3751
  }
3752
+ async function writeHermesApiServerEnv(profileName, config, options = {}) {
3753
+ const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
3754
+ const existingRaw = await readFile2(envPath, "utf8").catch(
3755
+ (error) => {
3756
+ if (isNodeError3(error, "ENOENT")) {
3757
+ return null;
3758
+ }
3759
+ throw error;
3760
+ }
3761
+ );
3762
+ if (existingRaw === null && profileName === "default" && !options.force) {
3763
+ return false;
3764
+ }
3765
+ return writeHermesEnvValues(
3766
+ profileName,
3767
+ [
3768
+ ["API_SERVER_ENABLED", config.enabled === false ? "false" : "true"],
3769
+ ["API_SERVER_HOST", config.host ?? DEFAULT_HERMES_API_SERVER_HOST],
3770
+ ["API_SERVER_PORT", String(config.port ?? DEFAULT_HERMES_API_SERVER_PORT)],
3771
+ ["API_SERVER_KEY", config.key ?? ""]
3772
+ ],
3773
+ existingRaw ?? ""
3774
+ );
3775
+ }
3776
+ async function writeHermesEnvValues(profileName, values, existingRaw) {
3777
+ const envPath = path3.join(resolveHermesProfileDir(profileName), ".env");
3778
+ const raw = existingRaw ?? await readFile2(envPath, "utf8").catch((error) => {
3779
+ if (isNodeError3(error, "ENOENT")) {
3780
+ return "";
3781
+ }
3782
+ throw error;
3783
+ });
3784
+ const lines = raw ? raw.split(/\r?\n/u) : [];
3785
+ const remaining = new Map(values);
3786
+ const nextLines = lines.map((line) => {
3787
+ const trimmed = line.trim();
3788
+ for (const [key, value] of remaining) {
3789
+ const keyPattern = new RegExp(
3790
+ `^(?:export\\s+)?${escapeRegExp(key)}=`,
3791
+ "u"
3792
+ );
3793
+ if (keyPattern.test(trimmed)) {
3794
+ remaining.delete(key);
3795
+ return `${key}=${formatEnvValue(value)}`;
3796
+ }
3797
+ }
3798
+ return line;
3799
+ });
3800
+ if (remaining.size > 0 && nextLines.length > 0 && nextLines.at(-1) !== "") {
3801
+ nextLines.push("");
3802
+ }
3803
+ for (const [key, value] of remaining) {
3804
+ nextLines.push(`${key}=${formatEnvValue(value)}`);
3805
+ }
3806
+ const nextRaw = nextLines.join("\n").replace(/\n*$/u, "\n");
3807
+ if (nextRaw === raw) {
3808
+ return false;
3809
+ }
3810
+ if (raw) {
3811
+ await atomicWriteFilePreservingMetadata(
3812
+ `${envPath}.bak.${Date.now()}`,
3813
+ raw,
3814
+ { metadataSourcePath: envPath }
3815
+ );
3816
+ }
3817
+ await atomicWriteFilePreservingMetadata(envPath, nextRaw);
3818
+ return true;
3819
+ }
3684
3820
  function applyEnvOverrides(config, env, withDefaults) {
3685
- const host = config.host ?? env.host;
3686
- const port = config.port ?? env.port;
3821
+ const host = env.host ?? config.host;
3822
+ const port = env.port ?? config.port;
3687
3823
  return {
3688
- enabled: config.enabled ?? env.enabled,
3824
+ enabled: env.enabled ?? config.enabled,
3689
3825
  host: withDefaults ? host ?? DEFAULT_HERMES_API_SERVER_HOST : host,
3690
3826
  port: withDefaults ? port ?? DEFAULT_HERMES_API_SERVER_PORT : port,
3691
- key: config.key ?? env.key
3827
+ key: env.key ?? config.key
3692
3828
  };
3693
3829
  }
3694
3830
  function parseEnvBoolean(value) {
@@ -3996,7 +4132,7 @@ import os2 from "os";
3996
4132
  import path5 from "path";
3997
4133
 
3998
4134
  // src/constants.ts
3999
- var LINK_VERSION = "0.4.4";
4135
+ var LINK_VERSION = "0.4.6";
4000
4136
  var LINK_COMMAND = "hermeslink";
4001
4137
  var LINK_DEFAULT_PORT = 52379;
4002
4138
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4496,6 +4632,7 @@ var DASHBOARD_STATUS_URL = "http://127.0.0.1:9119/api/status";
4496
4632
  var DASHBOARD_STATUS_TIMEOUT_MS = 1500;
4497
4633
  var DEFAULT_VERSION_CACHE_TTL_MS = 6e4;
4498
4634
  var MAX_VERSION_LOG_OUTPUT_LENGTH = 1200;
4635
+ var HERMES_GATEWAY_ENV_BLOCKLIST_PREFIXES = ["API_SERVER_"];
4499
4636
  var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
4500
4637
  var hermesVersionCache = /* @__PURE__ */ new Map();
4501
4638
  var hermesVersionInFlight = /* @__PURE__ */ new Map();
@@ -4558,6 +4695,47 @@ async function ensureHermesApiServerAvailable(options = {}) {
4558
4695
  });
4559
4696
  return { available: true, configResult, started: true, start, health };
4560
4697
  }
4698
+ if (health.authInvalid) {
4699
+ void options.logger?.warn("gateway_api_server_auth_repair_requested", {
4700
+ profile: profileName,
4701
+ port: configResult.apiServer.port ?? null,
4702
+ issue: health.issue ?? "auth_invalid"
4703
+ });
4704
+ const repairedConfigResult = await repairHermesApiServerConfig(profileName);
4705
+ const repairedStart = await startHermesGatewayOnce(
4706
+ options.paths ?? resolveRuntimePaths(),
4707
+ profileName,
4708
+ options.logger
4709
+ );
4710
+ health = await waitForHermesApiHealth(
4711
+ repairedConfigResult.apiServer,
4712
+ fetcher,
4713
+ options.timeoutMs ?? DEFAULT_START_TIMEOUT_MS
4714
+ );
4715
+ if (health.healthy) {
4716
+ void options.logger?.info("gateway_api_server_auth_repair_succeeded", {
4717
+ profile: profileName,
4718
+ pid: repairedStart.pid,
4719
+ port: repairedConfigResult.apiServer.port ?? null,
4720
+ log_path: repairedStart.logPath,
4721
+ hermes_version: repairedStart.version?.version ?? null
4722
+ });
4723
+ return {
4724
+ available: true,
4725
+ configResult: repairedConfigResult,
4726
+ started: true,
4727
+ start: repairedStart,
4728
+ health
4729
+ };
4730
+ }
4731
+ void options.logger?.error("gateway_api_server_auth_repair_failed", {
4732
+ profile: profileName,
4733
+ pid: repairedStart.pid,
4734
+ port: repairedConfigResult.apiServer.port ?? null,
4735
+ log_path: repairedStart.logPath,
4736
+ issue: health.issue ?? "unknown"
4737
+ });
4738
+ }
4561
4739
  const logHint = await readRecentGatewayLogHint(start.logPath);
4562
4740
  void options.logger?.error("gateway_auto_start_failed", {
4563
4741
  profile: profileName,
@@ -4648,12 +4826,9 @@ async function readHermesVersion(options = {}) {
4648
4826
  }
4649
4827
  async function execHermesVersion(hermesBin, logger) {
4650
4828
  const failures = [];
4651
- for (const args of [["version"], ["--version"]]) {
4829
+ for (const args of [["--version"], ["version"]]) {
4652
4830
  try {
4653
- return await execFileAsync2(hermesBin, args, {
4654
- timeout: 5e3,
4655
- windowsHide: true
4656
- });
4831
+ return await spawnHermesVersionCommand(hermesBin, args, 5e3);
4657
4832
  } catch (error) {
4658
4833
  const failure = describeVersionCommandFailure(hermesBin, args, error);
4659
4834
  failures.push(failure);
@@ -4669,6 +4844,85 @@ async function execHermesVersion(hermesBin, logger) {
4669
4844
  });
4670
4845
  throw new Error(summary);
4671
4846
  }
4847
+ async function spawnHermesVersionCommand(hermesBin, args, timeoutMs) {
4848
+ return await new Promise((resolve, reject) => {
4849
+ const child = spawn(hermesBin, args, {
4850
+ stdio: ["ignore", "pipe", "pipe"],
4851
+ windowsHide: true
4852
+ });
4853
+ let stdout = "";
4854
+ let stderr = "";
4855
+ let settled = false;
4856
+ const timer = setTimeout(() => {
4857
+ finish(() => {
4858
+ reject(
4859
+ createVersionCommandError(
4860
+ `${hermesBin} ${args.join(" ")} timed out after ${timeoutMs}ms`,
4861
+ { stdout, stderr, killed: true }
4862
+ )
4863
+ );
4864
+ }, true);
4865
+ }, timeoutMs);
4866
+ const finish = (callback, kill = false) => {
4867
+ if (settled) {
4868
+ return;
4869
+ }
4870
+ settled = true;
4871
+ clearTimeout(timer);
4872
+ if (kill && child.pid && !child.killed) {
4873
+ child.kill();
4874
+ }
4875
+ callback();
4876
+ };
4877
+ const resolveIfVersionPrinted = () => {
4878
+ if (parseHermesVersion(stdout)) {
4879
+ finish(() => resolve({ stdout: `${stdout}
4880
+ ${stderr}`.trim() }), true);
4881
+ }
4882
+ };
4883
+ child.stdout?.on("data", (chunk) => {
4884
+ stdout += chunk.toString();
4885
+ resolveIfVersionPrinted();
4886
+ });
4887
+ child.stderr?.on("data", (chunk) => {
4888
+ stderr += chunk.toString();
4889
+ });
4890
+ child.once("error", (error) => {
4891
+ finish(() => {
4892
+ reject(
4893
+ createVersionCommandError(error.message, {
4894
+ stdout,
4895
+ stderr,
4896
+ cause: error
4897
+ })
4898
+ );
4899
+ });
4900
+ });
4901
+ child.once("close", (code, signal) => {
4902
+ finish(() => {
4903
+ const raw = `${stdout}
4904
+ ${stderr}`.trim();
4905
+ const version = parseHermesVersion(raw);
4906
+ if (code === 0 && (raw || version)) {
4907
+ resolve({ stdout: raw });
4908
+ return;
4909
+ }
4910
+ reject(
4911
+ createVersionCommandError(
4912
+ `${hermesBin} ${args.join(" ")} exited with code ${code ?? "null"}`,
4913
+ {
4914
+ stdout,
4915
+ stderr,
4916
+ code,
4917
+ signal,
4918
+ killed: child.killed
4919
+ }
4920
+ )
4921
+ );
4922
+ });
4923
+ });
4924
+ });
4925
+ }
4672
4926
  function assertHermesRunsApiSupported(version, status) {
4673
4927
  if (status !== 404) {
4674
4928
  return;
@@ -4720,7 +4974,7 @@ async function startHermesGateway(paths, profileName, logger) {
4720
4974
  try {
4721
4975
  const child = spawn(resolveHermesBin(), gatewayRunArgs(profileName), {
4722
4976
  detached: true,
4723
- env: process.env,
4977
+ env: buildHermesGatewayChildEnv(),
4724
4978
  stdio: ["ignore", "pipe", "pipe"],
4725
4979
  windowsHide: true
4726
4980
  });
@@ -4786,6 +5040,7 @@ async function restartHermesGatewayServiceIfAvailable(options) {
4786
5040
  }
4787
5041
  try {
4788
5042
  await execFileAsync2(resolveHermesBin(), ["gateway", "restart"], {
5043
+ env: buildHermesGatewayChildEnv(),
4789
5044
  timeout: options.timeoutMs ?? DEFAULT_START_TIMEOUT_MS,
4790
5045
  windowsHide: true
4791
5046
  });
@@ -4840,6 +5095,17 @@ function gatewayRunArgs(profileName) {
4840
5095
  function formatHermesGatewayRunCommand(profileName) {
4841
5096
  return `${resolveHermesBin()} ${gatewayRunArgs(profileName).join(" ")}`;
4842
5097
  }
5098
+ function buildHermesGatewayChildEnv() {
5099
+ const env = { ...process.env };
5100
+ for (const key of Object.keys(env)) {
5101
+ if (HERMES_GATEWAY_ENV_BLOCKLIST_PREFIXES.some(
5102
+ (prefix) => key.startsWith(prefix)
5103
+ )) {
5104
+ delete env[key];
5105
+ }
5106
+ }
5107
+ return env;
5108
+ }
4843
5109
  async function fileExists(filePath) {
4844
5110
  return access(filePath).then(() => true).catch(() => false);
4845
5111
  }
@@ -4875,6 +5141,10 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
4875
5141
  }
4876
5142
  const issue = describeDetailedHealthIssue(record);
4877
5143
  const terminal = isTerminalDetailedHealth(record);
5144
+ const authIssue = issue ? null : await probeHermesApiServerAuth(resolvedConfig, fetcher);
5145
+ if (authIssue) {
5146
+ return authIssue;
5147
+ }
4878
5148
  return {
4879
5149
  healthy: !issue,
4880
5150
  terminal,
@@ -4910,22 +5180,9 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
4910
5180
  detailed: record
4911
5181
  };
4912
5182
  }
4913
- 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
- };
4928
- }
5183
+ const authIssue = await probeHermesApiServerAuth(resolvedConfig, fetcher);
5184
+ if (authIssue) {
5185
+ return authIssue;
4929
5186
  }
4930
5187
  return { healthy: true };
4931
5188
  }
@@ -4941,6 +5198,36 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
4941
5198
  issue: describePortHealthFailure(resolvedConfig.port)
4942
5199
  };
4943
5200
  }
5201
+ async function probeHermesApiServerAuth(config, fetcher) {
5202
+ const response = await fetchWithTimeout(
5203
+ `http://127.0.0.1:${config.port}/v1/models`,
5204
+ {
5205
+ method: "GET",
5206
+ headers: authHeaders(config)
5207
+ },
5208
+ fetcher
5209
+ );
5210
+ if (!response) {
5211
+ return {
5212
+ healthy: false,
5213
+ 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"
5214
+ };
5215
+ }
5216
+ if (response?.status === 401) {
5217
+ return {
5218
+ healthy: false,
5219
+ authInvalid: true,
5220
+ 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"
5221
+ };
5222
+ }
5223
+ if (response && !response.ok) {
5224
+ return {
5225
+ healthy: false,
5226
+ issue: `Hermes API Server \u9274\u6743\u63A2\u6D4B\u5931\u8D25\uFF1A/v1/models \u8FD4\u56DE HTTP ${response.status}\u3002`
5227
+ };
5228
+ }
5229
+ return null;
5230
+ }
4944
5231
  function fetchWithTimeout(input, init, fetcher) {
4945
5232
  const controller = new AbortController();
4946
5233
  const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
@@ -5122,6 +5409,21 @@ async function probeHermesVersion(hermesBin, options) {
5122
5409
  }
5123
5410
  }
5124
5411
  }
5412
+ function createVersionCommandError(message, details) {
5413
+ const error = new Error(message, { cause: details.cause });
5414
+ error.stdout = details.stdout;
5415
+ error.stderr = details.stderr;
5416
+ if (details.code !== void 0) {
5417
+ error.code = details.code;
5418
+ }
5419
+ if (details.signal !== void 0) {
5420
+ error.signal = details.signal;
5421
+ }
5422
+ if (details.killed !== void 0) {
5423
+ error.killed = details.killed;
5424
+ }
5425
+ return error;
5426
+ }
5125
5427
  function describeVersionCommandFailure(hermesBin, args, error) {
5126
5428
  const message = error instanceof Error ? error.message : String(error);
5127
5429
  const details = readExecErrorDetails(error, message);
@@ -8979,24 +9281,15 @@ var ConversationQueryCoordinator = class {
8979
9281
  async listConversationPage(options = {}) {
8980
9282
  const limit = normalizeConversationListPageLimit(options.limit);
8981
9283
  const cursor = decodeConversationListCursor(options.cursor);
8982
- const indexedPage = await listConversationStatsPage(this.deps.paths, {
9284
+ return this.listIndexedConversationPage({
8983
9285
  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: {
9286
+ cursor,
9287
+ fallback: () => this.listConversationPageFromStore({ limit, cursor }),
9288
+ listPage: (pageCursor) => listConversationStatsPage(this.deps.paths, {
8995
9289
  limit,
8996
- has_more: indexedPage.hasMore,
8997
- next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
8998
- }
8999
- };
9290
+ cursor: pageCursor
9291
+ })
9292
+ });
9000
9293
  }
9001
9294
  async searchConversationPage(options = {}) {
9002
9295
  const query = normalizeConversationSearchQuery(options.query);
@@ -9005,20 +9298,72 @@ var ConversationQueryCoordinator = class {
9005
9298
  }
9006
9299
  const limit = normalizeConversationListPageLimit(options.limit);
9007
9300
  const cursor = decodeConversationListCursor(options.cursor);
9008
- const indexedPage = await searchConversationStatsPage(this.deps.paths, {
9301
+ return this.listIndexedConversationPage({
9009
9302
  limit,
9010
9303
  cursor,
9011
- query
9304
+ listPage: (pageCursor) => searchConversationStatsPage(this.deps.paths, {
9305
+ limit,
9306
+ cursor: pageCursor,
9307
+ query
9308
+ })
9012
9309
  });
9013
- const summaries = await this.summarizeIndexedConversations(
9014
- indexedPage.records
9015
- );
9310
+ }
9311
+ async listIndexedConversationPage(input) {
9312
+ const collected = [];
9313
+ const seenConversationIds = /* @__PURE__ */ new Set();
9314
+ let scanCursor = input.cursor;
9315
+ let usedIndex = false;
9316
+ while (collected.length <= input.limit) {
9317
+ const indexedPage = await input.listPage(scanCursor);
9318
+ if (indexedPage.records.length === 0) {
9319
+ if (!usedIndex && input.fallback) {
9320
+ return input.fallback();
9321
+ }
9322
+ return this.buildConversationListPage({
9323
+ limit: input.limit,
9324
+ conversations: collected,
9325
+ hasMore: false
9326
+ });
9327
+ }
9328
+ usedIndex = true;
9329
+ const summaries = await this.summarizeIndexedConversations(
9330
+ indexedPage.records
9331
+ );
9332
+ for (const summary of summaries) {
9333
+ if (!seenConversationIds.add(summary.id)) {
9334
+ continue;
9335
+ }
9336
+ collected.push(summary);
9337
+ if (collected.length > input.limit) {
9338
+ break;
9339
+ }
9340
+ }
9341
+ if (collected.length > input.limit) {
9342
+ break;
9343
+ }
9344
+ if (!indexedPage.hasMore) {
9345
+ return this.buildConversationListPage({
9346
+ limit: input.limit,
9347
+ conversations: collected,
9348
+ hasMore: false
9349
+ });
9350
+ }
9351
+ scanCursor = conversationListCursorFromRecord(indexedPage.records.at(-1));
9352
+ }
9353
+ return this.buildConversationListPage({
9354
+ limit: input.limit,
9355
+ conversations: collected,
9356
+ hasMore: true
9357
+ });
9358
+ }
9359
+ buildConversationListPage(input) {
9360
+ const conversations = input.conversations.slice(0, input.limit);
9016
9361
  return {
9017
- conversations: summaries,
9362
+ conversations,
9018
9363
  page: {
9019
- limit,
9020
- has_more: indexedPage.hasMore,
9021
- next_cursor: indexedPage.hasMore && indexedPage.records.length > 0 ? encodeConversationListCursor(indexedPage.records.at(-1)) : null
9364
+ limit: input.limit,
9365
+ has_more: input.hasMore,
9366
+ next_cursor: input.hasMore && conversations.length > 0 ? encodeConversationListCursorFromSummary(conversations.at(-1)) : null
9022
9367
  }
9023
9368
  };
9024
9369
  }
@@ -9177,6 +9522,12 @@ function encodeConversationListCursor(record) {
9177
9522
  "utf8"
9178
9523
  ).toString("base64url");
9179
9524
  }
9525
+ function conversationListCursorFromRecord(record) {
9526
+ return {
9527
+ updatedAt: record.updatedAt,
9528
+ conversationId: record.conversationId
9529
+ };
9530
+ }
9180
9531
  function encodeConversationListCursorFromSummary(summary) {
9181
9532
  return encodeConversationListCursor({
9182
9533
  conversationId: summary.id,
@@ -11668,6 +12019,46 @@ async function callHermesApi(path26, init, options) {
11668
12019
  startedAt,
11669
12020
  response
11670
12021
  );
12022
+ if (response.status !== 401) {
12023
+ return response;
12024
+ }
12025
+ void options.logger?.warn("hermes_api_request_repairing_after_401", {
12026
+ method,
12027
+ path: path26,
12028
+ profile: options.profileName ?? "default",
12029
+ port: config.port ?? null,
12030
+ duration_ms: Date.now() - startedAt
12031
+ });
12032
+ const profileName = options.profileName ?? "default";
12033
+ const repairedConfig = await repairHermesApiServerConfig(profileName);
12034
+ const repairedAvailability = await ensureHermesApiServerAvailable({
12035
+ fetchImpl: options.fetchImpl,
12036
+ forceRestart: true,
12037
+ logger: options.logger,
12038
+ profileName
12039
+ });
12040
+ config = repairedAvailability.configResult.apiServer ?? repairedConfig.apiServer;
12041
+ try {
12042
+ response = await request();
12043
+ } catch (error) {
12044
+ logHermesApiError(
12045
+ options.logger,
12046
+ method,
12047
+ path26,
12048
+ options.profileName,
12049
+ startedAt,
12050
+ error
12051
+ );
12052
+ throw error;
12053
+ }
12054
+ logHermesApiResponse(
12055
+ options.logger,
12056
+ method,
12057
+ path26,
12058
+ options.profileName,
12059
+ startedAt,
12060
+ response
12061
+ );
11671
12062
  return response;
11672
12063
  }
11673
12064
  async function fetchHermesApi(fetcher, config, path26, init, options) {
@@ -15948,11 +16339,15 @@ function createHttpErrorMiddleware(logger) {
15948
16339
  }
15949
16340
 
15950
16341
  // src/hermes/profiles.ts
15951
- import { readdir as readdir9, readFile as readFile12, rename as rename3, rm as rm6, stat as stat12 } from "fs/promises";
16342
+ import { execFile as execFile4 } from "child_process";
16343
+ import { readdir as readdir9, readFile as readFile12, rename as rename3, stat as stat12 } from "fs/promises";
15952
16344
  import path18 from "path";
16345
+ import { promisify as promisify4 } from "util";
15953
16346
  import YAML2 from "yaml";
15954
16347
  var DEFAULT_PROFILE = "default";
15955
16348
  var PROFILE_NAME_PATTERN4 = /^[a-zA-Z0-9._-]{1,64}$/;
16349
+ var PROFILE_DELETE_TIMEOUT_MS = 3e4;
16350
+ var execFileAsync4 = promisify4(execFile4);
15956
16351
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
15957
16352
  const profiles = /* @__PURE__ */ new Map();
15958
16353
  profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
@@ -16024,12 +16419,7 @@ async function updateHermesProfileMetadata(name, metadata, paths = resolveRuntim
16024
16419
  async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
16025
16420
  assertMutableProfile(name);
16026
16421
  const profile = await profileInfo(name, paths);
16027
- const exists = await stat12(profile.path).then((value) => value.isDirectory()).catch((error) => {
16028
- if (isNodeError14(error, "ENOENT")) {
16029
- return false;
16030
- }
16031
- throw error;
16032
- });
16422
+ const exists = await pathExists(profile.path);
16033
16423
  if (!exists) {
16034
16424
  throw new LinkHttpError(
16035
16425
  404,
@@ -16037,7 +16427,14 @@ async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
16037
16427
  `Profile "${name}" does not exist`
16038
16428
  );
16039
16429
  }
16040
- await rm6(resolveHermesProfileDir(name), { recursive: true, force: true });
16430
+ await deleteHermesProfileWithCli(name);
16431
+ if (await pathExists(profile.path)) {
16432
+ throw new LinkHttpError(
16433
+ 502,
16434
+ "hermes_profile_delete_incomplete",
16435
+ `Hermes CLI reported success, but Profile "${name}" still exists at ${profile.path}`
16436
+ );
16437
+ }
16041
16438
  await deleteProfileIdentity(paths, {
16042
16439
  profileName: profile.name,
16043
16440
  profileUid: profile.uid
@@ -16093,6 +16490,52 @@ function assertProfileName(name) {
16093
16490
  throw new LinkHttpError(400, "invalid_profile_name", "invalid profile name");
16094
16491
  }
16095
16492
  }
16493
+ async function pathExists(targetPath) {
16494
+ return await stat12(targetPath).then(() => true).catch((error) => {
16495
+ if (isNodeError14(error, "ENOENT")) {
16496
+ return false;
16497
+ }
16498
+ throw error;
16499
+ });
16500
+ }
16501
+ async function deleteHermesProfileWithCli(name) {
16502
+ try {
16503
+ await execFileAsync4(resolveHermesBin(), ["profile", "delete", name, "--yes"], {
16504
+ timeout: PROFILE_DELETE_TIMEOUT_MS,
16505
+ windowsHide: true
16506
+ });
16507
+ } catch (error) {
16508
+ throw new LinkHttpError(
16509
+ 502,
16510
+ "hermes_profile_delete_failed",
16511
+ `Hermes CLI could not delete Profile "${name}": ${formatExecError(error)}`
16512
+ );
16513
+ }
16514
+ }
16515
+ function formatExecError(error) {
16516
+ if (!(error instanceof Error)) {
16517
+ return String(error);
16518
+ }
16519
+ const output = readExecErrorOutput2(error);
16520
+ return output ? `${error.message}
16521
+ ${output}` : error.message;
16522
+ }
16523
+ function readExecErrorOutput2(error) {
16524
+ const parts = [];
16525
+ if ("stdout" in error && error.stdout != null) {
16526
+ const stdout = String(error.stdout).trim();
16527
+ if (stdout) {
16528
+ parts.push(stdout);
16529
+ }
16530
+ }
16531
+ if ("stderr" in error && error.stderr != null) {
16532
+ const stderr = String(error.stderr).trim();
16533
+ if (stderr) {
16534
+ parts.push(stderr);
16535
+ }
16536
+ }
16537
+ return parts.join("\n");
16538
+ }
16096
16539
  function isNodeError14(error, code) {
16097
16540
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
16098
16541
  }
@@ -16806,7 +17249,7 @@ import {
16806
17249
  cp,
16807
17250
  mkdir as mkdir10,
16808
17251
  readFile as readFile13,
16809
- rm as rm7,
17252
+ rm as rm6,
16810
17253
  stat as stat13
16811
17254
  } from "fs/promises";
16812
17255
  import path19 from "path";
@@ -17107,7 +17550,7 @@ async function generateProfileName(displayName, paths) {
17107
17550
  for (let attempt = 0; attempt < 100; attempt += 1) {
17108
17551
  const suffix = randomSuffix(attempt);
17109
17552
  const candidate = `${base}-${suffix}`.slice(0, 64).replace(/[-_]+$/u, "");
17110
- if (!existing.has(candidate) && !await pathExists(resolveHermesProfileDir(candidate))) {
17553
+ if (!existing.has(candidate) && !await pathExists2(resolveHermesProfileDir(candidate))) {
17111
17554
  return candidate;
17112
17555
  }
17113
17556
  }
@@ -17133,7 +17576,7 @@ function randomSuffix(attempt) {
17133
17576
  }
17134
17577
  async function applyProfileCreationPostSteps(input) {
17135
17578
  const profilePath = resolveHermesProfileDir(input.profileName);
17136
- if (!await pathExists(profilePath)) {
17579
+ if (!await pathExists2(profilePath)) {
17137
17580
  await ensureDirectoryWithInheritedMetadata(profilePath, 448);
17138
17581
  }
17139
17582
  if (input.sourceProfile && input.copyScopes.length > 0) {
@@ -17299,10 +17742,10 @@ async function writeEnvValues(profileName, values) {
17299
17742
  async function copySkills(sourceProfile, targetProfile) {
17300
17743
  const sourceSkills = path19.join(resolveHermesProfileDir(sourceProfile), "skills");
17301
17744
  const targetSkills = path19.join(resolveHermesProfileDir(targetProfile), "skills");
17302
- if (!await pathExists(sourceSkills)) {
17745
+ if (!await pathExists2(sourceSkills)) {
17303
17746
  return;
17304
17747
  }
17305
- await rm7(targetSkills, { recursive: true, force: true });
17748
+ await rm6(targetSkills, { recursive: true, force: true });
17306
17749
  await cp(sourceSkills, targetSkills, {
17307
17750
  recursive: true,
17308
17751
  force: true,
@@ -17350,7 +17793,7 @@ async function failProfileCreation(input) {
17350
17793
  await input.writer.write(`
17351
17794
  Rolling back ${input.rollbackProfileName}...
17352
17795
  `);
17353
- await rm7(resolveHermesProfileDir(input.rollbackProfileName), {
17796
+ await rm6(resolveHermesProfileDir(input.rollbackProfileName), {
17354
17797
  recursive: true,
17355
17798
  force: true
17356
17799
  }).catch(() => void 0);
@@ -17401,14 +17844,14 @@ function profileCreationLogPath(paths) {
17401
17844
  async function clearProfileCreationLogFiles(paths) {
17402
17845
  const primary = profileCreationLogPath(paths);
17403
17846
  await Promise.all([
17404
- rm7(primary, { force: true }).catch(() => void 0),
17847
+ rm6(primary, { force: true }).catch(() => void 0),
17405
17848
  ...Array.from(
17406
17849
  { length: PROFILE_CREATE_LOG_MAX_FILES },
17407
- (_, index) => rm7(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
17850
+ (_, index) => rm6(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
17408
17851
  )
17409
17852
  ]);
17410
17853
  }
17411
- async function pathExists(targetPath) {
17854
+ async function pathExists2(targetPath) {
17412
17855
  return await stat13(targetPath).then(() => true).catch((error) => {
17413
17856
  if (isNodeError15(error, "ENOENT")) {
17414
17857
  return false;
@@ -20260,7 +20703,7 @@ function readModelList(payload) {
20260
20703
  // src/hermes/updates.ts
20261
20704
  import { EventEmitter as EventEmitter3 } from "events";
20262
20705
  import { spawn as spawn3 } from "child_process";
20263
- import { mkdir as mkdir11, readFile as readFile16, rm as rm8 } from "fs/promises";
20706
+ import { mkdir as mkdir11, readFile as readFile16, rm as rm7 } from "fs/promises";
20264
20707
  import path22 from "path";
20265
20708
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
20266
20709
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
@@ -20550,10 +20993,10 @@ function updateLogPath(paths) {
20550
20993
  async function clearUpdateLogFiles(paths) {
20551
20994
  const primary = updateLogPath(paths);
20552
20995
  await Promise.all([
20553
- rm8(primary, { force: true }).catch(() => void 0),
20996
+ rm7(primary, { force: true }).catch(() => void 0),
20554
20997
  ...Array.from(
20555
20998
  { length: UPDATE_LOG_MAX_FILES },
20556
- (_, index) => rm8(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
20999
+ (_, index) => rm7(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
20557
21000
  )
20558
21001
  ]);
20559
21002
  }
@@ -20645,20 +21088,22 @@ function readString17(payload, key) {
20645
21088
  // src/link/updates.ts
20646
21089
  import { spawn as spawn5 } from "child_process";
20647
21090
  import { EventEmitter as EventEmitter4 } from "events";
20648
- import { mkdir as mkdir14, readFile as readFile18, rm as rm11 } from "fs/promises";
21091
+ import { mkdir as mkdir14, readFile as readFile18, rm as rm10 } from "fs/promises";
20649
21092
  import path24 from "path";
20650
21093
 
20651
21094
  // src/daemon/process.ts
20652
21095
  import { spawn as spawn4 } from "child_process";
20653
- import { mkdir as mkdir13, readFile as readFile17, rm as rm10 } from "fs/promises";
21096
+ import { mkdir as mkdir13, readFile as readFile17, rm as rm9 } from "fs/promises";
20654
21097
  import path23 from "path";
20655
21098
 
20656
21099
  // src/daemon/service.ts
20657
21100
  import { createServer } from "http";
20658
- import { mkdir as mkdir12, rm as rm9, writeFile as writeFile3 } from "fs/promises";
21101
+ import { mkdir as mkdir12, rm as rm8, writeFile as writeFile3 } from "fs/promises";
20659
21102
 
20660
21103
  // src/relay/control-client.ts
20661
21104
  import WebSocket from "ws";
21105
+ var RELAY_SSE_BATCH_FLUSH_INTERVAL_MS = 50;
21106
+ var RELAY_SSE_BATCH_FLUSH_BYTES = 2 * 1024;
20662
21107
  function connectRelayControl(options) {
20663
21108
  const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
20664
21109
  wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
@@ -20794,6 +21239,7 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
20794
21239
  }
20795
21240
  const abortController = new AbortController();
20796
21241
  abortControllers.set(frame.id, abortController);
21242
+ let sseBatcher = null;
20797
21243
  try {
20798
21244
  const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
20799
21245
  method: frame.method,
@@ -20805,14 +21251,16 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
20805
21251
  const contentType = response.headers.get("content-type") ?? "";
20806
21252
  if (response.body && contentType.includes("text/event-stream")) {
20807
21253
  socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
21254
+ sseBatcher = createRelayStreamChunkBatcher(socket, frame.id);
20808
21255
  const reader = response.body.getReader();
20809
21256
  while (true) {
20810
21257
  const next = await reader.read();
20811
21258
  if (next.done) {
20812
21259
  break;
20813
21260
  }
20814
- socket.send(JSON.stringify({ type: "http.stream.chunk", id: frame.id, bodyBase64: Buffer.from(next.value).toString("base64") }));
21261
+ sseBatcher.push(next.value);
20815
21262
  }
21263
+ sseBatcher.flush();
20816
21264
  socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
20817
21265
  return;
20818
21266
  }
@@ -20822,15 +21270,73 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
20822
21270
  if (abortController.signal.aborted || isAbortError2(error)) {
20823
21271
  return;
20824
21272
  }
21273
+ sseBatcher?.flush();
20825
21274
  const message = error instanceof Error ? error.message : "Relay request failed";
20826
21275
  socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
20827
21276
  } finally {
21277
+ sseBatcher?.dispose();
20828
21278
  abortControllers.delete(frame.id);
20829
21279
  }
20830
21280
  }
20831
21281
  function isAbortError2(error) {
20832
21282
  return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
20833
21283
  }
21284
+ function createRelayStreamChunkBatcher(socket, id) {
21285
+ let chunks = [];
21286
+ let totalBytes = 0;
21287
+ let flushTimer = null;
21288
+ const clearFlushTimer = () => {
21289
+ if (flushTimer == null) {
21290
+ return;
21291
+ }
21292
+ clearTimeout(flushTimer);
21293
+ flushTimer = null;
21294
+ };
21295
+ const flush = () => {
21296
+ clearFlushTimer();
21297
+ if (totalBytes <= 0) {
21298
+ return;
21299
+ }
21300
+ const bodyBase64 = Buffer.concat(chunks, totalBytes).toString("base64");
21301
+ chunks = [];
21302
+ totalBytes = 0;
21303
+ if (socket.readyState !== WebSocket.OPEN) {
21304
+ return;
21305
+ }
21306
+ socket.send(JSON.stringify({ type: "http.stream.chunk", id, bodyBase64 }));
21307
+ };
21308
+ const scheduleFlush = () => {
21309
+ if (flushTimer != null) {
21310
+ return;
21311
+ }
21312
+ flushTimer = setTimeout(() => {
21313
+ flushTimer = null;
21314
+ flush();
21315
+ }, RELAY_SSE_BATCH_FLUSH_INTERVAL_MS);
21316
+ flushTimer.unref?.();
21317
+ };
21318
+ return {
21319
+ push(chunk) {
21320
+ if (chunk.byteLength <= 0) {
21321
+ return;
21322
+ }
21323
+ const buffer = Buffer.from(chunk);
21324
+ chunks.push(buffer);
21325
+ totalBytes += buffer.byteLength;
21326
+ if (totalBytes >= RELAY_SSE_BATCH_FLUSH_BYTES) {
21327
+ flush();
21328
+ return;
21329
+ }
21330
+ scheduleFlush();
21331
+ },
21332
+ flush,
21333
+ dispose() {
21334
+ clearFlushTimer();
21335
+ chunks = [];
21336
+ totalBytes = 0;
21337
+ }
21338
+ };
21339
+ }
20834
21340
 
20835
21341
  // src/runtime/system-info.ts
20836
21342
  import { execFileSync } from "child_process";
@@ -21705,7 +22211,7 @@ async function startLinkService(options = {}) {
21705
22211
  await logger.info("service_stopped");
21706
22212
  await logger.flush();
21707
22213
  if (options.writePidFile) {
21708
- await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
22214
+ await rm8(pidFilePath(paths), { force: true }).catch(() => void 0);
21709
22215
  }
21710
22216
  }
21711
22217
  };
@@ -21897,7 +22403,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
21897
22403
  try {
21898
22404
  process.kill(status.pid, "SIGTERM");
21899
22405
  } catch {
21900
- await rm10(pidFilePath(paths), { force: true }).catch(() => void 0);
22406
+ await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
21901
22407
  return await getDaemonStatus(paths);
21902
22408
  }
21903
22409
  for (let index = 0; index < 20; index += 1) {
@@ -21919,7 +22425,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
21919
22425
  }
21920
22426
  }
21921
22427
  if (!isProcessAlive3(status.pid) || !await pidBackedServiceIsReachable(paths)) {
21922
- await rm10(pidFilePath(paths), { force: true }).catch(() => void 0);
22428
+ await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
21923
22429
  }
21924
22430
  return await getDaemonStatus(paths);
21925
22431
  }
@@ -21927,7 +22433,7 @@ async function getDaemonStatus(paths = resolveRuntimePaths()) {
21927
22433
  const pidFile = pidFilePath(paths);
21928
22434
  const pid = await readPid(pidFile);
21929
22435
  if (pid && !isProcessAlive3(pid)) {
21930
- await rm10(pidFile, { force: true }).catch(() => void 0);
22436
+ await rm9(pidFile, { force: true }).catch(() => void 0);
21931
22437
  return {
21932
22438
  running: false,
21933
22439
  pid: null,
@@ -22382,10 +22888,10 @@ function updateLogPath2(paths) {
22382
22888
  async function clearUpdateLogFiles2(paths) {
22383
22889
  const primary = updateLogPath2(paths);
22384
22890
  await Promise.all([
22385
- rm11(primary, { force: true }).catch(() => void 0),
22891
+ rm10(primary, { force: true }).catch(() => void 0),
22386
22892
  ...Array.from(
22387
22893
  { length: UPDATE_LOG_MAX_FILES2 },
22388
- (_, index) => rm11(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
22894
+ (_, index) => rm10(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
22389
22895
  )
22390
22896
  ]);
22391
22897
  }
@@ -22442,7 +22948,7 @@ function readString18(payload, key) {
22442
22948
 
22443
22949
  // src/pairing/pairing.ts
22444
22950
  import path25 from "path";
22445
- import { rm as rm12 } from "fs/promises";
22951
+ import { rm as rm11 } from "fs/promises";
22446
22952
 
22447
22953
  // src/relay/bootstrap.ts
22448
22954
  var RelayNetworkError = class extends Error {
@@ -22704,7 +23210,7 @@ async function readPairingClaim(sessionId, paths = resolveRuntimePaths()) {
22704
23210
  };
22705
23211
  }
22706
23212
  async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
22707
- await rm12(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
23213
+ await rm11(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
22708
23214
  }
22709
23215
  async function claimPairing(input) {
22710
23216
  const paths = input.paths ?? resolveRuntimePaths();
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-DUQDO2LQ.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-DUQDO2LQ.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.6",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",