@hermespilot/link 0.4.5 → 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) {
@@ -3670,14 +3749,82 @@ async function writeHermesEnvValue(profileName, key, value) {
3670
3749
  }
3671
3750
  await atomicWriteFilePreservingMetadata(envPath, nextRaw);
3672
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
+ }
3673
3820
  function applyEnvOverrides(config, env, withDefaults) {
3674
- const host = config.host ?? env.host;
3675
- const port = config.port ?? env.port;
3821
+ const host = env.host ?? config.host;
3822
+ const port = env.port ?? config.port;
3676
3823
  return {
3677
- enabled: config.enabled ?? env.enabled,
3824
+ enabled: env.enabled ?? config.enabled,
3678
3825
  host: withDefaults ? host ?? DEFAULT_HERMES_API_SERVER_HOST : host,
3679
3826
  port: withDefaults ? port ?? DEFAULT_HERMES_API_SERVER_PORT : port,
3680
- key: config.key ?? env.key
3827
+ key: env.key ?? config.key
3681
3828
  };
3682
3829
  }
3683
3830
  function parseEnvBoolean(value) {
@@ -3985,7 +4132,7 @@ import os2 from "os";
3985
4132
  import path5 from "path";
3986
4133
 
3987
4134
  // src/constants.ts
3988
- var LINK_VERSION = "0.4.5";
4135
+ var LINK_VERSION = "0.4.6";
3989
4136
  var LINK_COMMAND = "hermeslink";
3990
4137
  var LINK_DEFAULT_PORT = 52379;
3991
4138
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4548,6 +4695,47 @@ async function ensureHermesApiServerAvailable(options = {}) {
4548
4695
  });
4549
4696
  return { available: true, configResult, started: true, start, health };
4550
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
+ }
4551
4739
  const logHint = await readRecentGatewayLogHint(start.logPath);
4552
4740
  void options.logger?.error("gateway_auto_start_failed", {
4553
4741
  profile: profileName,
@@ -4638,12 +4826,9 @@ async function readHermesVersion(options = {}) {
4638
4826
  }
4639
4827
  async function execHermesVersion(hermesBin, logger) {
4640
4828
  const failures = [];
4641
- for (const args of [["version"], ["--version"]]) {
4829
+ for (const args of [["--version"], ["version"]]) {
4642
4830
  try {
4643
- return await execFileAsync2(hermesBin, args, {
4644
- timeout: 5e3,
4645
- windowsHide: true
4646
- });
4831
+ return await spawnHermesVersionCommand(hermesBin, args, 5e3);
4647
4832
  } catch (error) {
4648
4833
  const failure = describeVersionCommandFailure(hermesBin, args, error);
4649
4834
  failures.push(failure);
@@ -4659,6 +4844,85 @@ async function execHermesVersion(hermesBin, logger) {
4659
4844
  });
4660
4845
  throw new Error(summary);
4661
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
+ }
4662
4926
  function assertHermesRunsApiSupported(version, status) {
4663
4927
  if (status !== 404) {
4664
4928
  return;
@@ -4916,11 +5180,9 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
4916
5180
  detailed: record
4917
5181
  };
4918
5182
  }
4919
- if (resolvedConfig.key) {
4920
- const authIssue = await probeHermesApiServerAuth(resolvedConfig, fetcher);
4921
- if (authIssue) {
4922
- return authIssue;
4923
- }
5183
+ const authIssue = await probeHermesApiServerAuth(resolvedConfig, fetcher);
5184
+ if (authIssue) {
5185
+ return authIssue;
4924
5186
  }
4925
5187
  return { healthy: true };
4926
5188
  }
@@ -4937,9 +5199,6 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
4937
5199
  };
4938
5200
  }
4939
5201
  async function probeHermesApiServerAuth(config, fetcher) {
4940
- if (!config.key) {
4941
- return null;
4942
- }
4943
5202
  const response = await fetchWithTimeout(
4944
5203
  `http://127.0.0.1:${config.port}/v1/models`,
4945
5204
  {
@@ -5150,6 +5409,21 @@ async function probeHermesVersion(hermesBin, options) {
5150
5409
  }
5151
5410
  }
5152
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
+ }
5153
5427
  function describeVersionCommandFailure(hermesBin, args, error) {
5154
5428
  const message = error instanceof Error ? error.message : String(error);
5155
5429
  const details = readExecErrorDetails(error, message);
@@ -11745,6 +12019,46 @@ async function callHermesApi(path26, init, options) {
11745
12019
  startedAt,
11746
12020
  response
11747
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
+ );
11748
12062
  return response;
11749
12063
  }
11750
12064
  async function fetchHermesApi(fetcher, config, path26, init, options) {
@@ -16025,11 +16339,15 @@ function createHttpErrorMiddleware(logger) {
16025
16339
  }
16026
16340
 
16027
16341
  // src/hermes/profiles.ts
16028
- 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";
16029
16344
  import path18 from "path";
16345
+ import { promisify as promisify4 } from "util";
16030
16346
  import YAML2 from "yaml";
16031
16347
  var DEFAULT_PROFILE = "default";
16032
16348
  var PROFILE_NAME_PATTERN4 = /^[a-zA-Z0-9._-]{1,64}$/;
16349
+ var PROFILE_DELETE_TIMEOUT_MS = 3e4;
16350
+ var execFileAsync4 = promisify4(execFile4);
16033
16351
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
16034
16352
  const profiles = /* @__PURE__ */ new Map();
16035
16353
  profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
@@ -16101,12 +16419,7 @@ async function updateHermesProfileMetadata(name, metadata, paths = resolveRuntim
16101
16419
  async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
16102
16420
  assertMutableProfile(name);
16103
16421
  const profile = await profileInfo(name, paths);
16104
- const exists = await stat12(profile.path).then((value) => value.isDirectory()).catch((error) => {
16105
- if (isNodeError14(error, "ENOENT")) {
16106
- return false;
16107
- }
16108
- throw error;
16109
- });
16422
+ const exists = await pathExists(profile.path);
16110
16423
  if (!exists) {
16111
16424
  throw new LinkHttpError(
16112
16425
  404,
@@ -16114,7 +16427,14 @@ async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
16114
16427
  `Profile "${name}" does not exist`
16115
16428
  );
16116
16429
  }
16117
- 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
+ }
16118
16438
  await deleteProfileIdentity(paths, {
16119
16439
  profileName: profile.name,
16120
16440
  profileUid: profile.uid
@@ -16170,6 +16490,52 @@ function assertProfileName(name) {
16170
16490
  throw new LinkHttpError(400, "invalid_profile_name", "invalid profile name");
16171
16491
  }
16172
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
+ }
16173
16539
  function isNodeError14(error, code) {
16174
16540
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
16175
16541
  }
@@ -16883,7 +17249,7 @@ import {
16883
17249
  cp,
16884
17250
  mkdir as mkdir10,
16885
17251
  readFile as readFile13,
16886
- rm as rm7,
17252
+ rm as rm6,
16887
17253
  stat as stat13
16888
17254
  } from "fs/promises";
16889
17255
  import path19 from "path";
@@ -17184,7 +17550,7 @@ async function generateProfileName(displayName, paths) {
17184
17550
  for (let attempt = 0; attempt < 100; attempt += 1) {
17185
17551
  const suffix = randomSuffix(attempt);
17186
17552
  const candidate = `${base}-${suffix}`.slice(0, 64).replace(/[-_]+$/u, "");
17187
- if (!existing.has(candidate) && !await pathExists(resolveHermesProfileDir(candidate))) {
17553
+ if (!existing.has(candidate) && !await pathExists2(resolveHermesProfileDir(candidate))) {
17188
17554
  return candidate;
17189
17555
  }
17190
17556
  }
@@ -17210,7 +17576,7 @@ function randomSuffix(attempt) {
17210
17576
  }
17211
17577
  async function applyProfileCreationPostSteps(input) {
17212
17578
  const profilePath = resolveHermesProfileDir(input.profileName);
17213
- if (!await pathExists(profilePath)) {
17579
+ if (!await pathExists2(profilePath)) {
17214
17580
  await ensureDirectoryWithInheritedMetadata(profilePath, 448);
17215
17581
  }
17216
17582
  if (input.sourceProfile && input.copyScopes.length > 0) {
@@ -17376,10 +17742,10 @@ async function writeEnvValues(profileName, values) {
17376
17742
  async function copySkills(sourceProfile, targetProfile) {
17377
17743
  const sourceSkills = path19.join(resolveHermesProfileDir(sourceProfile), "skills");
17378
17744
  const targetSkills = path19.join(resolveHermesProfileDir(targetProfile), "skills");
17379
- if (!await pathExists(sourceSkills)) {
17745
+ if (!await pathExists2(sourceSkills)) {
17380
17746
  return;
17381
17747
  }
17382
- await rm7(targetSkills, { recursive: true, force: true });
17748
+ await rm6(targetSkills, { recursive: true, force: true });
17383
17749
  await cp(sourceSkills, targetSkills, {
17384
17750
  recursive: true,
17385
17751
  force: true,
@@ -17427,7 +17793,7 @@ async function failProfileCreation(input) {
17427
17793
  await input.writer.write(`
17428
17794
  Rolling back ${input.rollbackProfileName}...
17429
17795
  `);
17430
- await rm7(resolveHermesProfileDir(input.rollbackProfileName), {
17796
+ await rm6(resolveHermesProfileDir(input.rollbackProfileName), {
17431
17797
  recursive: true,
17432
17798
  force: true
17433
17799
  }).catch(() => void 0);
@@ -17478,14 +17844,14 @@ function profileCreationLogPath(paths) {
17478
17844
  async function clearProfileCreationLogFiles(paths) {
17479
17845
  const primary = profileCreationLogPath(paths);
17480
17846
  await Promise.all([
17481
- rm7(primary, { force: true }).catch(() => void 0),
17847
+ rm6(primary, { force: true }).catch(() => void 0),
17482
17848
  ...Array.from(
17483
17849
  { length: PROFILE_CREATE_LOG_MAX_FILES },
17484
- (_, index) => rm7(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
17850
+ (_, index) => rm6(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
17485
17851
  )
17486
17852
  ]);
17487
17853
  }
17488
- async function pathExists(targetPath) {
17854
+ async function pathExists2(targetPath) {
17489
17855
  return await stat13(targetPath).then(() => true).catch((error) => {
17490
17856
  if (isNodeError15(error, "ENOENT")) {
17491
17857
  return false;
@@ -20337,7 +20703,7 @@ function readModelList(payload) {
20337
20703
  // src/hermes/updates.ts
20338
20704
  import { EventEmitter as EventEmitter3 } from "events";
20339
20705
  import { spawn as spawn3 } from "child_process";
20340
- 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";
20341
20707
  import path22 from "path";
20342
20708
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
20343
20709
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
@@ -20627,10 +20993,10 @@ function updateLogPath(paths) {
20627
20993
  async function clearUpdateLogFiles(paths) {
20628
20994
  const primary = updateLogPath(paths);
20629
20995
  await Promise.all([
20630
- rm8(primary, { force: true }).catch(() => void 0),
20996
+ rm7(primary, { force: true }).catch(() => void 0),
20631
20997
  ...Array.from(
20632
20998
  { length: UPDATE_LOG_MAX_FILES },
20633
- (_, index) => rm8(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
20999
+ (_, index) => rm7(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
20634
21000
  )
20635
21001
  ]);
20636
21002
  }
@@ -20722,17 +21088,17 @@ function readString17(payload, key) {
20722
21088
  // src/link/updates.ts
20723
21089
  import { spawn as spawn5 } from "child_process";
20724
21090
  import { EventEmitter as EventEmitter4 } from "events";
20725
- 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";
20726
21092
  import path24 from "path";
20727
21093
 
20728
21094
  // src/daemon/process.ts
20729
21095
  import { spawn as spawn4 } from "child_process";
20730
- 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";
20731
21097
  import path23 from "path";
20732
21098
 
20733
21099
  // src/daemon/service.ts
20734
21100
  import { createServer } from "http";
20735
- 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";
20736
21102
 
20737
21103
  // src/relay/control-client.ts
20738
21104
  import WebSocket from "ws";
@@ -21845,7 +22211,7 @@ async function startLinkService(options = {}) {
21845
22211
  await logger.info("service_stopped");
21846
22212
  await logger.flush();
21847
22213
  if (options.writePidFile) {
21848
- await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
22214
+ await rm8(pidFilePath(paths), { force: true }).catch(() => void 0);
21849
22215
  }
21850
22216
  }
21851
22217
  };
@@ -22037,7 +22403,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
22037
22403
  try {
22038
22404
  process.kill(status.pid, "SIGTERM");
22039
22405
  } catch {
22040
- await rm10(pidFilePath(paths), { force: true }).catch(() => void 0);
22406
+ await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
22041
22407
  return await getDaemonStatus(paths);
22042
22408
  }
22043
22409
  for (let index = 0; index < 20; index += 1) {
@@ -22059,7 +22425,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
22059
22425
  }
22060
22426
  }
22061
22427
  if (!isProcessAlive3(status.pid) || !await pidBackedServiceIsReachable(paths)) {
22062
- await rm10(pidFilePath(paths), { force: true }).catch(() => void 0);
22428
+ await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
22063
22429
  }
22064
22430
  return await getDaemonStatus(paths);
22065
22431
  }
@@ -22067,7 +22433,7 @@ async function getDaemonStatus(paths = resolveRuntimePaths()) {
22067
22433
  const pidFile = pidFilePath(paths);
22068
22434
  const pid = await readPid(pidFile);
22069
22435
  if (pid && !isProcessAlive3(pid)) {
22070
- await rm10(pidFile, { force: true }).catch(() => void 0);
22436
+ await rm9(pidFile, { force: true }).catch(() => void 0);
22071
22437
  return {
22072
22438
  running: false,
22073
22439
  pid: null,
@@ -22522,10 +22888,10 @@ function updateLogPath2(paths) {
22522
22888
  async function clearUpdateLogFiles2(paths) {
22523
22889
  const primary = updateLogPath2(paths);
22524
22890
  await Promise.all([
22525
- rm11(primary, { force: true }).catch(() => void 0),
22891
+ rm10(primary, { force: true }).catch(() => void 0),
22526
22892
  ...Array.from(
22527
22893
  { length: UPDATE_LOG_MAX_FILES2 },
22528
- (_, index) => rm11(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
22894
+ (_, index) => rm10(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
22529
22895
  )
22530
22896
  ]);
22531
22897
  }
@@ -22582,7 +22948,7 @@ function readString18(payload, key) {
22582
22948
 
22583
22949
  // src/pairing/pairing.ts
22584
22950
  import path25 from "path";
22585
- import { rm as rm12 } from "fs/promises";
22951
+ import { rm as rm11 } from "fs/promises";
22586
22952
 
22587
22953
  // src/relay/bootstrap.ts
22588
22954
  var RelayNetworkError = class extends Error {
@@ -22844,7 +23210,7 @@ async function readPairingClaim(sessionId, paths = resolveRuntimePaths()) {
22844
23210
  };
22845
23211
  }
22846
23212
  async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
22847
- await rm12(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
23213
+ await rm11(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
22848
23214
  }
22849
23215
  async function claimPairing(input) {
22850
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-UANE2YHT.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-UANE2YHT.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.5",
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",