@hermespilot/link 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1649,6 +1649,185 @@ async function listHermesModelConfigs(profileName = "default", configPath = reso
1649
1649
  models
1650
1650
  };
1651
1651
  }
1652
+ async function listHermesModelConfigCatalog(input) {
1653
+ const targetProfileName = input.targetProfileName?.trim() || null;
1654
+ const targetModels = targetProfileName ? await listHermesModelConfigs(targetProfileName).then((result) => result.models).catch(() => []) : [];
1655
+ const targetKeys = new Set(
1656
+ targetModels.map(
1657
+ (model) => modelConfigKey(model.provider, model.baseUrl, model.id)
1658
+ )
1659
+ );
1660
+ const items = /* @__PURE__ */ new Map();
1661
+ for (const profile of input.profiles) {
1662
+ const profileName = profile.name.trim();
1663
+ if (!profileName) {
1664
+ continue;
1665
+ }
1666
+ const listed = await listHermesModelConfigs(profileName).catch(() => null);
1667
+ if (!listed) {
1668
+ continue;
1669
+ }
1670
+ for (const model of listed.models) {
1671
+ const key = modelConfigKey(model.provider, model.baseUrl, model.id);
1672
+ const existing = items.get(key);
1673
+ if (existing) {
1674
+ if (!existing.sourceProfiles.some(
1675
+ (source) => source.name === profileName
1676
+ )) {
1677
+ existing.sourceProfiles.push({
1678
+ name: profileName,
1679
+ displayName: profile.displayName
1680
+ });
1681
+ }
1682
+ existing.alreadyAdded = existing.alreadyAdded || targetKeys.has(key);
1683
+ existing.isDefault = existing.isDefault || model.isDefault;
1684
+ if (existing.credentialState !== "configured" && model.credentialState === "configured") {
1685
+ existing.credentialState = "configured";
1686
+ }
1687
+ continue;
1688
+ }
1689
+ items.set(key, {
1690
+ id: model.id,
1691
+ provider: model.provider,
1692
+ providerName: model.providerName,
1693
+ baseUrl: model.baseUrl,
1694
+ apiMode: model.apiMode,
1695
+ ...model.contextLength ? { contextLength: model.contextLength } : {},
1696
+ ...model.keyEnv ? { keyEnv: model.keyEnv } : {},
1697
+ credentialState: model.credentialState,
1698
+ isDefault: model.isDefault,
1699
+ ...model.reasoningEffort ? { reasoningEffort: model.reasoningEffort } : {},
1700
+ reasoningSupport: model.reasoningSupport,
1701
+ supportedReasoningEfforts: model.supportedReasoningEfforts,
1702
+ sourceProfiles: [
1703
+ {
1704
+ name: profileName,
1705
+ displayName: profile.displayName
1706
+ }
1707
+ ],
1708
+ alreadyAdded: targetKeys.has(key)
1709
+ });
1710
+ }
1711
+ }
1712
+ return {
1713
+ ok: true,
1714
+ targetProfileName,
1715
+ models: [...items.values()].sort(compareCatalogItems)
1716
+ };
1717
+ }
1718
+ async function importHermesModelConfig(input, targetProfileName = "default", targetConfigPath = resolveHermesConfigPath(targetProfileName)) {
1719
+ const sourceProfileName = input.sourceProfileName.trim();
1720
+ const modelId = input.modelId.trim();
1721
+ if (!sourceProfileName || !modelId) {
1722
+ throw new Error("sourceProfileName and modelId are required");
1723
+ }
1724
+ const source = await readHermesModelConfigForImport({
1725
+ profileName: sourceProfileName,
1726
+ modelId,
1727
+ provider: input.provider?.trim(),
1728
+ baseUrl: input.baseUrl?.trim(),
1729
+ apiMode: input.apiMode?.trim()
1730
+ });
1731
+ const { document, config, existingRaw } = await readHermesConfigDocument(targetConfigPath);
1732
+ const targetEnv = await readHermesEnvFile(targetProfileName);
1733
+ const customProviders = ensureCustomProvidersList(config);
1734
+ const targetEntryIndex = findCustomProviderIndexByEndpoint(customProviders, {
1735
+ provider: source.model.provider,
1736
+ baseUrl: source.model.baseUrl
1737
+ });
1738
+ const entry = targetEntryIndex >= 0 ? toRecord(customProviders[targetEntryIndex]) : {};
1739
+ const entryHadModels = readEntryModelIds(entry).length > 0;
1740
+ const existingKeyEnv = readString2(entry.key_env) ?? parseEnvReference(readString2(entry.api_key));
1741
+ const existingInlineApiKey = readInlineApiKey(readString2(entry.api_key));
1742
+ const sourceKeyEnv = source.model.keyEnv;
1743
+ const sourceApiKey = sourceKeyEnv ? source.env[sourceKeyEnv]?.trim() : source.inlineApiKey;
1744
+ let keyEnv = existingKeyEnv;
1745
+ if (!keyEnv && !existingInlineApiKey) {
1746
+ keyEnv = sourceKeyEnv ?? (sourceApiKey ? buildApiKeyEnvName(source.model.providerName, source.model.id) : void 0);
1747
+ }
1748
+ if (sourceApiKey && keyEnv && !existingInlineApiKey && (!existingKeyEnv || !targetEnv[keyEnv]?.trim())) {
1749
+ await writeHermesEnvValue(targetProfileName, keyEnv, sourceApiKey);
1750
+ }
1751
+ entry.name = readString2(entry.name) ?? readString2(entry.provider_name) ?? source.model.providerName;
1752
+ entry.provider_key = source.model.provider;
1753
+ entry.base_url = source.model.baseUrl;
1754
+ if (!entryHadModels) {
1755
+ entry.model = source.model.id;
1756
+ }
1757
+ entry.api_mode = source.model.apiMode;
1758
+ addEntryModel(entry, source.model.id);
1759
+ if (source.model.contextLength) {
1760
+ writeEntryModelContextLength(
1761
+ entry,
1762
+ source.model.id,
1763
+ source.model.contextLength
1764
+ );
1765
+ } else if (!entryHadModels) {
1766
+ delete entry.context_length;
1767
+ }
1768
+ if (keyEnv) {
1769
+ entry.key_env = keyEnv;
1770
+ delete entry.api_key;
1771
+ } else {
1772
+ delete entry.key_env;
1773
+ if (!existingInlineApiKey) {
1774
+ delete entry.api_key;
1775
+ }
1776
+ }
1777
+ writeEntryModelReasoningEffort(
1778
+ entry,
1779
+ source.model.id,
1780
+ source.model.reasoningEffort
1781
+ );
1782
+ if (targetEntryIndex >= 0) {
1783
+ customProviders[targetEntryIndex] = entry;
1784
+ } else {
1785
+ customProviders.push(entry);
1786
+ }
1787
+ const modelConfig = ensureRecord(config, "model");
1788
+ const currentDefaultConfig = readModelConfig(modelConfig);
1789
+ const currentDefaultReasoningEffort = readProfileReasoningEffort(config);
1790
+ if (input.setDefault || !currentDefaultConfig.model) {
1791
+ if (input.setDefault && currentDefaultConfig.model && currentDefaultConfig.model !== source.model.id) {
1792
+ retainModelDefaultAsCustomProvider(customProviders, {
1793
+ ...currentDefaultConfig,
1794
+ ...currentDefaultReasoningEffort ? { reasoningEffort: currentDefaultReasoningEffort } : {}
1795
+ });
1796
+ }
1797
+ writeDefaultModelConfig(modelConfig, {
1798
+ id: source.model.id,
1799
+ provider: source.model.provider,
1800
+ baseUrl: source.model.baseUrl,
1801
+ apiMode: source.model.apiMode,
1802
+ contextLength: source.model.contextLength,
1803
+ keyEnv
1804
+ });
1805
+ if (source.model.reasoningEffort) {
1806
+ writeProfileReasoningEffort(config, source.model.reasoningEffort);
1807
+ }
1808
+ }
1809
+ const backupPath = await writeHermesConfigDocument({
1810
+ configPath: targetConfigPath,
1811
+ document,
1812
+ config,
1813
+ existingRaw
1814
+ });
1815
+ const listed = await listHermesModelConfigs(targetProfileName, targetConfigPath);
1816
+ const importedModel = listed.models.find(
1817
+ (model) => model.id === source.model.id && model.provider === source.model.provider && model.baseUrl === source.model.baseUrl
1818
+ ) ?? listed.models.find((model) => model.id === source.model.id);
1819
+ if (!importedModel) {
1820
+ throw new Error("imported model is missing from config");
1821
+ }
1822
+ return {
1823
+ ...listed,
1824
+ model: importedModel,
1825
+ sourceProfileName,
1826
+ backupPath,
1827
+ requiresGatewayReload: true,
1828
+ restartHint: MODEL_CONFIG_RESTART_HINT
1829
+ };
1830
+ }
1652
1831
  async function saveHermesModelConfig(input, profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
1653
1832
  const normalized = normalizeModelConfigInput(input);
1654
1833
  const shouldUpdateReasoningEffort = input.reasoningEffort !== void 0;
@@ -2620,6 +2799,14 @@ function findCustomProviderIndex(entries, modelId) {
2620
2799
  (entry) => readEntryModelIds(toRecord(entry)).includes(modelId)
2621
2800
  );
2622
2801
  }
2802
+ function findCustomProviderIndexByEndpoint(entries, endpoint) {
2803
+ return entries.findIndex((entry) => {
2804
+ const record = toRecord(entry);
2805
+ const provider = readString2(record.provider_key) ?? readString2(record.provider) ?? "custom";
2806
+ const baseUrl = readString2(record.base_url) ?? readString2(record.url) ?? readString2(record.api) ?? "";
2807
+ return provider.trim().toLowerCase() === endpoint.provider.trim().toLowerCase() && normalizeBaseUrl(baseUrl) === normalizeBaseUrl(endpoint.baseUrl);
2808
+ });
2809
+ }
2623
2810
  function updateEntryModels(entry, originalModelId, nextModelId) {
2624
2811
  const models = entry.models;
2625
2812
  if (Array.isArray(models)) {
@@ -2638,6 +2825,39 @@ function updateEntryModels(entry, originalModelId, nextModelId) {
2638
2825
  delete record[originalModelId];
2639
2826
  }
2640
2827
  }
2828
+ function addEntryModel(entry, modelId) {
2829
+ const id = modelId.trim();
2830
+ if (!id) {
2831
+ return;
2832
+ }
2833
+ const models = entry.models;
2834
+ if (Array.isArray(models)) {
2835
+ entry.models = Object.fromEntries(
2836
+ Array.from(
2837
+ /* @__PURE__ */ new Set([
2838
+ ...models.filter(
2839
+ (value) => typeof value === "string" && value.trim().length > 0
2840
+ ),
2841
+ id
2842
+ ])
2843
+ ).map((model) => [model, {}])
2844
+ );
2845
+ return;
2846
+ }
2847
+ if (typeof models === "object" && models !== null) {
2848
+ const record = models;
2849
+ record[id] = toRecord(record[id]);
2850
+ return;
2851
+ }
2852
+ if (readString2(entry.model) !== id && readString2(entry.default_model) !== id) {
2853
+ entry.models = Object.fromEntries(
2854
+ Array.from(/* @__PURE__ */ new Set([...readEntryModelIds(entry), id])).map((model) => [
2855
+ model,
2856
+ {}
2857
+ ])
2858
+ );
2859
+ }
2860
+ }
2641
2861
  function removeModelFromCustomProvider(entry, modelId) {
2642
2862
  if (readString2(entry.model) === modelId || readString2(entry.default_model) === modelId) {
2643
2863
  delete entry.model;
@@ -2687,6 +2907,27 @@ function readEntryModelContextLength(entry, modelId) {
2687
2907
  modelConfig.context_length ?? modelConfig.contextLength
2688
2908
  );
2689
2909
  }
2910
+ function writeEntryModelContextLength(entry, modelId, contextLength) {
2911
+ const models = entry.models;
2912
+ if (typeof models === "object" && models !== null && !Array.isArray(models)) {
2913
+ const modelMap = models;
2914
+ const modelConfig = toRecord(modelMap[modelId]);
2915
+ if (contextLength) {
2916
+ modelConfig.context_length = contextLength;
2917
+ } else {
2918
+ delete modelConfig.context_length;
2919
+ delete modelConfig.contextLength;
2920
+ }
2921
+ modelMap[modelId] = modelConfig;
2922
+ return;
2923
+ }
2924
+ if (contextLength) {
2925
+ entry.context_length = contextLength;
2926
+ } else {
2927
+ delete entry.context_length;
2928
+ delete entry.contextLength;
2929
+ }
2930
+ }
2690
2931
  function readEntryModelReasoningEffort(entry, modelId) {
2691
2932
  const models = entry.models;
2692
2933
  if (typeof models === "object" && models !== null && !Array.isArray(models)) {
@@ -2723,6 +2964,80 @@ function writeEntryModelReasoningEffort(entry, modelId, reasoningEffort) {
2723
2964
  delete entry.reasoningEffort;
2724
2965
  }
2725
2966
  }
2967
+ async function readHermesModelConfigForImport(input) {
2968
+ const { config } = await readHermesConfigDocument(
2969
+ resolveHermesConfigPath(input.profileName)
2970
+ );
2971
+ const env = await readHermesEnvFile(input.profileName);
2972
+ const models = readManagedModelConfigs(
2973
+ config,
2974
+ env,
2975
+ readModelConfig(config.model).model ?? null,
2976
+ readProfileReasoningEffort(config)
2977
+ );
2978
+ const model = models.find((candidate) => {
2979
+ if (candidate.id !== input.modelId) {
2980
+ return false;
2981
+ }
2982
+ if (input.provider && candidate.provider !== input.provider) {
2983
+ return false;
2984
+ }
2985
+ if (input.baseUrl !== void 0 && normalizeBaseUrl(candidate.baseUrl) !== normalizeBaseUrl(input.baseUrl)) {
2986
+ return false;
2987
+ }
2988
+ if (input.apiMode && candidate.apiMode !== input.apiMode) {
2989
+ return false;
2990
+ }
2991
+ return true;
2992
+ });
2993
+ if (!model) {
2994
+ throw new Error(`model "${input.modelId}" is not configured`);
2995
+ }
2996
+ return {
2997
+ model,
2998
+ env,
2999
+ inlineApiKey: readInlineApiKeyForModel(config, model)
3000
+ };
3001
+ }
3002
+ function readInlineApiKeyForModel(config, model) {
3003
+ const defaultConfig = readModelConfig(config.model);
3004
+ if (defaultConfig.model === model.id && (defaultConfig.provider ?? "default") === model.provider && normalizeBaseUrl(defaultConfig.baseUrl ?? "") === normalizeBaseUrl(model.baseUrl)) {
3005
+ const key = readInlineApiKey(defaultConfig.apiKey);
3006
+ if (key) {
3007
+ return key;
3008
+ }
3009
+ }
3010
+ const customProviders = Array.isArray(config.custom_providers) ? config.custom_providers : [];
3011
+ for (const rawEntry of customProviders) {
3012
+ const entry = toRecord(rawEntry);
3013
+ const provider = readString2(entry.provider_key) ?? readString2(entry.provider) ?? "custom";
3014
+ const baseUrl = readString2(entry.base_url) ?? readString2(entry.url) ?? readString2(entry.api) ?? "";
3015
+ if (provider === model.provider && normalizeBaseUrl(baseUrl) === normalizeBaseUrl(model.baseUrl) && readEntryModelIds(entry).includes(model.id)) {
3016
+ const key = readInlineApiKey(readString2(entry.api_key));
3017
+ if (key) {
3018
+ return key;
3019
+ }
3020
+ }
3021
+ }
3022
+ return void 0;
3023
+ }
3024
+ function readInlineApiKey(value) {
3025
+ if (!value || parseEnvReference(value)) {
3026
+ return void 0;
3027
+ }
3028
+ return value.trim() || void 0;
3029
+ }
3030
+ function compareCatalogItems(left, right) {
3031
+ const provider = left.providerName.localeCompare(right.providerName);
3032
+ if (provider !== 0) {
3033
+ return provider;
3034
+ }
3035
+ const baseUrl = left.baseUrl.localeCompare(right.baseUrl);
3036
+ if (baseUrl !== 0) {
3037
+ return baseUrl;
3038
+ }
3039
+ return left.id.localeCompare(right.id);
3040
+ }
2726
3041
  function readCredentialState(entry, env) {
2727
3042
  const apiKey = readString2(entry.api_key);
2728
3043
  const keyEnv = readString2(entry.key_env) ?? parseEnvReference(apiKey);
@@ -2744,10 +3059,13 @@ function readModelCredentialState(model, env) {
2744
3059
  return env[model.keyEnv]?.trim() ? "configured" : "missing";
2745
3060
  }
2746
3061
  function modelConfigKey(provider, baseUrl, modelId) {
2747
- return [provider, baseUrl.replace(/\/+$/u, ""), modelId].join("\n").toLowerCase();
3062
+ return [provider, normalizeBaseUrl(baseUrl), modelId].join("\n").toLowerCase();
2748
3063
  }
2749
3064
  function modelEndpointKey(baseUrl, modelId) {
2750
- return [baseUrl.replace(/\/+$/u, ""), modelId].join("\n").toLowerCase();
3065
+ return [normalizeBaseUrl(baseUrl), modelId].join("\n").toLowerCase();
3066
+ }
3067
+ function normalizeBaseUrl(baseUrl) {
3068
+ return baseUrl.trim().replace(/\/+$/u, "");
2751
3069
  }
2752
3070
  function inferApiMode(provider, baseUrl, explicit) {
2753
3071
  const normalizedExplicit = explicit?.trim();
@@ -4107,7 +4425,7 @@ async function listCronOutputFiles(profileName, jobId) {
4107
4425
  mtimeMs: fileStat.mtimeMs
4108
4426
  });
4109
4427
  }
4110
- return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path27, mtime }) => ({ path: path27, mtime }));
4428
+ return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path26, mtime }) => ({ path: path26, mtime }));
4111
4429
  }
4112
4430
  async function readCronOutput(outputPath) {
4113
4431
  const content = await readFile3(outputPath, "utf8");
@@ -4184,7 +4502,7 @@ import os2 from "os";
4184
4502
  import path5 from "path";
4185
4503
 
4186
4504
  // src/constants.ts
4187
- var LINK_VERSION = "0.5.1";
4505
+ var LINK_VERSION = "0.5.2";
4188
4506
  var LINK_COMMAND = "hermeslink";
4189
4507
  var LINK_DEFAULT_PORT = 52379;
4190
4508
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -12677,10 +12995,10 @@ function parseHermesApiCapabilities(payload) {
12677
12995
  sessionKeyHeader: readString10(features, "session_key_header")
12678
12996
  };
12679
12997
  }
12680
- async function callHermesApi(path27, init, options) {
12998
+ async function callHermesApi(path26, init, options) {
12681
12999
  const method = init.method ?? "GET";
12682
13000
  const startedAt = Date.now();
12683
- void options.logger?.debug("hermes_api_request_started", { method, path: path27 });
13001
+ void options.logger?.debug("hermes_api_request_started", { method, path: path26 });
12684
13002
  const availability = await ensureHermesApiServerAvailable({
12685
13003
  fetchImpl: options.fetchImpl,
12686
13004
  logger: options.logger,
@@ -12688,7 +13006,7 @@ async function callHermesApi(path27, init, options) {
12688
13006
  });
12689
13007
  let config = availability.configResult.apiServer;
12690
13008
  const fetcher = options.fetchImpl ?? fetch;
12691
- const request = () => fetchHermesApi(fetcher, config, path27, init, options);
13009
+ const request = () => fetchHermesApi(fetcher, config, path26, init, options);
12692
13010
  let response;
12693
13011
  try {
12694
13012
  response = await request();
@@ -12696,7 +13014,7 @@ async function callHermesApi(path27, init, options) {
12696
13014
  logHermesApiError(
12697
13015
  options.logger,
12698
13016
  method,
12699
- path27,
13017
+ path26,
12700
13018
  options.profileName,
12701
13019
  startedAt,
12702
13020
  error
@@ -12707,7 +13025,7 @@ async function callHermesApi(path27, init, options) {
12707
13025
  logHermesApiResponse(
12708
13026
  options.logger,
12709
13027
  method,
12710
- path27,
13028
+ path26,
12711
13029
  options.profileName,
12712
13030
  startedAt,
12713
13031
  response
@@ -12716,7 +13034,7 @@ async function callHermesApi(path27, init, options) {
12716
13034
  }
12717
13035
  void options.logger?.warn("hermes_api_request_retrying_after_401", {
12718
13036
  method,
12719
- path: path27,
13037
+ path: path26,
12720
13038
  profile: options.profileName ?? "default",
12721
13039
  port: config.port ?? null,
12722
13040
  duration_ms: Date.now() - startedAt
@@ -12734,7 +13052,7 @@ async function callHermesApi(path27, init, options) {
12734
13052
  logHermesApiError(
12735
13053
  options.logger,
12736
13054
  method,
12737
- path27,
13055
+ path26,
12738
13056
  options.profileName,
12739
13057
  startedAt,
12740
13058
  error
@@ -12744,7 +13062,7 @@ async function callHermesApi(path27, init, options) {
12744
13062
  logHermesApiResponse(
12745
13063
  options.logger,
12746
13064
  method,
12747
- path27,
13065
+ path26,
12748
13066
  options.profileName,
12749
13067
  startedAt,
12750
13068
  response
@@ -12754,7 +13072,7 @@ async function callHermesApi(path27, init, options) {
12754
13072
  }
12755
13073
  void options.logger?.warn("hermes_api_request_repairing_after_401", {
12756
13074
  method,
12757
- path: path27,
13075
+ path: path26,
12758
13076
  profile: options.profileName ?? "default",
12759
13077
  port: config.port ?? null,
12760
13078
  duration_ms: Date.now() - startedAt
@@ -12774,7 +13092,7 @@ async function callHermesApi(path27, init, options) {
12774
13092
  logHermesApiError(
12775
13093
  options.logger,
12776
13094
  method,
12777
- path27,
13095
+ path26,
12778
13096
  options.profileName,
12779
13097
  startedAt,
12780
13098
  error
@@ -12784,21 +13102,21 @@ async function callHermesApi(path27, init, options) {
12784
13102
  logHermesApiResponse(
12785
13103
  options.logger,
12786
13104
  method,
12787
- path27,
13105
+ path26,
12788
13106
  options.profileName,
12789
13107
  startedAt,
12790
13108
  response
12791
13109
  );
12792
13110
  return response;
12793
13111
  }
12794
- async function fetchHermesApi(fetcher, config, path27, init, options) {
13112
+ async function fetchHermesApi(fetcher, config, path26, init, options) {
12795
13113
  const headers = new Headers(init.headers);
12796
13114
  headers.set("accept", headers.get("accept") ?? "application/json");
12797
13115
  if (config.key) {
12798
13116
  headers.set("x-api-key", config.key);
12799
13117
  headers.set("authorization", `Bearer ${config.key}`);
12800
13118
  }
12801
- return await fetcher(`http://127.0.0.1:${config.port}${path27}`, {
13119
+ return await fetcher(`http://127.0.0.1:${config.port}${path26}`, {
12802
13120
  ...init,
12803
13121
  headers
12804
13122
  }).catch((error) => {
@@ -12807,10 +13125,10 @@ async function fetchHermesApi(fetcher, config, path27, init, options) {
12807
13125
  }
12808
13126
  void options.logger?.warn("hermes_api_server_connect_failed", {
12809
13127
  method: String(init.method ?? "GET").toUpperCase(),
12810
- path: path27,
13128
+ path: path26,
12811
13129
  profile: options.profileName ?? "default",
12812
13130
  port: config.port ?? null,
12813
- url: `http://127.0.0.1:${config.port}${path27}`,
13131
+ url: `http://127.0.0.1:${config.port}${path26}`,
12814
13132
  error: error instanceof Error ? error.message : String(error)
12815
13133
  });
12816
13134
  throw new LinkHttpError(
@@ -12820,10 +13138,10 @@ async function fetchHermesApi(fetcher, config, path27, init, options) {
12820
13138
  );
12821
13139
  });
12822
13140
  }
12823
- function logHermesApiResponse(logger, method, path27, profileName, startedAt, response) {
13141
+ function logHermesApiResponse(logger, method, path26, profileName, startedAt, response) {
12824
13142
  const fields = {
12825
13143
  method,
12826
- path: path27,
13144
+ path: path26,
12827
13145
  profile: profileName ?? "default",
12828
13146
  status: response.status,
12829
13147
  duration_ms: Date.now() - startedAt
@@ -12844,10 +13162,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
12844
13162
  ...upstreamError ? { upstream_error: upstreamError } : {}
12845
13163
  });
12846
13164
  }
12847
- function logHermesApiError(logger, method, path27, profileName, startedAt, error) {
13165
+ function logHermesApiError(logger, method, path26, profileName, startedAt, error) {
12848
13166
  void logger?.warn("hermes_api_request_failed", {
12849
13167
  method,
12850
- path: path27,
13168
+ path: path26,
12851
13169
  profile: profileName ?? "default",
12852
13170
  duration_ms: Date.now() - startedAt,
12853
13171
  ...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
@@ -18228,6 +18546,22 @@ function registerModelConfigRoutes(router, options) {
18228
18546
  ctx.set("cache-control", "no-store");
18229
18547
  ctx.body = await listHermesModelConfigs();
18230
18548
  });
18549
+ router.get("/api/v1/model-configs/catalog", async (ctx) => {
18550
+ await authenticateRequest(ctx, paths);
18551
+ const targetProfileName = readQueryString(ctx.query.target_profile);
18552
+ if (targetProfileName) {
18553
+ await getHermesProfileStatus(targetProfileName, paths);
18554
+ }
18555
+ ctx.set("cache-control", "no-store");
18556
+ const profiles = await listHermesProfiles(paths);
18557
+ ctx.body = await listHermesModelConfigCatalog({
18558
+ targetProfileName,
18559
+ profiles: profiles.map((profile) => ({
18560
+ name: profile.name,
18561
+ displayName: profile.displayName
18562
+ }))
18563
+ });
18564
+ });
18231
18565
  router.post("/api/v1/model-configs", async (ctx) => {
18232
18566
  await authenticateRequest(ctx, paths);
18233
18567
  const body = await readJsonBody(ctx.req);
@@ -18291,6 +18625,23 @@ function registerModelConfigRoutes(router, options) {
18291
18625
  throw toModelConfigHttpError(error);
18292
18626
  }
18293
18627
  });
18628
+ router.post("/api/v1/profiles/:name/model-configs/import", async (ctx) => {
18629
+ await authenticateRequest(ctx, paths);
18630
+ await getHermesProfileStatus(ctx.params.name, paths);
18631
+ const body = await readJsonBody(ctx.req);
18632
+ const input = readModelConfigImportInput(body);
18633
+ await getHermesProfileStatus(input.sourceProfileName, paths);
18634
+ try {
18635
+ const result = await importHermesModelConfig(input, ctx.params.name);
18636
+ ctx.body = shouldReloadGatewayAfterModelConfigChange(body) ? await reloadGatewayAfterProfileModelConfigChange(result, {
18637
+ paths,
18638
+ logger,
18639
+ profileName: ctx.params.name
18640
+ }) : markModelConfigAppliedWithoutGatewayReload(result);
18641
+ } catch (error) {
18642
+ throw toModelConfigHttpError(error);
18643
+ }
18644
+ });
18294
18645
  router.patch("/api/v1/profiles/:name/model-configs/defaults", async (ctx) => {
18295
18646
  await authenticateRequest(ctx, paths);
18296
18647
  await getHermesProfileStatus(ctx.params.name, paths);
@@ -18357,6 +18708,25 @@ function readModelDefaultsInput(body) {
18357
18708
  compressionModelId: readString14(body, "compression_model_id") ?? readString14(body, "compressionModelId") ?? void 0
18358
18709
  };
18359
18710
  }
18711
+ function readModelConfigImportInput(body) {
18712
+ const sourceProfileName = readString14(body, "source_profile") ?? readString14(body, "sourceProfile") ?? readString14(body, "source_profile_name") ?? readString14(body, "sourceProfileName");
18713
+ const modelId = readString14(body, "model_id") ?? readString14(body, "modelId") ?? readString14(body, "id");
18714
+ if (!sourceProfileName || !modelId) {
18715
+ throw new LinkHttpError(
18716
+ 400,
18717
+ "model_import_invalid",
18718
+ "source_profile and model_id are required"
18719
+ );
18720
+ }
18721
+ return {
18722
+ sourceProfileName,
18723
+ modelId,
18724
+ provider: readString14(body, "provider") ?? readString14(body, "provider_key") ?? readString14(body, "providerKey") ?? void 0,
18725
+ baseUrl: readString14(body, "base_url") ?? readString14(body, "baseUrl") ?? void 0,
18726
+ apiMode: readString14(body, "api_mode") ?? readString14(body, "apiMode") ?? void 0,
18727
+ setDefault: readBoolean3(body.set_default ?? body.setDefault)
18728
+ };
18729
+ }
18360
18730
  function shouldReloadGatewayAfterModelConfigChange(body) {
18361
18731
  const explicit = readBoolean3(body.reload_gateway ?? body.reloadGateway) ?? (readBoolean3(body.skip_gateway_reload ?? body.skipGatewayReload) === true ? false : void 0);
18362
18732
  return explicit ?? true;
@@ -19312,6 +19682,7 @@ var CUSTOM_PROVIDER_CARD_ID = "__custom__";
19312
19682
  var CUSTOM_PROVIDER_REGISTRY_FILE = "memory-providers.json";
19313
19683
  var HINDSIGHT_DEFAULT_API_URL = "https://api.hindsight.vectorize.io";
19314
19684
  var HINDSIGHT_DEFAULT_LOCAL_URL = "http://localhost:8888";
19685
+ var MEMORY_PROVIDER_TEST_TIMEOUT_MS = 4e3;
19315
19686
  var OPENVIKING_DEFAULT_ENDPOINT = "http://127.0.0.1:1933";
19316
19687
  var RETAINDB_DEFAULT_BASE_URL = "https://api.retaindb.com";
19317
19688
  var HINDSIGHT_LLM_PROVIDERS = [
@@ -19335,25 +19706,25 @@ var MEMORY_PROVIDER_CATALOG = [
19335
19706
  {
19336
19707
  id: "honcho",
19337
19708
  label: "Honcho",
19338
- description: "AI-native cross-session user modeling provider\uFF1B\u8FD9\u91CC\u53EF\u7F16\u8F91 workspace\u3001peer \u4E0E\u53EC\u56DE\u7B56\u7565\u3002",
19709
+ description: "AI-native cross-session user modeling provider\uFF1B\u8FD9\u91CC\u53EF\u586B\u5199 API Key\uFF0C\u5E76\u7F16\u8F91 workspace\u3001peer \u4E0E\u53EC\u56DE\u7B56\u7565\u3002",
19339
19710
  configurable: true
19340
19711
  },
19341
19712
  {
19342
19713
  id: "openviking",
19343
19714
  label: "OpenViking",
19344
- description: "\u4E0A\u4E0B\u6587\u6570\u636E\u5E93\u4E0E\u5C42\u7EA7\u77E5\u8BC6\u5E93 provider\uFF1B\u8FD9\u91CC\u53EF\u7F16\u8F91\u975E\u654F\u611F endpoint\u3001account \u4E0E agent \u6807\u8BC6\u3002",
19715
+ description: "\u4E0A\u4E0B\u6587\u6570\u636E\u5E93\u4E0E\u5C42\u7EA7\u77E5\u8BC6\u5E93 provider\uFF1B\u8FD9\u91CC\u53EF\u7F16\u8F91 endpoint\u3001account\u3001agent \u6807\u8BC6\u4E0E\u53EF\u9009 API Key\u3002",
19345
19716
  configurable: true
19346
19717
  },
19347
19718
  {
19348
19719
  id: "mem0",
19349
19720
  label: "Mem0",
19350
- description: "\u8BED\u4E49\u641C\u7D22\u4E0E\u4E8B\u5B9E\u62BD\u53D6 provider\uFF1B\u8FD9\u91CC\u53EF\u7F16\u8F91 user\u3001agent \u4E0E rerank \u7B56\u7565\u3002",
19721
+ description: "\u8BED\u4E49\u641C\u7D22\u4E0E\u4E8B\u5B9E\u62BD\u53D6 provider\uFF1B\u8FD9\u91CC\u53EF\u586B\u5199 API Key\uFF0C\u5E76\u7F16\u8F91 user\u3001agent \u4E0E rerank \u7B56\u7565\u3002",
19351
19722
  configurable: true
19352
19723
  },
19353
19724
  {
19354
19725
  id: "hindsight",
19355
19726
  label: "Hindsight",
19356
- description: "\u77E5\u8BC6\u56FE\u8C31\u4E0E\u591A\u7B56\u7565\u68C0\u7D22 provider\uFF1B\u8FD9\u91CC\u53EF\u7F16\u8F91\u975E\u654F\u611F\u8FDE\u63A5\u3001bank \u4E0E\u53EC\u56DE\u7B56\u7565\u3002",
19727
+ description: "\u77E5\u8BC6\u56FE\u8C31\u4E0E\u591A\u7B56\u7565\u68C0\u7D22 provider\uFF1B\u8FD9\u91CC\u53EF\u6309\u8FDE\u63A5\u6A21\u5F0F\u586B\u5199 API Key\u3001LLM Key\u3001URL\u3001bank \u4E0E\u53EC\u56DE\u7B56\u7565\u3002",
19357
19728
  configurable: true
19358
19729
  },
19359
19730
  {
@@ -19365,19 +19736,19 @@ var MEMORY_PROVIDER_CATALOG = [
19365
19736
  {
19366
19737
  id: "retaindb",
19367
19738
  label: "RetainDB",
19368
- description: "Cloud memory API provider\uFF1B\u8FD9\u91CC\u53EF\u7F16\u8F91\u975E\u654F\u611F endpoint \u4E0E project\u3002",
19739
+ description: "Cloud memory API provider\uFF1B\u8FD9\u91CC\u53EF\u586B\u5199 API Key\uFF0C\u5E76\u7F16\u8F91 endpoint \u4E0E project\u3002",
19369
19740
  configurable: true
19370
19741
  },
19371
19742
  {
19372
19743
  id: "byterover",
19373
19744
  label: "ByteRover",
19374
- description: "\u57FA\u4E8E brv CLI \u7684\u672C\u5730\u4F18\u5148\u77E5\u8BC6\u6811 provider\uFF1B\u4E91\u540C\u6B65 API key \u9700\u5728\u672C\u673A\u914D\u7F6E\u3002",
19375
- configurable: false
19745
+ description: "\u57FA\u4E8E brv CLI \u7684\u672C\u5730\u4F18\u5148\u77E5\u8BC6\u6811 provider\uFF1B\u8FD9\u91CC\u53EF\u586B\u5199\u53EF\u9009\u4E91\u540C\u6B65 API Key\uFF0C\u4ECD\u9700\u672C\u673A\u5B89\u88C5 brv CLI\u3002",
19746
+ configurable: true
19376
19747
  },
19377
19748
  {
19378
19749
  id: "supermemory",
19379
19750
  label: "Supermemory",
19380
- description: "\u8BED\u4E49\u957F\u671F\u8BB0\u5FC6 provider\uFF1B\u8FD9\u91CC\u53EF\u7F16\u8F91\u5BB9\u5668\u3001\u81EA\u52A8\u6355\u83B7\u4E0E\u53EC\u56DE\u7B56\u7565\u3002",
19751
+ description: "\u8BED\u4E49\u957F\u671F\u8BB0\u5FC6 provider\uFF1B\u8FD9\u91CC\u53EF\u586B\u5199 API Key\uFF0C\u5E76\u7F16\u8F91\u5BB9\u5668\u3001\u81EA\u52A8\u6355\u83B7\u4E0E\u53EC\u56DE\u7B56\u7565\u3002",
19381
19752
  configurable: true
19382
19753
  }
19383
19754
  ];
@@ -19464,14 +19835,107 @@ async function saveHermesMemoryProviderSettings(profileName, provider, patch) {
19464
19835
  );
19465
19836
  return readHermesProfileMemory(profileName);
19466
19837
  }
19838
+ async function testHermesMemoryProviderSettings(profileName, provider, patch) {
19839
+ const providerId = normalizeConfigurableProvider(provider, patch);
19840
+ if (providerId === "hindsight") {
19841
+ return testHindsightProviderSettings(profileName, patch);
19842
+ }
19843
+ return {
19844
+ ok: false,
19845
+ provider: providerId,
19846
+ message: "\u8FD9\u4E2A memory provider \u6682\u65F6\u8FD8\u6CA1\u6709 App \u5185\u8FDE\u63A5\u6D4B\u8BD5\u3002",
19847
+ checks: []
19848
+ };
19849
+ }
19467
19850
  async function setHermesMemoryProvider(profileName, provider) {
19468
19851
  const providerId = normalizeSelectableProvider(provider);
19469
19852
  await assertProviderCanBeActivated(profileName, providerId);
19470
19853
  await patchHermesMemoryProvider(profileName, providerId);
19471
19854
  return readHermesProfileMemory(profileName);
19472
19855
  }
19856
+ async function testHindsightProviderSettings(profileName, patch) {
19857
+ const config = await readJsonObject(
19858
+ memoryProviderConfigPath(profileName, "hindsight") ?? ""
19859
+ );
19860
+ const env = await readHermesMemoryEnv(profileName);
19861
+ const mode = normalizeHindsightMode(
19862
+ patch.mode ?? config.mode ?? env.HINDSIGHT_MODE
19863
+ );
19864
+ const apiUrl = readString15(patch.apiUrl) ?? readString15(config.api_url) ?? env.HINDSIGHT_API_URL ?? (mode === "cloud" ? HINDSIGHT_DEFAULT_API_URL : HINDSIGHT_DEFAULT_LOCAL_URL);
19865
+ const bankId = readString15(patch.bankId) ?? readString15(config.bank_id) ?? "hermes";
19866
+ const apiKey = readString15(patch.apiKey) ?? env.HINDSIGHT_API_KEY ?? readString15(config.apiKey) ?? readString15(config.api_key);
19867
+ const baseUrl = normalizeHttpUrl(apiUrl);
19868
+ if (!baseUrl) {
19869
+ return {
19870
+ ok: false,
19871
+ provider: "hindsight",
19872
+ message: "Hindsight API URL \u4E0D\u662F\u6709\u6548\u7684 http/https \u5730\u5740\u3002",
19873
+ checks: [
19874
+ {
19875
+ id: "api_url",
19876
+ label: "API URL",
19877
+ ok: false,
19878
+ detail: "\u8BF7\u8F93\u5165\u6709\u6548\u7684 Hindsight API URL\u3002"
19879
+ }
19880
+ ]
19881
+ };
19882
+ }
19883
+ if (mode === "cloud" && !isConfiguredEnvValue(apiKey)) {
19884
+ return {
19885
+ ok: false,
19886
+ provider: "hindsight",
19887
+ message: "Hindsight Cloud \u9700\u8981\u5148\u586B\u5199 API key \u624D\u80FD\u6D4B\u8BD5\u3002",
19888
+ checks: [
19889
+ {
19890
+ id: "api_key",
19891
+ label: "API key",
19892
+ ok: false,
19893
+ detail: "Cloud \u6A21\u5F0F\u7F3A\u5C11 Hindsight API key\u3002"
19894
+ }
19895
+ ]
19896
+ };
19897
+ }
19898
+ const headers = hindsightRequestHeaders(apiKey);
19899
+ const health = await probeHindsightJson(baseUrl, "/health", headers);
19900
+ const version = await probeHindsightJson(baseUrl, "/version", headers);
19901
+ const bankConfig = await probeHindsightJson(
19902
+ baseUrl,
19903
+ `/v1/default/banks/${encodeURIComponent(bankId)}/config`,
19904
+ headers
19905
+ );
19906
+ const checks = [
19907
+ {
19908
+ id: "health",
19909
+ label: "Hindsight API",
19910
+ ok: health.ok,
19911
+ detail: health.detail
19912
+ },
19913
+ {
19914
+ id: "version",
19915
+ label: "Hindsight version",
19916
+ ok: version.ok,
19917
+ detail: version.detail
19918
+ },
19919
+ {
19920
+ id: "bank",
19921
+ label: `Memory bank "${bankId}"`,
19922
+ ok: bankConfig.ok,
19923
+ detail: bankConfig.detail
19924
+ }
19925
+ ];
19926
+ const ok = checks.every((check) => check.ok);
19927
+ return {
19928
+ ok,
19929
+ provider: "hindsight",
19930
+ message: ok ? "Hindsight \u8FDE\u63A5\u6D4B\u8BD5\u901A\u8FC7\u3002" : "Hindsight \u8FDE\u63A5\u6D4B\u8BD5\u672A\u901A\u8FC7\uFF0C\u8BF7\u68C0\u67E5 API URL\u3001API key \u6216\u670D\u52A1\u72B6\u6001\u3002",
19931
+ checks
19932
+ };
19933
+ }
19473
19934
  async function saveProviderSettings(profileName, provider, patch) {
19474
19935
  if (provider === "honcho") {
19936
+ await patchHermesMemoryEnv(profileName, {
19937
+ HONCHO_API_KEY: patch.apiKey
19938
+ });
19475
19939
  await patchJsonProviderConfig(profileName, "honcho.json", {
19476
19940
  baseUrl: patch.baseUrl,
19477
19941
  workspace: patch.workspace,
@@ -19489,6 +19953,9 @@ async function saveProviderSettings(profileName, provider, patch) {
19489
19953
  return;
19490
19954
  }
19491
19955
  if (provider === "mem0") {
19956
+ await patchHermesMemoryEnv(profileName, {
19957
+ MEM0_API_KEY: patch.apiKey
19958
+ });
19492
19959
  await patchJsonProviderConfig(profileName, "mem0.json", {
19493
19960
  user_id: patch.userId,
19494
19961
  agent_id: patch.agentId,
@@ -19499,6 +19966,7 @@ async function saveProviderSettings(profileName, provider, patch) {
19499
19966
  if (provider === "openviking") {
19500
19967
  await patchHermesMemoryEnv(profileName, {
19501
19968
  OPENVIKING_ENDPOINT: patch.endpoint,
19969
+ OPENVIKING_API_KEY: patch.apiKey,
19502
19970
  OPENVIKING_ACCOUNT: patch.account,
19503
19971
  OPENVIKING_USER: patch.user,
19504
19972
  OPENVIKING_AGENT: patch.agent
@@ -19506,6 +19974,9 @@ async function saveProviderSettings(profileName, provider, patch) {
19506
19974
  return;
19507
19975
  }
19508
19976
  if (provider === "supermemory") {
19977
+ await patchHermesMemoryEnv(profileName, {
19978
+ SUPERMEMORY_API_KEY: patch.apiKey
19979
+ });
19509
19980
  await patchJsonProviderConfig(profileName, "supermemory.json", {
19510
19981
  container_tag: patch.containerTag,
19511
19982
  auto_recall: patch.autoRecall,
@@ -19518,6 +19989,10 @@ async function saveProviderSettings(profileName, provider, patch) {
19518
19989
  return;
19519
19990
  }
19520
19991
  if (provider === "hindsight") {
19992
+ await patchHermesMemoryEnv(profileName, {
19993
+ HINDSIGHT_API_KEY: patch.apiKey,
19994
+ HINDSIGHT_LLM_API_KEY: patch.llmApiKey
19995
+ });
19521
19996
  await patchJsonProviderConfig(
19522
19997
  profileName,
19523
19998
  path21.join("hindsight", "config.json"),
@@ -19547,12 +20022,16 @@ async function saveProviderSettings(profileName, provider, patch) {
19547
20022
  }
19548
20023
  if (provider === "retaindb") {
19549
20024
  await patchHermesMemoryEnv(profileName, {
20025
+ RETAINDB_API_KEY: patch.apiKey,
19550
20026
  RETAINDB_BASE_URL: patch.baseUrl,
19551
20027
  RETAINDB_PROJECT: patch.project
19552
20028
  });
19553
20029
  return;
19554
20030
  }
19555
20031
  if (provider === "byterover") {
20032
+ await patchHermesMemoryEnv(profileName, {
20033
+ BRV_API_KEY: patch.apiKey
20034
+ });
19556
20035
  return;
19557
20036
  }
19558
20037
  await patchCustomProviderConfig(profileName, provider, patch);
@@ -19888,7 +20367,7 @@ async function readProviderConfigurationStatus(profileName, provider) {
19888
20367
  );
19889
20368
  return isConfiguredEnvValue(env.HONCHO_API_KEY) || isConfiguredEnvValue(readString15(config2.apiKey)) || isConfiguredEnvValue(readString15(config2.api_key)) || isConfiguredEnvValue(readString15(config2.baseUrl)) ? { configured: true, issue: null } : {
19890
20369
  configured: false,
19891
- issue: "Honcho \u9700\u8981\u5148\u914D\u7F6E HONCHO_API_KEY\uFF0C\u6216\u5728 honcho.json \u914D\u7F6E self-hosted baseUrl\u3002"
20370
+ issue: "Honcho \u9700\u8981\u5148\u586B\u5199 API Key\uFF0C\u6216\u5728 honcho.json \u914D\u7F6E self-hosted baseUrl\u3002"
19892
20371
  };
19893
20372
  }
19894
20373
  if (provider === "mem0") {
@@ -19897,7 +20376,7 @@ async function readProviderConfigurationStatus(profileName, provider) {
19897
20376
  );
19898
20377
  return isConfiguredEnvValue(env.MEM0_API_KEY) || isConfiguredEnvValue(readString15(config2.api_key)) ? { configured: true, issue: null } : {
19899
20378
  configured: false,
19900
- issue: "Mem0 \u9700\u8981\u5148\u5728\u672C\u673A Hermes .env \u914D\u7F6E MEM0_API_KEY\u3002"
20379
+ issue: "Mem0 \u9700\u8981\u5148\u5728\u672C\u9875\u586B\u5199 API Key\uFF0CLink \u4F1A\u5199\u5165\u5F53\u524D Profile \u7684 .env\u3002"
19901
20380
  };
19902
20381
  }
19903
20382
  if (provider === "openviking") {
@@ -19909,7 +20388,7 @@ async function readProviderConfigurationStatus(profileName, provider) {
19909
20388
  if (provider === "supermemory") {
19910
20389
  return isConfiguredEnvValue(env.SUPERMEMORY_API_KEY) ? { configured: true, issue: null } : {
19911
20390
  configured: false,
19912
- issue: "Supermemory \u9700\u8981\u5148\u5728\u672C\u673A Hermes .env \u914D\u7F6E SUPERMEMORY_API_KEY\u3002"
20391
+ issue: "Supermemory \u9700\u8981\u5148\u5728\u672C\u9875\u586B\u5199 API Key\uFF0CLink \u4F1A\u5199\u5165\u5F53\u524D Profile \u7684 .env\u3002"
19913
20392
  };
19914
20393
  }
19915
20394
  if (provider === "holographic") {
@@ -19918,7 +20397,7 @@ async function readProviderConfigurationStatus(profileName, provider) {
19918
20397
  if (provider === "retaindb") {
19919
20398
  return isConfiguredEnvValue(env.RETAINDB_API_KEY) ? { configured: true, issue: null } : {
19920
20399
  configured: false,
19921
- issue: "RetainDB \u9700\u8981\u5148\u5728\u672C\u673A Hermes .env \u914D\u7F6E RETAINDB_API_KEY\u3002"
20400
+ issue: "RetainDB \u9700\u8981\u5148\u5728\u672C\u9875\u586B\u5199 API Key\uFF0CLink \u4F1A\u5199\u5165\u5F53\u524D Profile \u7684 .env\u3002"
19922
20401
  };
19923
20402
  }
19924
20403
  if (provider === "byterover") {
@@ -19941,7 +20420,7 @@ async function readProviderConfigurationStatus(profileName, provider) {
19941
20420
  if (mode === "cloud") {
19942
20421
  return isConfiguredEnvValue(apiKey) ? { configured: true, issue: null } : {
19943
20422
  configured: false,
19944
- issue: "Hindsight Cloud \u9700\u8981\u5148\u5728\u672C\u673A Hermes .env \u914D\u7F6E HINDSIGHT_API_KEY\u3002"
20423
+ issue: "Hindsight Cloud \u9700\u8981\u5148\u5728\u672C\u9875\u586B\u5199 API Key\uFF0CLink \u4F1A\u5199\u5165\u5F53\u524D Profile \u7684 .env\u3002"
19945
20424
  };
19946
20425
  }
19947
20426
  if (mode === "local_external") {
@@ -19957,7 +20436,7 @@ async function readProviderConfigurationStatus(profileName, provider) {
19957
20436
  if (!llmModel) {
19958
20437
  return {
19959
20438
  configured: false,
19960
- issue: "Hindsight local_embedded \u9700\u8981\u5148\u914D\u7F6E LLM \u6A21\u578B\u3002"
20439
+ issue: "Hindsight local_embedded \u9700\u8981\u5148\u586B\u5199 LLM \u6A21\u578B\u3002"
19961
20440
  };
19962
20441
  }
19963
20442
  if (llmProvider === "openai_compatible" && !isConfiguredEnvValue(
@@ -19965,7 +20444,7 @@ async function readProviderConfigurationStatus(profileName, provider) {
19965
20444
  )) {
19966
20445
  return {
19967
20446
  configured: false,
19968
- issue: "Hindsight openai_compatible \u9700\u8981\u5148\u914D\u7F6E LLM Base URL\u3002"
20447
+ issue: "Hindsight openai_compatible \u9700\u8981\u5148\u586B\u5199 LLM Base URL\u3002"
19969
20448
  };
19970
20449
  }
19971
20450
  if (!["ollama", "lmstudio", "openai_compatible"].includes(llmProvider) && !isConfiguredEnvValue(
@@ -19973,7 +20452,7 @@ async function readProviderConfigurationStatus(profileName, provider) {
19973
20452
  )) {
19974
20453
  return {
19975
20454
  configured: false,
19976
- issue: "Hindsight local_embedded \u9700\u8981\u5148\u5728\u672C\u673A Hermes .env \u914D\u7F6E HINDSIGHT_LLM_API_KEY\u3002"
20455
+ issue: "Hindsight local_embedded \u9700\u8981\u5148\u586B\u5199 LLM API Key\uFF0CLink \u4F1A\u5199\u5165\u5F53\u524D Profile \u7684 .env\u3002"
19977
20456
  };
19978
20457
  }
19979
20458
  return { configured: true, issue: null };
@@ -20019,8 +20498,15 @@ async function readProviderSettings(profileName, provider) {
20019
20498
  const config = await readJsonObject(
20020
20499
  memoryProviderConfigPath(profileName, provider) ?? ""
20021
20500
  );
20501
+ const env = await readHermesMemoryEnv(profileName);
20022
20502
  return [
20023
20503
  stringSetting("baseUrl", "Base URL", config.baseUrl ?? ""),
20504
+ secretSetting(
20505
+ "apiKey",
20506
+ "API Key",
20507
+ env.HONCHO_API_KEY ?? readString15(config.apiKey) ?? readString15(config.api_key),
20508
+ isConfiguredEnvValue(env.HONCHO_API_KEY) || isConfiguredEnvValue(readString15(config.apiKey)) || isConfiguredEnvValue(readString15(config.api_key))
20509
+ ),
20024
20510
  stringSetting("workspace", "Workspace", config.workspace ?? "hermes"),
20025
20511
  stringSetting("peerName", "\u7528\u6237 Peer", config.peerName ?? ""),
20026
20512
  stringSetting("aiPeer", "AI Peer", config.aiPeer ?? "hermes"),
@@ -20056,7 +20542,14 @@ async function readProviderSettings(profileName, provider) {
20056
20542
  const config = await readJsonObject(
20057
20543
  memoryProviderConfigPath(profileName, provider) ?? ""
20058
20544
  );
20545
+ const env = await readHermesMemoryEnv(profileName);
20059
20546
  return [
20547
+ secretSetting(
20548
+ "apiKey",
20549
+ "API Key",
20550
+ env.MEM0_API_KEY ?? readString15(config.apiKey) ?? readString15(config.api_key),
20551
+ isConfiguredEnvValue(env.MEM0_API_KEY) || isConfiguredEnvValue(readString15(config.apiKey)) || isConfiguredEnvValue(readString15(config.api_key))
20552
+ ),
20060
20553
  stringSetting("userId", "User ID", config.user_id ?? "hermes-user"),
20061
20554
  stringSetting("agentId", "Agent ID", config.agent_id ?? "hermes"),
20062
20555
  booleanSetting("rerank", "\u542F\u7528 rerank", config.rerank ?? true)
@@ -20070,6 +20563,12 @@ async function readProviderSettings(profileName, provider) {
20070
20563
  "Endpoint",
20071
20564
  env.OPENVIKING_ENDPOINT ?? OPENVIKING_DEFAULT_ENDPOINT
20072
20565
  ),
20566
+ secretSetting(
20567
+ "apiKey",
20568
+ "API Key (optional)",
20569
+ env.OPENVIKING_API_KEY,
20570
+ isConfiguredEnvValue(env.OPENVIKING_API_KEY)
20571
+ ),
20073
20572
  stringSetting("account", "Account", env.OPENVIKING_ACCOUNT ?? "default"),
20074
20573
  stringSetting("user", "User", env.OPENVIKING_USER ?? "default"),
20075
20574
  stringSetting("agent", "Agent", env.OPENVIKING_AGENT ?? "hermes")
@@ -20079,7 +20578,14 @@ async function readProviderSettings(profileName, provider) {
20079
20578
  const config = await readJsonObject(
20080
20579
  memoryProviderConfigPath(profileName, provider) ?? ""
20081
20580
  );
20581
+ const env = await readHermesMemoryEnv(profileName);
20082
20582
  return [
20583
+ secretSetting(
20584
+ "apiKey",
20585
+ "API Key",
20586
+ env.SUPERMEMORY_API_KEY,
20587
+ isConfiguredEnvValue(env.SUPERMEMORY_API_KEY)
20588
+ ),
20083
20589
  stringSetting("containerTag", "\u5BB9\u5668\u6807\u7B7E", config.container_tag ?? "hermes"),
20084
20590
  booleanSetting("autoRecall", "\u81EA\u52A8\u56DE\u5FC6", config.auto_recall ?? true),
20085
20591
  booleanSetting("autoCapture", "\u81EA\u52A8\u6355\u83B7", config.auto_capture ?? true),
@@ -20109,6 +20615,7 @@ async function readProviderSettings(profileName, provider) {
20109
20615
  const config = await readJsonObject(
20110
20616
  memoryProviderConfigPath(profileName, provider) ?? ""
20111
20617
  );
20618
+ const env = await readHermesMemoryEnv(profileName);
20112
20619
  const banks = toRecord14(config.banks);
20113
20620
  const hermesBank = toRecord14(banks.hermes);
20114
20621
  const mode = normalizeHindsightMode(config.mode);
@@ -20123,6 +20630,12 @@ async function readProviderSettings(profileName, provider) {
20123
20630
  "API URL",
20124
20631
  config.api_url ?? (mode === "cloud" ? HINDSIGHT_DEFAULT_API_URL : HINDSIGHT_DEFAULT_LOCAL_URL)
20125
20632
  ),
20633
+ secretSetting(
20634
+ "apiKey",
20635
+ "Hindsight API Key",
20636
+ env.HINDSIGHT_API_KEY ?? readString15(config.apiKey) ?? readString15(config.api_key),
20637
+ isConfiguredEnvValue(env.HINDSIGHT_API_KEY) || isConfiguredEnvValue(readString15(config.apiKey)) || isConfiguredEnvValue(readString15(config.api_key))
20638
+ ),
20126
20639
  stringSetting(
20127
20640
  "bankId",
20128
20641
  "Memory Bank",
@@ -20140,6 +20653,12 @@ async function readProviderSettings(profileName, provider) {
20140
20653
  "\u672C\u5730 LLM Base URL",
20141
20654
  config.llm_base_url ?? ""
20142
20655
  ),
20656
+ secretSetting(
20657
+ "llmApiKey",
20658
+ "LLM API Key",
20659
+ env.HINDSIGHT_LLM_API_KEY ?? readString15(config.llmApiKey) ?? readString15(config.llm_api_key),
20660
+ isConfiguredEnvValue(env.HINDSIGHT_LLM_API_KEY) || isConfiguredEnvValue(readString15(config.llmApiKey)) || isConfiguredEnvValue(readString15(config.llm_api_key))
20661
+ ),
20143
20662
  booleanSetting("autoRecall", "\u81EA\u52A8\u56DE\u5FC6", config.auto_recall ?? true),
20144
20663
  booleanSetting("autoRetain", "\u81EA\u52A8\u6C89\u6DC0", config.auto_retain ?? true),
20145
20664
  selectSetting(
@@ -20172,6 +20691,12 @@ async function readProviderSettings(profileName, provider) {
20172
20691
  if (provider === "retaindb") {
20173
20692
  const env = await readHermesMemoryEnv(profileName);
20174
20693
  return [
20694
+ secretSetting(
20695
+ "apiKey",
20696
+ "API Key",
20697
+ env.RETAINDB_API_KEY,
20698
+ isConfiguredEnvValue(env.RETAINDB_API_KEY)
20699
+ ),
20175
20700
  stringSetting(
20176
20701
  "baseUrl",
20177
20702
  "Base URL",
@@ -20181,7 +20706,14 @@ async function readProviderSettings(profileName, provider) {
20181
20706
  ];
20182
20707
  }
20183
20708
  if (provider === "byterover") {
20709
+ const env = await readHermesMemoryEnv(profileName);
20184
20710
  return [
20711
+ secretSetting(
20712
+ "apiKey",
20713
+ "Cloud Sync API Key (optional)",
20714
+ env.BRV_API_KEY,
20715
+ isConfiguredEnvValue(env.BRV_API_KEY)
20716
+ ),
20185
20717
  stringSetting(
20186
20718
  "workingDirectory",
20187
20719
  "\u5DE5\u4F5C\u76EE\u5F55",
@@ -20443,10 +20975,18 @@ async function patchHermesMemoryEnv(profileName, patch) {
20443
20975
  }
20444
20976
  function isMemoryEnvKeyWritable(key) {
20445
20977
  return [
20978
+ "HONCHO_API_KEY",
20979
+ "MEM0_API_KEY",
20980
+ "HINDSIGHT_API_KEY",
20981
+ "HINDSIGHT_LLM_API_KEY",
20446
20982
  "OPENVIKING_ENDPOINT",
20983
+ "OPENVIKING_API_KEY",
20447
20984
  "OPENVIKING_ACCOUNT",
20448
20985
  "OPENVIKING_USER",
20449
20986
  "OPENVIKING_AGENT",
20987
+ "SUPERMEMORY_API_KEY",
20988
+ "BRV_API_KEY",
20989
+ "RETAINDB_API_KEY",
20450
20990
  "RETAINDB_BASE_URL",
20451
20991
  "RETAINDB_PROJECT"
20452
20992
  ].includes(key);
@@ -20455,30 +20995,119 @@ function normalizeHindsightMode(value) {
20455
20995
  const mode = readString15(value) ?? "cloud";
20456
20996
  return mode === "local" ? "local_embedded" : mode;
20457
20997
  }
20458
- async function readActiveMemoryProvider(profileName) {
20459
- const raw = await readFile14(
20460
- resolveHermesConfigPath(profileName),
20461
- "utf8"
20462
- ).catch((error) => {
20463
- if (isNodeError16(error, "ENOENT")) {
20464
- return "";
20465
- }
20466
- throw error;
20467
- });
20468
- const config = raw ? toRecord14(YAML4.parse(raw)) : {};
20469
- const memory = toRecord14(config.memory);
20470
- const provider = readString15(memory.provider);
20471
- if (!provider || provider === "built-in" || provider === "builtin" || provider === "built_in") {
20998
+ function normalizeHttpUrl(value) {
20999
+ try {
21000
+ const url = new URL(value);
21001
+ return url.protocol === "http:" || url.protocol === "https:" ? url : null;
21002
+ } catch {
20472
21003
  return null;
20473
21004
  }
20474
- return provider;
20475
21005
  }
20476
- async function patchJsonProviderConfig(profileName, relativePath, patch) {
20477
- const configPath = path21.join(
20478
- resolveHermesProfileDir(profileName),
20479
- relativePath
20480
- );
20481
- const current = await readJsonObject(configPath);
21006
+ function hindsightRequestHeaders(apiKey) {
21007
+ const headers = { accept: "application/json" };
21008
+ if (isConfiguredEnvValue(apiKey)) {
21009
+ const secret = String(apiKey).trim();
21010
+ headers.authorization = `Bearer ${secret}`;
21011
+ headers["x-api-key"] = secret;
21012
+ }
21013
+ return headers;
21014
+ }
21015
+ async function probeHindsightJson(baseUrl, pathName, headers) {
21016
+ const url = joinHindsightUrl(baseUrl, pathName);
21017
+ const controller = new AbortController();
21018
+ const timer = setTimeout(
21019
+ () => controller.abort(),
21020
+ MEMORY_PROVIDER_TEST_TIMEOUT_MS
21021
+ );
21022
+ try {
21023
+ const response = await fetch(url, {
21024
+ headers,
21025
+ signal: controller.signal
21026
+ });
21027
+ const text = await response.text();
21028
+ const json = parseJsonObject2(text);
21029
+ if (!response.ok) {
21030
+ return {
21031
+ ok: false,
21032
+ detail: `HTTP ${response.status}${readHindsightError(json)}`
21033
+ };
21034
+ }
21035
+ const semanticIssue = hindsightSemanticIssue(pathName, json);
21036
+ if (semanticIssue) {
21037
+ return { ok: false, detail: semanticIssue };
21038
+ }
21039
+ return { ok: true, detail: summarizeHindsightProbe(pathName, json) };
21040
+ } catch (error) {
21041
+ return {
21042
+ ok: false,
21043
+ detail: error instanceof Error && error.name === "AbortError" ? "\u8BF7\u6C42\u8D85\u65F6" : error instanceof Error ? error.message : "\u8BF7\u6C42\u5931\u8D25"
21044
+ };
21045
+ } finally {
21046
+ clearTimeout(timer);
21047
+ }
21048
+ }
21049
+ function joinHindsightUrl(baseUrl, pathName) {
21050
+ const base = new URL(baseUrl.toString());
21051
+ if (!base.pathname.endsWith("/")) {
21052
+ base.pathname = `${base.pathname}/`;
21053
+ }
21054
+ return new URL(pathName.replace(/^\/+/u, ""), base);
21055
+ }
21056
+ function parseJsonObject2(text) {
21057
+ try {
21058
+ return toRecord14(JSON.parse(text));
21059
+ } catch {
21060
+ return {};
21061
+ }
21062
+ }
21063
+ function readHindsightError(json) {
21064
+ const detail = readString15(json.detail) ?? readString15(json.error);
21065
+ return detail ? `\uFF1A${detail}` : "";
21066
+ }
21067
+ function hindsightSemanticIssue(pathName, json) {
21068
+ if (pathName === "/health") {
21069
+ const status = readString15(json.status);
21070
+ return status && ["healthy", "ok"].includes(status.toLowerCase()) ? null : `\u5065\u5EB7\u72B6\u6001\u5F02\u5E38\uFF1A${status ?? "unknown"}`;
21071
+ }
21072
+ return null;
21073
+ }
21074
+ function summarizeHindsightProbe(pathName, json) {
21075
+ if (pathName === "/health") {
21076
+ const status = readString15(json.status) ?? "ok";
21077
+ const database = readString15(json.database);
21078
+ return database ? `${status}, database ${database}` : status;
21079
+ }
21080
+ if (pathName === "/version") {
21081
+ const version = readString15(json.api_version);
21082
+ return version ? `API ${version}` : "version endpoint reachable";
21083
+ }
21084
+ const bankId = readString15(json.bank_id);
21085
+ return bankId ? `bank ${bankId} reachable` : "bank config reachable";
21086
+ }
21087
+ async function readActiveMemoryProvider(profileName) {
21088
+ const raw = await readFile14(
21089
+ resolveHermesConfigPath(profileName),
21090
+ "utf8"
21091
+ ).catch((error) => {
21092
+ if (isNodeError16(error, "ENOENT")) {
21093
+ return "";
21094
+ }
21095
+ throw error;
21096
+ });
21097
+ const config = raw ? toRecord14(YAML4.parse(raw)) : {};
21098
+ const memory = toRecord14(config.memory);
21099
+ const provider = readString15(memory.provider);
21100
+ if (!provider || provider === "built-in" || provider === "builtin" || provider === "built_in") {
21101
+ return null;
21102
+ }
21103
+ return provider;
21104
+ }
21105
+ async function patchJsonProviderConfig(profileName, relativePath, patch) {
21106
+ const configPath = path21.join(
21107
+ resolveHermesProfileDir(profileName),
21108
+ relativePath
21109
+ );
21110
+ const current = await readJsonObject(configPath);
20482
21111
  const next = { ...current };
20483
21112
  for (const [key, value] of Object.entries(patch)) {
20484
21113
  if (value !== void 0) {
@@ -20529,6 +21158,16 @@ function stringSetting(key, label, value, editable = true) {
20529
21158
  kind: "string"
20530
21159
  };
20531
21160
  }
21161
+ function secretSetting(key, label, value, configured) {
21162
+ return {
21163
+ key,
21164
+ label,
21165
+ value: "",
21166
+ editable: true,
21167
+ kind: "secret",
21168
+ configured: configured || isConfiguredEnvValue(readString15(value))
21169
+ };
21170
+ }
20532
21171
  function textSetting(key, label, value, editable = true) {
20533
21172
  return {
20534
21173
  key,
@@ -20752,6 +21391,23 @@ function registerProfileMemoryRoutes(router, options) {
20752
21391
  }
20753
21392
  }
20754
21393
  );
21394
+ router.post(
21395
+ "/api/v1/profiles/:name/memory/providers/:provider/test",
21396
+ async (ctx) => {
21397
+ await authenticateRequest(ctx, paths);
21398
+ const body = await readJsonBody(ctx.req);
21399
+ await getHermesProfileStatus(ctx.params.name, paths);
21400
+ try {
21401
+ ctx.body = await testHermesMemoryProviderSettings(
21402
+ ctx.params.name,
21403
+ ctx.params.provider,
21404
+ readMemorySettingsPatch(body)
21405
+ );
21406
+ } catch (error) {
21407
+ throw toMemoryHttpError(error);
21408
+ }
21409
+ }
21410
+ );
20755
21411
  }
20756
21412
  function readMemoryTarget(body) {
20757
21413
  const raw = readString14(body, "target");
@@ -20818,6 +21474,10 @@ function readMemorySettingsPatch(body) {
20818
21474
  if (apiUrl !== void 0) {
20819
21475
  input.apiUrl = apiUrl;
20820
21476
  }
21477
+ const apiKey = readOptionalSecretString(body, "api_key", "apiKey");
21478
+ if (apiKey !== void 0) {
21479
+ input.apiKey = apiKey;
21480
+ }
20821
21481
  const bankId = readOptionalString(body, "bank_id", "bankId");
20822
21482
  if (bankId !== void 0) {
20823
21483
  input.bankId = bankId;
@@ -20834,6 +21494,14 @@ function readMemorySettingsPatch(body) {
20834
21494
  if (llmBaseUrl !== void 0) {
20835
21495
  input.llmBaseUrl = llmBaseUrl;
20836
21496
  }
21497
+ const llmApiKey = readOptionalSecretString(
21498
+ body,
21499
+ "llm_api_key",
21500
+ "llmApiKey"
21501
+ );
21502
+ if (llmApiKey !== void 0) {
21503
+ input.llmApiKey = llmApiKey;
21504
+ }
20837
21505
  const containerTag = readOptionalString(body, "container_tag", "containerTag");
20838
21506
  if (containerTag !== void 0) {
20839
21507
  input.containerTag = containerTag;
@@ -21034,6 +21702,10 @@ function readOptionalString(body, ...keys) {
21034
21702
  }
21035
21703
  return void 0;
21036
21704
  }
21705
+ function readOptionalSecretString(body, ...keys) {
21706
+ const value = readOptionalString(body, ...keys);
21707
+ return value && value.trim().length > 0 ? value : void 0;
21708
+ }
21037
21709
  function toMemoryHttpError(error) {
21038
21710
  if (error instanceof HermesMemoryError) {
21039
21711
  return new LinkHttpError(400, error.code, error.message);
@@ -22277,1959 +22949,929 @@ function readString17(payload, key) {
22277
22949
  }
22278
22950
 
22279
22951
  // src/link/updates.ts
22280
- import { spawn as spawn5 } from "child_process";
22281
- import { EventEmitter as EventEmitter4 } from "events";
22282
- import { mkdir as mkdir15, readFile as readFile18, rm as rm10 } from "fs/promises";
22283
- import path25 from "path";
22284
-
22285
- // src/daemon/process.ts
22286
22952
  import { spawn as spawn4 } from "child_process";
22287
- import { mkdir as mkdir14, readFile as readFile17, rm as rm9 } from "fs/promises";
22953
+ import { EventEmitter as EventEmitter4 } from "events";
22954
+ import { mkdir as mkdir13, readFile as readFile17, rm as rm8 } from "fs/promises";
22288
22955
  import path24 from "path";
22289
-
22290
- // src/daemon/service.ts
22291
- import { createServer } from "http";
22292
- import { mkdir as mkdir13, rm as rm8, writeFile as writeFile3 } from "fs/promises";
22293
-
22294
- // src/relay/control-client.ts
22295
- import WebSocket from "ws";
22296
- var RELAY_SSE_BATCH_FLUSH_INTERVAL_MS = 50;
22297
- var RELAY_SSE_BATCH_FLUSH_BYTES = 2 * 1024;
22298
- function connectRelayControl(options) {
22299
- const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
22300
- wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
22301
- wsUrl.searchParams.set("link_id", options.linkId);
22302
- const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
22303
- const backoffBaseMs = options.backoffBaseMs ?? 1e3;
22304
- const backoffMaxMs = options.backoffMaxMs ?? 3e4;
22305
- let reconnectAttempts = 0;
22306
- let closedByUser = false;
22307
- let socket = null;
22308
- let retryTimer = null;
22309
- let abortControllers = /* @__PURE__ */ new Map();
22310
- let fatalRelayRejection = null;
22311
- let latestNetworkRoutes = null;
22312
- const connect = () => {
22313
- options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
22314
- fatalRelayRejection = null;
22315
- socket = new WebSocket(wsUrl, {
22316
- headers: {
22317
- "x-hermes-link-version": LINK_VERSION
22318
- }
22319
- });
22320
- socket.on("open", () => {
22321
- reconnectAttempts = 0;
22322
- options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
22323
- const currentSocket = socket;
22324
- if (currentSocket && latestNetworkRoutes) {
22325
- sendNetworkRoutes(currentSocket, options.linkId, latestNetworkRoutes);
22326
- }
22327
- });
22328
- socket.on("message", (raw) => {
22329
- if (!socket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
22330
- return;
22331
- }
22332
- void handleFrame(socket, String(raw), options.localPort, abortControllers).catch((error) => {
22333
- const message = error instanceof Error ? error.message : "Relay request failed";
22334
- socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
22335
- });
22336
- });
22337
- socket.on("error", (error) => {
22338
- const message = error instanceof Error ? error.message : "Relay websocket error";
22339
- fatalRelayRejection = resolveFatalRelayRejection(message);
22340
- options.onStatus?.({
22341
- state: "disconnected",
22342
- attempt: reconnectAttempts,
22343
- message: fatalRelayRejection ?? message
22344
- });
22345
- });
22346
- socket.on("close", () => {
22347
- abortAll(abortControllers);
22348
- abortControllers = /* @__PURE__ */ new Map();
22349
- if (fatalRelayRejection) {
22350
- options.onStatus?.({
22351
- state: "failed",
22352
- attempt: reconnectAttempts,
22353
- message: fatalRelayRejection
22354
- });
22355
- return;
22356
- }
22357
- if (closedByUser) {
22358
- options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
22359
- return;
22360
- }
22361
- if (reconnectAttempts >= maxReconnectAttempts) {
22362
- options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
22363
- return;
22364
- }
22365
- reconnectAttempts += 1;
22366
- const delay3 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
22367
- options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay3}ms` });
22368
- retryTimer = setTimeout(connect, delay3);
22369
- retryTimer.unref?.();
22370
- });
22371
- };
22372
- connect();
22956
+ var SERVER_LINK_CURRENT_RELEASE_PATH = "/api/v1/link/releases/current";
22957
+ var LINK_NPM_PACKAGE = "@hermespilot/link";
22958
+ var OFFICIAL_INSTALLER_BASE_URL = "https://raw.githubusercontent.com/HangbinYang/hermespilot-install/main";
22959
+ var OFFICIAL_UNIX_INSTALLER_URL = `${OFFICIAL_INSTALLER_BASE_URL}/install.sh`;
22960
+ var OFFICIAL_WINDOWS_INSTALLER_URL = `${OFFICIAL_INSTALLER_BASE_URL}/install.ps1`;
22961
+ var UPDATE_LOG_FILE2 = "link-update.log";
22962
+ var UPDATE_LOG_MAX_FILES2 = 3;
22963
+ var UPDATE_FETCH_TIMEOUT_MS = 5e3;
22964
+ var MAX_UPDATE_LOG_LINES2 = 240;
22965
+ var MAX_OUTPUT_LINE_LENGTH3 = 1200;
22966
+ var updateEvents2 = new EventEmitter4();
22967
+ var runningUpdate2 = null;
22968
+ async function readLinkUpdateCheck(options) {
22969
+ const remoteResult = await readRemoteLinkPolicy(options);
22970
+ const remote = remoteResult.remote;
22971
+ const state = computeLinkUpdateState(LINK_VERSION, remote);
22972
+ const targetVersion = remote?.target_version ?? null;
22373
22973
  return {
22374
- publishNetworkRoutes(routes) {
22375
- latestNetworkRoutes = routes;
22376
- if (socket?.readyState === WebSocket.OPEN) {
22377
- sendNetworkRoutes(socket, options.linkId, routes);
22378
- }
22974
+ ok: true,
22975
+ local: {
22976
+ version: LINK_VERSION,
22977
+ raw: LINK_VERSION
22379
22978
  },
22380
- close() {
22381
- closedByUser = true;
22382
- if (retryTimer) {
22383
- clearTimeout(retryTimer);
22384
- retryTimer = null;
22385
- }
22386
- abortAll(abortControllers);
22387
- socket?.terminate();
22979
+ remote,
22980
+ state,
22981
+ update_available: state === "update_available" || state === "unsafe" || state === "blocked",
22982
+ unsafe: state === "unsafe",
22983
+ blocked: state === "blocked",
22984
+ check_state: remoteResult.state,
22985
+ issue: remoteResult.issue,
22986
+ manual: {
22987
+ command: targetVersion ? manualInstallCommand(targetVersion) : null,
22988
+ package: LINK_NPM_PACKAGE,
22989
+ version: targetVersion
22388
22990
  }
22389
22991
  };
22390
22992
  }
22391
- function sendNetworkRoutes(socket, linkId, routes) {
22392
- socket.send(JSON.stringify({
22393
- type: "network.routes",
22394
- id: `routes_${Date.now().toString(36)}`,
22395
- payload: {
22396
- link_id: linkId,
22397
- lan_ips: routes.lanIps,
22398
- public_ipv4s: routes.publicIpv4s,
22399
- public_ipv6s: routes.publicIpv6s,
22400
- observed_at: (/* @__PURE__ */ new Date()).toISOString()
22401
- }
22402
- }));
22403
- }
22404
- function resolveFatalRelayRejection(message) {
22405
- if (!/Unexpected server response:\s*(400|401|403|426)\b/u.test(message)) {
22406
- return null;
22993
+ async function startLinkUpdate(options) {
22994
+ const current = await readLinkUpdateStatus(options.paths);
22995
+ if (runningUpdate2 || current.state === "running") {
22996
+ return current;
22407
22997
  }
22408
- return "Relay refused the Hermes Link connection. Check Link version and pairing state before retrying.";
22409
- }
22410
- function abortAll(abortControllers) {
22411
- for (const controller of abortControllers.values()) {
22412
- controller.abort();
22998
+ const check = await readLinkUpdateCheck(options);
22999
+ const targetVersion = check.remote?.target_version ?? null;
23000
+ if (!targetVersion) {
23001
+ return writeFailedStartState(
23002
+ options,
23003
+ "HermesPilot Server has no Link target version."
23004
+ );
22413
23005
  }
22414
- abortControllers.clear();
22415
- }
22416
- function computeBackoffMs(attempt, baseMs, maxMs) {
22417
- const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1));
22418
- const jitter = Math.floor(Math.random() * Math.min(1e3, exponential * 0.2));
22419
- return exponential + jitter;
22420
- }
22421
- async function handleFrame(socket, raw, localPort, abortControllers) {
22422
- const frame = JSON.parse(raw);
22423
- if (frame.type === "http.cancel") {
22424
- abortControllers.get(frame.id)?.abort();
22425
- abortControllers.delete(frame.id);
22426
- return;
23006
+ if (!isValidReleaseVersion(targetVersion)) {
23007
+ return writeFailedStartState(
23008
+ options,
23009
+ `HermesPilot Server returned invalid Link target version: ${targetVersion}.`,
23010
+ targetVersion
23011
+ );
22427
23012
  }
22428
- if (frame.type !== "http.request") {
22429
- return;
23013
+ if (options.targetVersion && options.targetVersion !== targetVersion) {
23014
+ return writeFailedStartState(
23015
+ options,
23016
+ `Requested target ${options.targetVersion} does not match current Link target ${targetVersion}.`,
23017
+ targetVersion
23018
+ );
22430
23019
  }
22431
- const abortController = new AbortController();
22432
- abortControllers.set(frame.id, abortController);
22433
- let sseBatcher = null;
22434
- try {
22435
- const response = await fetch(`http://127.0.0.1:${localPort}${frame.path}`, {
22436
- method: frame.method,
22437
- headers: frame.headers ?? {},
22438
- body: frame.bodyBase64 ? Buffer.from(frame.bodyBase64, "base64") : void 0,
22439
- signal: abortController.signal
22440
- });
22441
- const headers = Object.fromEntries(response.headers.entries());
22442
- const contentType = response.headers.get("content-type") ?? "";
22443
- if (response.body && contentType.includes("text/event-stream")) {
22444
- socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
22445
- sseBatcher = createRelayStreamChunkBatcher(socket, frame.id);
22446
- const reader = response.body.getReader();
22447
- while (true) {
22448
- const next = await reader.read();
22449
- if (next.done) {
22450
- break;
22451
- }
22452
- sseBatcher.push(next.value);
22453
- }
22454
- sseBatcher.flush();
22455
- socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
22456
- return;
22457
- }
22458
- const body = Buffer.from(await response.arrayBuffer()).toString("base64");
22459
- socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
22460
- } catch (error) {
22461
- if (abortController.signal.aborted || isAbortError2(error)) {
22462
- return;
22463
- }
22464
- sseBatcher?.flush();
22465
- const message = error instanceof Error ? error.message : "Relay request failed";
22466
- socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
22467
- } finally {
22468
- sseBatcher?.dispose();
22469
- abortControllers.delete(frame.id);
23020
+ if (check.state === "current") {
23021
+ return writeFailedStartState(
23022
+ options,
23023
+ "Hermes Link is already on the current version.",
23024
+ targetVersion
23025
+ );
22470
23026
  }
22471
- }
22472
- function isAbortError2(error) {
22473
- return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
22474
- }
22475
- function createRelayStreamChunkBatcher(socket, id) {
22476
- let chunks = [];
22477
- let totalBytes = 0;
22478
- let flushTimer = null;
22479
- const clearFlushTimer = () => {
22480
- if (flushTimer == null) {
22481
- return;
22482
- }
22483
- clearTimeout(flushTimer);
22484
- flushTimer = null;
22485
- };
22486
- const flush = () => {
22487
- clearFlushTimer();
22488
- if (totalBytes <= 0) {
22489
- return;
22490
- }
22491
- const bodyBase64 = Buffer.concat(chunks, totalBytes).toString("base64");
22492
- chunks = [];
22493
- totalBytes = 0;
22494
- if (socket.readyState !== WebSocket.OPEN) {
22495
- return;
22496
- }
22497
- socket.send(JSON.stringify({ type: "http.stream.chunk", id, bodyBase64 }));
22498
- };
22499
- const scheduleFlush = () => {
22500
- if (flushTimer != null) {
22501
- return;
22502
- }
22503
- flushTimer = setTimeout(() => {
22504
- flushTimer = null;
22505
- flush();
22506
- }, RELAY_SSE_BATCH_FLUSH_INTERVAL_MS);
22507
- flushTimer.unref?.();
23027
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
23028
+ const jobId = `link_update_${now().getTime().toString(36)}`;
23029
+ await clearUpdateLogFiles2(options.paths);
23030
+ const writer = createRotatingTextLogWriter({
23031
+ paths: options.paths,
23032
+ fileName: UPDATE_LOG_FILE2,
23033
+ maxFileBytes: 512 * 1024,
23034
+ maxFiles: UPDATE_LOG_MAX_FILES2
23035
+ });
23036
+ const startedAt = now().toISOString();
23037
+ const installCommand = buildOfficialInstallCommand(targetVersion);
23038
+ const manualCommand = installCommand.displayCommand;
23039
+ const started = {
23040
+ state: "running",
23041
+ job_id: jobId,
23042
+ pid: null,
23043
+ target_version: targetVersion,
23044
+ started_at: startedAt,
23045
+ finished_at: null,
23046
+ exit_code: null,
23047
+ signal: null,
23048
+ error: null,
23049
+ manual_command: manualCommand
22508
23050
  };
22509
- return {
22510
- push(chunk) {
22511
- if (chunk.byteLength <= 0) {
22512
- return;
22513
- }
22514
- const buffer = Buffer.from(chunk);
22515
- chunks.push(buffer);
22516
- totalBytes += buffer.byteLength;
22517
- if (totalBytes >= RELAY_SSE_BATCH_FLUSH_BYTES) {
22518
- flush();
22519
- return;
22520
- }
22521
- scheduleFlush();
22522
- },
22523
- flush,
22524
- dispose() {
22525
- clearFlushTimer();
22526
- chunks = [];
22527
- totalBytes = 0;
22528
- }
23051
+ await mkdir13(options.paths.runDir, { recursive: true, mode: 448 });
23052
+ await writer.write(
23053
+ `
23054
+ === link update started ${startedAt} target=${targetVersion} ===
23055
+ `
23056
+ );
23057
+ await writer.write(`$ ${manualCommand}
23058
+ `);
23059
+ await writeUpdateState2(options.paths, started);
23060
+ const child = spawnInstallCommand(installCommand);
23061
+ started.pid = child.pid ?? null;
23062
+ await writeUpdateState2(options.paths, started);
23063
+ const appendChunk = async (chunk) => {
23064
+ await writer.write(chunk);
23065
+ await emitUpdateStatus2(options.paths);
22529
23066
  };
22530
- }
22531
-
22532
- // src/runtime/system-info.ts
22533
- import { execFileSync } from "child_process";
22534
- import { readFileSync } from "fs";
22535
- import os4 from "os";
22536
- function readLinkSystemInfo() {
22537
- const platform = process.platform;
22538
- const hostname = readHostname(platform);
22539
- const osLabel = readOsLabel(platform);
22540
- const defaultDisplayName = buildDefaultDisplayName({ hostname, osLabel, platform });
22541
- return {
22542
- platform,
22543
- hostname,
22544
- osLabel,
22545
- defaultDisplayName
22546
- };
22547
- }
22548
- function buildDefaultDisplayName(input) {
22549
- const hostname = normalizeText(input.hostname);
22550
- const osLabel = normalizeText(input.osLabel);
22551
- if (hostname) {
22552
- return truncateText(hostname, 128);
22553
- }
22554
- return truncateText(hostname ?? osLabel ?? `Hermes Link ${input.platform}`, 128);
22555
- }
22556
- function parseLinuxOsRelease(content) {
22557
- const values = /* @__PURE__ */ new Map();
22558
- for (const line of content.split(/\r?\n/u)) {
22559
- const match = /^([A-Z0-9_]+)=(.*)$/u.exec(line.trim());
22560
- if (!match) {
22561
- continue;
22562
- }
22563
- values.set(match[1], unquoteOsReleaseValue(match[2]));
22564
- }
22565
- return normalizeText(values.get("PRETTY_NAME")) ?? buildLinuxName(values);
22566
- }
22567
- function readHostname(platform) {
22568
- if (platform === "darwin") {
22569
- const computerName = readCommandOutput("scutil", ["--get", "ComputerName"]);
22570
- if (computerName) {
22571
- return computerName;
22572
- }
22573
- }
22574
- return normalizeText(os4.hostname());
22575
- }
22576
- function readOsLabel(platform) {
22577
- if (platform === "darwin") {
22578
- const version = readCommandOutput("sw_vers", ["-productVersion"]);
22579
- return version ? `macOS ${version}` : "macOS";
22580
- }
22581
- if (platform === "linux") {
22582
- return readLinuxOsRelease() ?? `Linux ${os4.release()}`;
22583
- }
22584
- if (platform === "win32") {
22585
- return `Windows ${os4.release()}`;
22586
- }
22587
- return `${os4.type()} ${os4.release()}`.trim();
22588
- }
22589
- function readLinuxOsRelease() {
22590
- for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
22591
- try {
22592
- return parseLinuxOsRelease(readFileSync(file, "utf8"));
22593
- } catch {
22594
- }
22595
- }
22596
- return null;
22597
- }
22598
- function readCommandOutput(command, args) {
22599
- try {
22600
- const output = execFileSync(command, args, {
22601
- encoding: "utf8",
22602
- stdio: ["ignore", "pipe", "ignore"],
22603
- timeout: 1e3
22604
- });
22605
- return normalizeText(output);
22606
- } catch {
22607
- return null;
22608
- }
22609
- }
22610
- function buildLinuxName(values) {
22611
- const name = normalizeText(values.get("NAME"));
22612
- const version = normalizeText(values.get("VERSION_ID")) ?? normalizeText(values.get("VERSION"));
22613
- if (name && version) {
22614
- return `${name} ${version}`;
22615
- }
22616
- return name ?? version;
22617
- }
22618
- function unquoteOsReleaseValue(value) {
22619
- const trimmed = value.trim();
22620
- if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
22621
- return trimmed.slice(1, -1).replace(/\\(["'`$\\])/gu, "$1");
22622
- }
22623
- return trimmed;
22624
- }
22625
- function normalizeText(value) {
22626
- const normalized = value?.replace(/\s+/gu, " ").trim();
22627
- return normalized ? normalized : null;
22628
- }
22629
- function truncateText(value, maxLength) {
22630
- return value.length > maxLength ? value.slice(0, maxLength).trimEnd() : value;
22631
- }
22632
-
22633
- // src/topology/network.ts
22634
- import os6 from "os";
22635
-
22636
- // src/topology/environment.ts
22637
- import { existsSync, readFileSync as readFileSync2 } from "fs";
22638
- import os5 from "os";
22639
- function detectRuntimeEnvironment(env = process.env) {
22640
- if (isWsl(env)) {
22641
- return {
22642
- kind: "wsl",
22643
- lanAutoDiscoveryUsable: false,
22644
- warning: "Detected WSL. The LAN IP found inside WSL is usually a private VM address and is not reachable from your phone. Use Relay or set `hermeslink config set lan-host <Windows LAN IP>`."
22645
- };
22646
- }
22647
- if (isContainer(env)) {
22648
- return {
22649
- kind: "container",
22650
- lanAutoDiscoveryUsable: false,
22651
- warning: "Detected a container environment. Container LAN IPs are usually not reachable from your phone. Use Relay or set `hermeslink config set lan-host <host LAN IP>`."
22652
- };
22653
- }
22654
- return {
22655
- kind: "native",
22656
- lanAutoDiscoveryUsable: true,
22657
- warning: null
22658
- };
22659
- }
22660
- function isWsl(env) {
22661
- if (process.platform !== "linux") {
22662
- return false;
22663
- }
22664
- if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
22665
- return true;
22666
- }
22667
- const release = os5.release().toLowerCase();
22668
- return release.includes("microsoft") || release.includes("wsl");
22669
- }
22670
- function isContainer(env) {
22671
- if (env.container || env.CONTAINER || env.KUBERNETES_SERVICE_HOST) {
22672
- return true;
22673
- }
22674
- if (existsSync("/.dockerenv")) {
22675
- return true;
22676
- }
22677
- try {
22678
- const cgroup = readFileSync2("/proc/1/cgroup", "utf8").toLowerCase();
22679
- return /docker|containerd|kubepods|libpod|podman/u.test(cgroup);
22680
- } catch {
22681
- return false;
22682
- }
22683
- }
22684
-
22685
- // src/topology/network.ts
22686
- var VIRTUAL_INTERFACE_NAME_PATTERN = /(docker|veth|vmnet|vmenet|vbox|virtualbox|vmware|tailscale|zerotier|wireguard|utun|virbr|hyper-v|vethernet|loopback|\blo\b|^lo\d*$|^br-|^bridge\d+$|^zt|^tun|^tap|awdl|llw|anpi|gif|stf|ipsec|ppp)/iu;
22687
- var MAX_LAN_IPS = 4;
22688
- var MAX_PUBLIC_IPV4S = 2;
22689
- var MAX_PUBLIC_IPV6S = 2;
22690
- async function discoverRouteCandidates(options) {
22691
- const environment = detectRuntimeEnvironment();
22692
- const configuredLanHost = normalizeLanHost(options.configuredLanHost);
22693
- const lanIps = configuredLanHost ? [configuredLanHost] : environment.lanAutoDiscoveryUsable ? discoverLanIps() : [];
22694
- const publicIps = options.relayBootstrapToken || options.observePublicRoute ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
22695
- const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
22696
- const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
22697
- const preferredUrls = [
22698
- ...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
22699
- ...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
22700
- ...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
22701
- `${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/links/${options.linkId}`
22702
- ];
22703
- return {
22704
- lanIps,
22705
- publicIpv4s,
22706
- publicIpv6s,
22707
- preferredUrls,
22708
- environment
22709
- };
22710
- }
22711
- function discoverLanIps() {
22712
- return discoverLanIpsFromInterfaces(os6.networkInterfaces());
22713
- }
22714
- function discoverLanIpsFromInterfaces(interfaces) {
22715
- const result = /* @__PURE__ */ new Set();
22716
- const candidates = [];
22717
- for (const [name, items] of Object.entries(interfaces)) {
22718
- if (shouldIgnoreInterface(name)) {
22719
- continue;
22720
- }
22721
- for (const item of items ?? []) {
22722
- if (!item.internal && item.address && item.family === "IPv4" && isUsableLanIpv42(item.address, item.netmask)) {
22723
- candidates.push({ name, address: item.address });
22724
- }
22725
- }
22726
- }
22727
- for (const candidate of candidates.sort(compareLanCandidate)) {
22728
- result.add(candidate.address);
22729
- }
22730
- return [...result].slice(0, MAX_LAN_IPS);
22731
- }
22732
- async function observePublicRoute(options) {
22733
- const fetcher = options.fetchImpl ?? fetch;
22734
- const response = await fetcher(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/public-route/observe`, {
22735
- method: "POST",
22736
- headers: {
22737
- "content-type": "application/json",
22738
- ...options.relayBootstrapToken ? { authorization: `Bearer ${options.relayBootstrapToken}` } : {}
22739
- },
22740
- body: JSON.stringify({
22741
- install_id: options.installId,
22742
- link_id: options.linkId,
22743
- public_key_pem: options.publicKeyPem
22744
- })
22745
- });
22746
- const payload = await response.json().catch(() => null);
22747
- const record = typeof payload?.record === "object" && payload.record !== null ? payload.record : null;
22748
- const observed = typeof payload?.observed === "object" && payload.observed !== null ? payload.observed : null;
22749
- const values = [
22750
- readIpRecord(record?.ipv4),
22751
- readIpRecord(record?.ipv6),
22752
- typeof observed?.ip === "string" ? observed.ip : null
22753
- ].filter((value) => Boolean(value));
22754
- return {
22755
- publicIpv4s: unique(values.filter(isUsablePublicIpv4)),
22756
- publicIpv6s: unique(values.filter(isUsablePublicIpv6))
22757
- };
22758
- }
22759
- function readIpRecord(value) {
22760
- if (typeof value !== "object" || value === null) {
22761
- return null;
22762
- }
22763
- const ip = value.ip;
22764
- return typeof ip === "string" && ip.trim() ? ip.trim() : null;
22765
- }
22766
- function buildDirectUrl(ip, port) {
22767
- return `http://${ip.includes(":") ? `[${ip}]` : ip}:${port}`;
22768
- }
22769
- function shouldIgnoreInterface(name) {
22770
- return !name.trim() || VIRTUAL_INTERFACE_NAME_PATTERN.test(name);
22771
- }
22772
- function compareLanCandidate(left, right) {
22773
- const priority = interfacePriority(left.name) - interfacePriority(right.name);
22774
- return priority || left.name.localeCompare(right.name) || left.address.localeCompare(right.address);
22775
- }
22776
- function interfacePriority(name) {
22777
- if (/^(en|eth|wlan|wi-fi|wifi)/iu.test(name)) {
22778
- return 0;
22779
- }
22780
- return 1;
22781
- }
22782
- function isUsableLanIpv42(address, netmask) {
22783
- return isPrivateIpv4(address) && !isNetworkOrBroadcastIpv4Address(address, netmask);
22784
- }
22785
- function isUsablePublicIpv4(address) {
22786
- return isValidIpv4(address) && !isSpecialIpv4(address);
22787
- }
22788
- function isUsablePublicIpv6(address) {
22789
- const normalized = address.toLowerCase();
22790
- return normalized.includes(":") && !normalized.startsWith("fe80:") && !normalized.startsWith("fc") && !normalized.startsWith("fd") && !normalized.startsWith("ff") && normalized !== "::" && normalized !== "::1";
22791
- }
22792
- function isPrivateIpv4(address) {
22793
- const parts = parseIpv4Segments(address);
22794
- if (!parts) {
22795
- return false;
22796
- }
22797
- const [first, second] = parts;
22798
- return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
22799
- }
22800
- function isSpecialIpv4(address) {
22801
- const parts = parseIpv4Segments(address);
22802
- if (!parts) {
22803
- return true;
22804
- }
22805
- const [first, second, third, fourth] = parts;
22806
- return first === 0 || first === 10 || first === 127 || first >= 224 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 192 && second === 0 && third === 2 || first === 198 && (second === 18 || second === 19) || first === 198 && second === 51 && third === 100 || first === 203 && second === 0 && third === 113 || first === 255 && second === 255 && third === 255 && fourth === 255;
22807
- }
22808
- function isNetworkOrBroadcastIpv4Address(address, netmask) {
22809
- const addressParts = parseIpv4Segments(address);
22810
- const netmaskParts = netmask ? parseIpv4Segments(netmask) : null;
22811
- if (!addressParts) {
22812
- return true;
22813
- }
22814
- if (!netmaskParts) {
22815
- const last = addressParts[3];
22816
- return last === 0 || last === 255;
22817
- }
22818
- const addressInt = ipv4SegmentsToInt(addressParts);
22819
- const netmaskInt = ipv4SegmentsToInt(netmaskParts);
22820
- const hostMask = ~netmaskInt >>> 0;
22821
- if (hostMask === 0) {
22822
- return false;
22823
- }
22824
- const networkInt = addressInt & netmaskInt;
22825
- const broadcastInt = (networkInt | hostMask) >>> 0;
22826
- return addressInt === networkInt || addressInt === broadcastInt;
22827
- }
22828
- function isValidIpv4(address) {
22829
- return Boolean(parseIpv4Segments(address));
22830
- }
22831
- function parseIpv4Segments(address) {
22832
- if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(address)) {
22833
- return null;
22834
- }
22835
- const parts = address.split(".").map((part) => Number.parseInt(part, 10));
22836
- if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
22837
- return null;
22838
- }
22839
- return parts;
22840
- }
22841
- function ipv4SegmentsToInt(parts) {
22842
- return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
22843
- }
22844
- function unique(values) {
22845
- return [...new Set(values)];
22846
- }
22847
-
22848
- // src/link/network-report-state.ts
22849
- var DEFAULT_AUTO_DAILY_LIMIT = 20;
22850
- async function readNetworkReportState(paths) {
22851
- const state = await readLinkState(paths);
22852
- return normalizeNetworkReportState(state.networkReport);
22853
- }
22854
- async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
22855
- const snapshot = normalizeNetworkSnapshot(snapshotInput);
22856
- await updateNetworkReportState(paths, (current) => ({
22857
- ...current,
22858
- lastReportedLanIps: snapshot.lanIps,
22859
- lastReportedPublicIpv4s: snapshot.publicIpv4s,
22860
- lastReportedPublicIpv6s: snapshot.publicIpv6s,
22861
- lastReportedAt: reportedAt.toISOString(),
22862
- lastAutoAttempt: current.lastAutoAttempt ? { ...current.lastAutoAttempt, ...snapshot, success: true } : null
22863
- }));
22864
- }
22865
- async function reserveAutomaticNetworkReport(paths, snapshotInput, options = {}) {
22866
- const snapshot = normalizeNetworkSnapshot(snapshotInput);
22867
- const now = options.now ?? /* @__PURE__ */ new Date();
22868
- const dailyLimit = Math.max(0, Math.floor(options.dailyLimit ?? DEFAULT_AUTO_DAILY_LIMIT));
22869
- let reservation = { allowed: false, reason: "unchanged" };
22870
- await updateNetworkReportState(paths, (current) => {
22871
- if (sameNetworkSnapshot(readReportedSnapshot(current), snapshot)) {
22872
- const lastReportedAt = current.lastReportedAt ? Date.parse(current.lastReportedAt) : Number.NaN;
22873
- const unchangedMinIntervalMs = Math.max(0, Math.floor(options.unchangedMinIntervalMs ?? 0));
22874
- if (!options.force || Number.isFinite(lastReportedAt) && now.getTime() - lastReportedAt < unchangedMinIntervalMs) {
22875
- reservation = { allowed: false, reason: "unchanged" };
22876
- return current;
22877
- }
22878
- }
22879
- if (current.lastAutoAttempt && !current.lastAutoAttempt.success && sameNetworkSnapshot(readAttemptSnapshot(current.lastAutoAttempt), snapshot)) {
22880
- reservation = { allowed: false, reason: "failed_snapshot_not_retried" };
22881
- return current;
22882
- }
22883
- const quotaDay = formatUtcDay(now);
22884
- const reportsToday = current.autoQuotaDay === quotaDay ? current.autoReportsToday : 0;
22885
- if (reportsToday >= dailyLimit) {
22886
- reservation = { allowed: false, reason: "daily_limit_reached" };
22887
- return current;
22888
- }
22889
- reservation = { allowed: true };
22890
- return {
22891
- ...current,
22892
- autoQuotaDay: quotaDay,
22893
- autoReportsToday: reportsToday + 1,
22894
- lastAutoAttempt: {
22895
- ...snapshot,
22896
- attemptedAt: now.toISOString(),
22897
- success: false
22898
- }
22899
- };
22900
- });
22901
- return reservation;
22902
- }
22903
- async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
22904
- const state = await readNetworkReportState(paths);
22905
- return {
22906
- ...snapshotInput,
22907
- publicIpv4s: uniqueStrings([
22908
- ...snapshotInput.publicIpv4s,
22909
- ...state.lastReportedPublicIpv4s
22910
- ]).slice(0, 2),
22911
- publicIpv6s: uniqueStrings([
22912
- ...snapshotInput.publicIpv6s,
22913
- ...state.lastReportedPublicIpv6s
22914
- ]).slice(0, 2)
22915
- };
22916
- }
22917
- async function updateNetworkReportState(paths, update) {
22918
- const state = await readLinkState(paths);
22919
- const next = {
22920
- ...state,
22921
- networkReport: update(normalizeNetworkReportState(state.networkReport))
22922
- };
22923
- await writeJsonFile(paths.stateFile, next);
22924
- }
22925
- async function readLinkState(paths) {
22926
- const state = await readJsonFile(paths.stateFile);
22927
- return state && typeof state === "object" ? state : {};
22928
- }
22929
- function normalizeNetworkReportState(value) {
22930
- const record = value && typeof value === "object" ? value : {};
22931
- return {
22932
- lastReportedLanIps: normalizeLanIps(record.lastReportedLanIps),
22933
- lastReportedPublicIpv4s: normalizeLanIps(record.lastReportedPublicIpv4s),
22934
- lastReportedPublicIpv6s: normalizeLanIps(record.lastReportedPublicIpv6s),
22935
- lastReportedAt: typeof record.lastReportedAt === "string" ? record.lastReportedAt : null,
22936
- autoQuotaDay: typeof record.autoQuotaDay === "string" ? record.autoQuotaDay : null,
22937
- autoReportsToday: Number.isFinite(record.autoReportsToday) ? Math.max(0, Math.floor(record.autoReportsToday ?? 0)) : 0,
22938
- lastAutoAttempt: normalizeAttempt(record.lastAutoAttempt)
22939
- };
22940
- }
22941
- function normalizeAttempt(value) {
22942
- if (!value || typeof value !== "object") {
22943
- return null;
22944
- }
22945
- const record = value;
22946
- if (typeof record.attemptedAt !== "string") {
22947
- return null;
22948
- }
22949
- return {
22950
- lanIps: normalizeLanIps(record.lanIps),
22951
- publicIpv4s: normalizeLanIps(record.publicIpv4s),
22952
- publicIpv6s: normalizeLanIps(record.publicIpv6s),
22953
- attemptedAt: record.attemptedAt,
22954
- success: record.success === true
22955
- };
22956
- }
22957
- function normalizeNetworkSnapshot(value) {
22958
- if (Array.isArray(value)) {
22959
- return {
22960
- lanIps: normalizeLanIps(value),
22961
- publicIpv4s: [],
22962
- publicIpv6s: []
22963
- };
22964
- }
22965
- const record = value && typeof value === "object" ? value : {};
22966
- return {
22967
- lanIps: normalizeLanIps(record.lanIps),
22968
- publicIpv4s: normalizeLanIps(record.publicIpv4s),
22969
- publicIpv6s: normalizeLanIps(record.publicIpv6s)
22970
- };
22971
- }
22972
- function readReportedSnapshot(state) {
22973
- return {
22974
- lanIps: state.lastReportedLanIps,
22975
- publicIpv4s: state.lastReportedPublicIpv4s,
22976
- publicIpv6s: state.lastReportedPublicIpv6s
22977
- };
22978
- }
22979
- function readAttemptSnapshot(attempt) {
22980
- return {
22981
- lanIps: attempt.lanIps,
22982
- publicIpv4s: attempt.publicIpv4s,
22983
- publicIpv6s: attempt.publicIpv6s
22984
- };
22985
- }
22986
- function normalizeLanIps(value) {
22987
- if (!Array.isArray(value)) {
22988
- return [];
22989
- }
22990
- return [
22991
- ...new Set(
22992
- value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
22993
- )
22994
- ];
22995
- }
22996
- function sameNetworkSnapshot(left, right) {
22997
- return sameStringList(left.lanIps, right.lanIps) && sameStringList(left.publicIpv4s, right.publicIpv4s) && sameStringList(left.publicIpv6s, right.publicIpv6s);
22998
- }
22999
- function sameStringList(left, right) {
23000
- if (left.length !== right.length) {
23001
- return false;
23002
- }
23003
- return left.every((value, index) => value === right[index]);
23004
- }
23005
- function uniqueStrings(values) {
23006
- return [...new Set(values)];
23007
- }
23008
- function formatUtcDay(date) {
23009
- return date.toISOString().slice(0, 10);
23010
- }
23011
-
23012
- // src/link/server-report.ts
23013
- async function reportLinkStatusToServer(options = {}) {
23014
- const paths = options.paths ?? resolveRuntimePaths();
23015
- const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
23016
- if (!identity?.link_id) {
23017
- return null;
23018
- }
23019
- const discoveredRoutes = options.routes ?? await discoverRouteCandidates({
23020
- port: config.port,
23021
- relayBaseUrl: config.relayBaseUrl,
23022
- linkId: identity.link_id,
23023
- installId: identity.install_id,
23024
- publicKeyPem: identity.public_key_pem,
23025
- observePublicRoute: true,
23026
- configuredLanHost: config.lanHost,
23027
- fetchImpl: options.fetchImpl
23028
- });
23029
- const routes = await mergeLastReportedPublicRoutes(paths, discoveredRoutes);
23030
- const systemInfo = readLinkSystemInfo();
23031
- const payload = {
23032
- type: "hermes_link_status_report",
23033
- link_id: identity.link_id,
23034
- install_id: identity.install_id,
23035
- link_version: LINK_VERSION,
23036
- display_name: systemInfo.defaultDisplayName,
23037
- platform: systemInfo.platform,
23038
- hostname: systemInfo.hostname ?? void 0,
23039
- lan_ips: routes.lanIps,
23040
- public_ipv4s: routes.publicIpv4s,
23041
- public_ipv6s: routes.publicIpv6s,
23042
- reported_at: (/* @__PURE__ */ new Date()).toISOString()
23043
- };
23044
- const signature = signIdentityPayload(identity, canonicalJson(payload));
23045
- const fetcher = options.fetchImpl ?? fetch;
23046
- const response = await fetcher(
23047
- `${config.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/links/${encodeURIComponent(identity.link_id)}/report`,
23048
- {
23049
- method: "POST",
23050
- headers: {
23051
- accept: "application/json",
23052
- "content-type": "application/json"
23053
- },
23054
- body: JSON.stringify({
23055
- ...payload,
23056
- public_key_pem: identity.public_key_pem,
23057
- signature
23058
- })
23059
- }
23060
- );
23061
- const body = await response.json().catch(() => null);
23062
- if (!response.ok || !body) {
23063
- const message = readErrorMessage3(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
23064
- throw new LinkHttpError(response.status, "server_request_failed", message);
23065
- }
23066
- await markNetworkStatusReported(paths, routes);
23067
- return body;
23068
- }
23069
- function canonicalJson(value) {
23070
- return JSON.stringify(sortJsonValue(value));
23071
- }
23072
- function sortJsonValue(value) {
23073
- if (Array.isArray(value)) {
23074
- return value.map(sortJsonValue);
23075
- }
23076
- if (value && typeof value === "object") {
23077
- const record = value;
23078
- const sorted = {};
23079
- for (const key of Object.keys(record).sort()) {
23080
- sorted[key] = sortJsonValue(record[key]);
23081
- }
23082
- return sorted;
23083
- }
23084
- return value;
23085
- }
23086
- function readErrorMessage3(payload) {
23087
- if (typeof payload !== "object" || payload === null) {
23088
- return null;
23089
- }
23090
- const error = payload.error;
23091
- if (typeof error !== "object" || error === null) {
23092
- return null;
23093
- }
23094
- const message = error.message;
23095
- return typeof message === "string" ? message : null;
23096
- }
23097
-
23098
- // src/daemon/lan-ip-monitor.ts
23099
- var DEFAULT_INTERVAL_MS = 5 * 6e4;
23100
- var DEFAULT_DAILY_REPORT_LIMIT = 20;
23101
- var DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS = 15 * 6e4;
23102
- function startLanIpMonitor(options) {
23103
- let running = false;
23104
- let closed = false;
23105
- let current = Promise.resolve();
23106
- const check = async (context = {}) => {
23107
- if (running || closed) {
23108
- return;
23109
- }
23110
- running = true;
23111
- try {
23112
- await checkLanIpChange(options, context);
23113
- } catch (error) {
23114
- void options.logger.warn("lan_ip_monitor_failed", {
23115
- error: error instanceof Error ? error.message : String(error)
23116
- });
23117
- } finally {
23118
- running = false;
23119
- }
23120
- };
23121
- current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
23122
- const timer = setInterval(() => {
23123
- current = check({ observePublicRoute: false });
23124
- }, options.intervalMs ?? DEFAULT_INTERVAL_MS);
23125
- timer.unref?.();
23126
- return {
23127
- async refreshPublicRoutes() {
23128
- current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
23129
- await current;
23130
- },
23131
- async close() {
23132
- closed = true;
23133
- clearInterval(timer);
23134
- await current.catch(() => void 0);
23135
- }
23136
- };
23137
- }
23138
- async function checkLanIpChange(options, context = {}) {
23139
- const [identity, config] = await Promise.all([
23140
- loadIdentity(options.paths),
23141
- loadConfig(options.paths)
23142
- ]);
23143
- if (!identity?.link_id) {
23144
- return;
23145
- }
23146
- const discoveredRoutes = await discoverRouteCandidates({
23147
- port: config.port,
23148
- relayBaseUrl: config.relayBaseUrl,
23149
- linkId: identity.link_id,
23150
- installId: identity.install_id,
23151
- publicKeyPem: identity.public_key_pem,
23152
- observePublicRoute: context.observePublicRoute === true,
23153
- configuredLanHost: config.lanHost,
23154
- fetchImpl: options.fetchImpl
23155
- });
23156
- const routes = await mergeLastReportedPublicRoutes(options.paths, discoveredRoutes);
23157
- if (context.publishToRelay) {
23158
- options.onNetworkRoutes?.(routes);
23159
- }
23160
- const reservation = await reserveAutomaticNetworkReport(options.paths, routes, {
23161
- dailyLimit: options.dailyReportLimit ?? DEFAULT_DAILY_REPORT_LIMIT,
23162
- force: context.forceReport === true,
23163
- unchangedMinIntervalMs: options.startupReportMinIntervalMs ?? DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS
23164
- });
23165
- if (!reservation.allowed) {
23166
- const logFields = {
23167
- lan_ips: routes.lanIps,
23168
- public_ipv4s: routes.publicIpv4s,
23169
- public_ipv6s: routes.publicIpv6s,
23170
- reason: reservation.reason
23171
- };
23172
- void options.logger.debug("lan_ip_report_skipped", logFields);
23173
- return;
23174
- }
23175
- try {
23176
- const result = await reportLinkStatusToServer({
23177
- paths: options.paths,
23178
- fetchImpl: options.fetchImpl,
23179
- routes
23180
- });
23181
- if (result) {
23182
- options.onNetworkRoutes?.(routes);
23183
- void options.logger.info("lan_ip_change_reported", {
23184
- link_id: result.linkId,
23185
- lan_ips: routes.lanIps,
23186
- public_ipv4s: routes.publicIpv4s,
23187
- public_ipv6s: routes.publicIpv6s
23188
- });
23189
- }
23190
- } catch (error) {
23191
- void options.logger.warn("lan_ip_change_report_failed", {
23192
- lan_ips: routes.lanIps,
23193
- error: error instanceof Error ? error.message : String(error)
23194
- });
23195
- }
23196
- }
23197
-
23198
- // src/daemon/scheduler.ts
23199
- function startCronDeliveryScheduler(options) {
23200
- let running = false;
23201
- let current = Promise.resolve();
23202
- const syncCronDeliveries = async () => {
23203
- if (running) {
23204
- return;
23205
- }
23206
- running = true;
23207
- try {
23208
- await syncHermesLinkCronDeliveries(
23209
- options.paths,
23210
- options.conversations,
23211
- options.logger
23212
- );
23213
- } catch (error) {
23214
- void options.logger.warn("cron_link_delivery_sync_failed", {
23215
- source: "daemon_scheduler",
23216
- error: error instanceof Error ? error.message : String(error)
23217
- });
23218
- } finally {
23219
- running = false;
23220
- }
23221
- };
23222
- const timer = setInterval(() => {
23223
- current = syncCronDeliveries();
23224
- }, options.intervalMs ?? 3e4);
23225
- timer.unref?.();
23226
- return {
23227
- async close() {
23228
- clearInterval(timer);
23229
- await current.catch(() => void 0);
23230
- }
23231
- };
23232
- }
23233
- function startHermesSessionSyncScheduler(options) {
23234
- let running = false;
23235
- let current = Promise.resolve();
23236
- const syncSessions = async () => {
23237
- if (running) {
23238
- return;
23239
- }
23240
- running = true;
23241
- try {
23242
- await options.conversations.syncHermesSessions();
23243
- } catch (error) {
23244
- void options.logger.warn("hermes_session_sync_failed", {
23245
- source: "daemon_scheduler",
23246
- error: error instanceof Error ? error.message : String(error)
23247
- });
23248
- } finally {
23249
- running = false;
23250
- }
23251
- };
23252
- const timer = setInterval(() => {
23253
- current = syncSessions();
23254
- }, options.intervalMs ?? 10 * 60 * 1e3);
23255
- timer.unref?.();
23256
- return {
23257
- async close() {
23258
- clearInterval(timer);
23259
- await current.catch(() => void 0);
23260
- }
23261
- };
23262
- }
23263
-
23264
- // src/daemon/service.ts
23265
- var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
23266
- var RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS = 15 * 6e4;
23267
- async function startLinkService(options = {}) {
23268
- const paths = options.paths ?? resolveRuntimePaths();
23269
- const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
23270
- const logger = createFileLogger({ paths, minLevel: config.logLevel });
23271
- await logger.info("service_starting", {
23272
- port: config.port,
23273
- mode: identity?.link_id ? "paired" : "local-only"
23067
+ child.stdout?.on("data", (chunk) => {
23068
+ void appendChunk(chunk);
23274
23069
  });
23275
- const migration = await migrateLinkDatabase(paths);
23276
- if (migration.appliedVersions.length > 0) {
23277
- await logger.info("database_migrated", {
23278
- database_file: migration.databaseFile,
23279
- applied_versions: migration.appliedVersions,
23280
- current_version: migration.currentVersion
23281
- });
23282
- }
23283
- const conversations = new ConversationService(paths, logger);
23284
- await conversations.rebuildStatisticsIndex();
23285
- let hermesSessionSync = Promise.resolve();
23286
- const triggerHermesSessionSync = () => {
23287
- hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
23288
- void logger.warn("hermes_session_sync_failed", {
23289
- source: "service_startup",
23290
- error: error instanceof Error ? error.message : String(error)
23291
- });
23292
- });
23293
- };
23294
- const app = await createApp({
23295
- paths,
23296
- logger,
23297
- conversations,
23298
- onPairingClaimed: async () => {
23299
- triggerHermesSessionSync();
23300
- await options.onPairingClaimed?.();
23301
- }
23070
+ child.stderr?.on("data", (chunk) => {
23071
+ void appendChunk(chunk);
23302
23072
  });
23303
- const server = createServer(app.callback());
23304
- try {
23305
- await listenServer(server, config.port);
23306
- } catch (error) {
23307
- await logger.error("service_start_failed", {
23308
- port: config.port,
23309
- link_id: identity?.link_id ?? null,
23310
- error: error instanceof Error ? error.message : String(error)
23073
+ runningUpdate2 = new Promise((resolve) => {
23074
+ child.on("error", (error) => {
23075
+ void (async () => {
23076
+ const failed = {
23077
+ ...started,
23078
+ state: "failed",
23079
+ finished_at: now().toISOString(),
23080
+ error: error.message
23081
+ };
23082
+ await writer.write(
23083
+ `
23084
+ [failed] link update failed to start: ${error.message}
23085
+ `
23086
+ );
23087
+ await writeUpdateState2(options.paths, failed);
23088
+ await emitUpdateStatus2(options.paths);
23089
+ void options.logger?.error("link_update_spawn_failed", {
23090
+ job_id: jobId,
23091
+ target_version: targetVersion,
23092
+ error: error.message
23093
+ });
23094
+ resolve(await readLinkUpdateStatus(options.paths));
23095
+ })();
23311
23096
  });
23312
- await logger.flush();
23313
- throw error;
23314
- }
23315
- server.on("error", (error) => {
23316
- void logger.error("service_error", {
23317
- port: config.port,
23318
- link_id: identity?.link_id ?? null,
23319
- error: error.message
23097
+ child.on("close", (code, signal) => {
23098
+ void (async () => {
23099
+ const succeeded = code === 0;
23100
+ const state = {
23101
+ ...started,
23102
+ state: succeeded ? "restart_required" : "failed",
23103
+ finished_at: now().toISOString(),
23104
+ exit_code: code,
23105
+ signal,
23106
+ error: succeeded ? null : `install script exited with code ${code ?? "unknown"}`
23107
+ };
23108
+ await writer.write(
23109
+ `
23110
+ === link update finished ${state.finished_at} exit=${code ?? "null"} signal=${signal ?? "null"} ===
23111
+ `
23112
+ );
23113
+ if (succeeded) {
23114
+ await writer.write(
23115
+ `
23116
+ [restart-requested] The install script should restart Hermes Link and verify the running version. If it does not reconnect, run \`${LINK_COMMAND} restart\` on this computer.
23117
+ `
23118
+ );
23119
+ }
23120
+ if (succeeded) {
23121
+ await writer.flush();
23122
+ setTimeout(() => {
23123
+ void (async () => {
23124
+ await writeUpdateState2(options.paths, state);
23125
+ await emitUpdateStatus2(options.paths);
23126
+ })();
23127
+ }, 1e3).unref();
23128
+ } else {
23129
+ await writeUpdateState2(options.paths, state);
23130
+ await emitUpdateStatus2(options.paths);
23131
+ }
23132
+ void options.logger?.info(
23133
+ succeeded ? "link_update_restart_required" : "link_update_failed",
23134
+ {
23135
+ job_id: jobId,
23136
+ target_version: targetVersion,
23137
+ exit_code: code,
23138
+ signal: signal ?? null
23139
+ }
23140
+ );
23141
+ resolve(await readLinkUpdateStatus(options.paths));
23142
+ })();
23320
23143
  });
23144
+ }).finally(() => {
23145
+ runningUpdate2 = null;
23321
23146
  });
23322
- void logger.info("service_started", {
23323
- port: config.port,
23324
- link_id: identity?.link_id ?? null
23325
- });
23326
- triggerHermesSessionSync();
23327
- const scheduler = startCronDeliveryScheduler({
23328
- paths,
23329
- conversations,
23330
- logger
23331
- });
23332
- const hermesSessionSyncScheduler = startHermesSessionSyncScheduler({
23333
- conversations,
23334
- logger
23147
+ await emitUpdateStatus2(options.paths);
23148
+ void options.logger?.info("link_update_started", {
23149
+ job_id: jobId,
23150
+ pid: child.pid ?? null,
23151
+ target_version: targetVersion,
23152
+ log_path: writer.filePath
23335
23153
  });
23336
- let relay = null;
23337
- let lanIpMonitor = null;
23338
- let hasSeenRelayConnected = false;
23339
- let lastRelayReconnectPublicRouteRefreshAt = 0;
23340
- if (identity?.link_id) {
23341
- let resolveRelayReady = null;
23342
- const relayReady = new Promise((resolve) => {
23343
- resolveRelayReady = resolve;
23344
- });
23345
- relay = connectRelayControl({
23346
- relayBaseUrl: config.relayBaseUrl,
23347
- linkId: identity.link_id,
23348
- localPort: config.port,
23349
- maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
23350
- backoffBaseMs: 1e3,
23351
- backoffMaxMs: 3e4,
23352
- onStatus: (status) => {
23353
- void logger.info("relay_status", status);
23354
- if (status.state === "connected") {
23355
- const now = Date.now();
23356
- if (hasSeenRelayConnected && lanIpMonitor && now - lastRelayReconnectPublicRouteRefreshAt >= RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS) {
23357
- lastRelayReconnectPublicRouteRefreshAt = now;
23358
- void lanIpMonitor.refreshPublicRoutes();
23359
- }
23360
- hasSeenRelayConnected = true;
23361
- resolveRelayReady?.(true);
23362
- resolveRelayReady = null;
23363
- } else if (status.state === "failed") {
23364
- resolveRelayReady?.(false);
23365
- resolveRelayReady = null;
23366
- }
23367
- }
23368
- });
23369
- if (options.waitForRelayReady) {
23370
- await Promise.race([
23371
- relayReady,
23372
- waitForRelayReadyTimeout(options.relayReadyTimeoutMs)
23373
- ]);
23374
- resolveRelayReady = null;
23154
+ return readLinkUpdateStatus(options.paths);
23155
+ }
23156
+ async function readLinkUpdateStatus(paths) {
23157
+ let state = await readJsonFile(updateStatePath2(paths));
23158
+ if (state?.state === "restart_required" && state.target_version) {
23159
+ if (compareSemver3(LINK_VERSION, state.target_version) >= 0) {
23160
+ state = {
23161
+ ...state,
23162
+ state: "succeeded",
23163
+ finished_at: state.finished_at ?? (/* @__PURE__ */ new Date()).toISOString()
23164
+ };
23165
+ await writeUpdateState2(paths, state);
23375
23166
  }
23376
- } else {
23377
- void logger.info("relay_skipped", { reason: "link_not_paired" });
23378
23167
  }
23379
- lanIpMonitor = startLanIpMonitor({
23380
- paths,
23381
- logger,
23382
- intervalMs: options.lanIpMonitorIntervalMs,
23383
- dailyReportLimit: options.lanIpMonitorDailyReportLimit,
23384
- fetchImpl: options.lanIpMonitorFetchImpl,
23385
- onNetworkRoutes: (routes) => {
23386
- relay?.publishNetworkRoutes(routes);
23387
- }
23388
- });
23389
- if (options.writePidFile) {
23390
- await writePidFile(paths);
23168
+ if (state?.state === "running" && !runningUpdate2 && !isRecentRunningState3(state) && !isProcessAlive3(state.pid)) {
23169
+ const reachedTarget = state.target_version && compareSemver3(LINK_VERSION, state.target_version) >= 0;
23170
+ state = reachedTarget ? {
23171
+ ...state,
23172
+ state: "succeeded",
23173
+ finished_at: (/* @__PURE__ */ new Date()).toISOString(),
23174
+ error: null
23175
+ } : {
23176
+ ...state,
23177
+ state: "failed",
23178
+ finished_at: (/* @__PURE__ */ new Date()).toISOString(),
23179
+ error: state.error ?? "Link update was interrupted before Hermes Link could observe completion."
23180
+ };
23181
+ await writeUpdateState2(paths, state);
23391
23182
  }
23392
23183
  return {
23393
- async close() {
23394
- relay?.close();
23395
- await closeServer(server);
23396
- await Promise.all([
23397
- scheduler.close(),
23398
- hermesSessionSyncScheduler.close(),
23399
- lanIpMonitor?.close(),
23400
- hermesSessionSync.catch(() => void 0)
23401
- ]);
23402
- await logger.info("service_stopped");
23403
- await logger.flush();
23404
- if (options.writePidFile) {
23405
- await rm8(pidFilePath(paths), { force: true }).catch(() => void 0);
23406
- }
23407
- }
23184
+ ok: true,
23185
+ state: state?.state ?? "idle",
23186
+ job_id: state?.job_id ?? null,
23187
+ pid: state?.pid ?? null,
23188
+ target_version: state?.target_version ?? null,
23189
+ started_at: state?.started_at ?? null,
23190
+ finished_at: state?.finished_at ?? null,
23191
+ exit_code: state?.exit_code ?? null,
23192
+ signal: state?.signal ?? null,
23193
+ log_path: updateLogPath2(paths),
23194
+ lines: await readUpdateLogLines2(paths),
23195
+ error: state?.error ?? null,
23196
+ manual_command: state?.manual_command ?? null
23408
23197
  };
23409
23198
  }
23410
- function waitForRelayReadyTimeout(timeoutMs) {
23411
- return new Promise((resolve) => {
23412
- const timer = setTimeout(
23413
- () => resolve(false),
23414
- timeoutMs ?? DEFAULT_RELAY_READY_TIMEOUT_MS
23415
- );
23416
- timer.unref?.();
23417
- });
23418
- }
23419
- function pidFilePath(paths = resolveRuntimePaths()) {
23420
- return `${paths.runDir}/hermeslink.pid`;
23199
+ function subscribeLinkUpdateStatus(listener) {
23200
+ updateEvents2.on("status", listener);
23201
+ return () => updateEvents2.off("status", listener);
23421
23202
  }
23422
- async function writePidFile(paths) {
23423
- await mkdir13(paths.runDir, { recursive: true, mode: 448 });
23424
- await writeFile3(pidFilePath(paths), `${process.pid}
23425
- `, { mode: 384 });
23203
+ async function writeFailedStartState(options, error, targetVersion = null) {
23204
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
23205
+ const state = {
23206
+ state: "failed",
23207
+ job_id: `link_update_${now().getTime().toString(36)}`,
23208
+ pid: null,
23209
+ target_version: targetVersion,
23210
+ started_at: now().toISOString(),
23211
+ finished_at: now().toISOString(),
23212
+ exit_code: null,
23213
+ signal: null,
23214
+ error,
23215
+ manual_command: targetVersion ? manualInstallCommand(targetVersion) : null
23216
+ };
23217
+ await writeUpdateState2(options.paths, state);
23218
+ await emitUpdateStatus2(options.paths);
23219
+ return readLinkUpdateStatus(options.paths);
23426
23220
  }
23427
- async function closeServer(server) {
23428
- await new Promise((resolve, reject) => {
23429
- let settled = false;
23430
- let forceCloseTimer;
23431
- let timeoutTimer;
23432
- const settle = (error) => {
23433
- if (settled) {
23434
- return;
23435
- }
23436
- settled = true;
23437
- clearTimeout(forceCloseTimer);
23438
- clearTimeout(timeoutTimer);
23439
- if (error) {
23440
- reject(error);
23441
- return;
23442
- }
23443
- resolve();
23444
- };
23445
- forceCloseTimer = setTimeout(() => {
23446
- server.closeIdleConnections?.();
23447
- server.closeAllConnections?.();
23448
- }, 250);
23449
- timeoutTimer = setTimeout(() => {
23450
- server.closeAllConnections?.();
23451
- settle();
23452
- }, 5e3);
23453
- server.close((error) => {
23454
- if (error) {
23455
- settle(error);
23456
- return;
23457
- }
23458
- settle();
23221
+ async function readRemoteLinkPolicy(options) {
23222
+ const context = await readLinkReleaseCheckContext(options.paths).catch(
23223
+ () => null
23224
+ );
23225
+ try {
23226
+ const response = await fetchCurrentLinkReleaseFromServer(
23227
+ options,
23228
+ options.fetchImpl ?? fetch
23229
+ );
23230
+ if (!response.ok) {
23231
+ throw new Error(`HermesPilot Server returned HTTP ${response.status}`);
23232
+ }
23233
+ const snapshot = normalizeServerSnapshot(await response.json());
23234
+ if (!snapshot.remote) {
23235
+ return {
23236
+ remote: null,
23237
+ state: "unavailable",
23238
+ issue: snapshot.issue ?? "HermesPilot Server has no Link release policy"
23239
+ };
23240
+ }
23241
+ return {
23242
+ remote: snapshot.remote,
23243
+ state: "fresh",
23244
+ issue: snapshot.issue
23245
+ };
23246
+ } catch (error) {
23247
+ const issue = error instanceof Error ? error.message : String(error);
23248
+ void options.logger?.warn("link_release_server_check_failed", {
23249
+ server_base_url: context?.serverBaseUrl ?? null,
23250
+ release_check_url: context?.releaseCheckUrl ?? null,
23251
+ error: issue
23459
23252
  });
23460
- server.closeIdleConnections?.();
23461
- });
23253
+ return { remote: null, state: "unavailable", issue };
23254
+ }
23462
23255
  }
23463
- async function listenServer(server, port) {
23464
- await new Promise((resolve, reject) => {
23465
- const cleanup = () => {
23466
- server.off("error", onError);
23467
- server.off("listening", onListening);
23468
- };
23469
- const onError = (error) => {
23470
- cleanup();
23471
- reject(error);
23256
+ function normalizeServerSnapshot(payload) {
23257
+ const snapshot = toRecord17(payload);
23258
+ const policy = toNullableRecord2(snapshot.policy);
23259
+ if (!policy) {
23260
+ return {
23261
+ remote: null,
23262
+ issue: readString18(snapshot, "issue")
23472
23263
  };
23473
- const onListening = () => {
23474
- cleanup();
23475
- resolve();
23264
+ }
23265
+ const release = toNullableRecord2(snapshot.release);
23266
+ const currentVersion = readString18(policy, "current_version") ?? readString18(policy, "currentVersion");
23267
+ const minSafeVersion = readString18(policy, "min_safe_version") ?? readString18(policy, "minSafeVersion");
23268
+ if (!currentVersion) {
23269
+ return {
23270
+ remote: null,
23271
+ issue: readString18(snapshot, "issue")
23476
23272
  };
23477
- server.once("error", onError);
23478
- server.once("listening", onListening);
23479
- server.listen(port);
23480
- });
23273
+ }
23274
+ return {
23275
+ remote: {
23276
+ current_version: currentVersion,
23277
+ min_safe_version: minSafeVersion,
23278
+ target_version: currentVersion,
23279
+ release_url: release ? readString18(release, "release_url") ?? readString18(release, "releaseUrl") : null,
23280
+ published_at: release ? readString18(release, "published_at") ?? readString18(release, "publishedAt") : null
23281
+ },
23282
+ issue: readString18(snapshot, "issue")
23283
+ };
23481
23284
  }
23482
-
23483
- // src/daemon/process.ts
23484
- async function startDaemonProcess(paths = resolveRuntimePaths()) {
23485
- const config = await loadConfig(paths);
23486
- let status = await getDaemonStatus(paths);
23487
- if (status.running) {
23488
- const probe = await probeLocalLinkService({ port: config.port, timeoutMs: 500 });
23489
- if (probe.reachable) {
23490
- return status;
23491
- }
23492
- await stopDaemonProcess(paths);
23493
- status = await getDaemonStatus(paths);
23494
- if (status.running) {
23495
- return status;
23496
- }
23497
- }
23498
- await mkdir14(paths.logsDir, { recursive: true, mode: 448 });
23499
- await mkdir14(paths.runDir, { recursive: true, mode: 448 });
23500
- const scriptPath = currentCliScriptPath();
23501
- const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
23502
- detached: true,
23503
- stdio: "ignore",
23504
- env: process.env
23505
- });
23506
- child.unref();
23507
- for (let index = 0; index < 12; index += 1) {
23508
- await wait(250);
23509
- const next = await getDaemonStatus(paths);
23510
- if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
23511
- return next;
23285
+ async function fetchCurrentLinkReleaseFromServer(options, fetcher) {
23286
+ const config = await loadConfig(options.paths);
23287
+ const url = new URL(SERVER_LINK_CURRENT_RELEASE_PATH, config.serverBaseUrl);
23288
+ url.searchParams.set("channel", "stable");
23289
+ url.searchParams.set("lang", "en");
23290
+ const controller = new AbortController();
23291
+ const timer = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
23292
+ try {
23293
+ return await fetcher(url, {
23294
+ headers: {
23295
+ accept: "application/json",
23296
+ "user-agent": `HermesPilot-Link/${LINK_VERSION}`
23297
+ },
23298
+ signal: controller.signal
23299
+ });
23300
+ } catch (error) {
23301
+ if (error instanceof Error && error.name === "AbortError") {
23302
+ throw new Error("HermesPilot Server Link release check timed out");
23512
23303
  }
23304
+ throw error;
23305
+ } finally {
23306
+ clearTimeout(timer);
23513
23307
  }
23514
- return await getDaemonStatus(paths);
23515
23308
  }
23516
- async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
23517
- await mkdir14(paths.logsDir, { recursive: true, mode: 448 });
23518
- const log = createRotatingTextLogWriter({
23519
- paths,
23520
- fileName: path24.basename(daemonLogFile(paths))
23521
- });
23522
- const scriptPath = currentCliScriptPath();
23523
- const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
23524
- stdio: ["ignore", "pipe", "pipe"],
23525
- env: process.env
23526
- });
23527
- const write = (chunk) => {
23528
- void log.write(chunk);
23309
+ function buildOfficialInstallCommand(targetVersion) {
23310
+ const env = {
23311
+ HERMESLINK_VERSION: targetVersion,
23312
+ HERMESLINK_YES: "1",
23313
+ HERMESLINK_REQUIRE_RESTART_VERIFY: "1"
23529
23314
  };
23530
- write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor started
23531
- `);
23532
- child.stdout?.on("data", write);
23533
- child.stderr?.on("data", write);
23534
- const forwardStop = () => {
23535
- if (child.pid && isProcessAlive3(child.pid)) {
23536
- child.kill("SIGTERM");
23537
- }
23315
+ if (process.platform === "win32") {
23316
+ return {
23317
+ command: `Invoke-RestMethod ${OFFICIAL_WINDOWS_INSTALLER_URL} | Invoke-Expression`,
23318
+ displayCommand: `$env:HERMESLINK_VERSION="${targetVersion}"; $env:HERMESLINK_YES="1"; $env:HERMESLINK_REQUIRE_RESTART_VERIFY="1"; Invoke-RestMethod ${OFFICIAL_WINDOWS_INSTALLER_URL} | Invoke-Expression`,
23319
+ env,
23320
+ source: "official-installer"
23321
+ };
23322
+ }
23323
+ return {
23324
+ command: `curl -fsSL ${OFFICIAL_UNIX_INSTALLER_URL} | bash`,
23325
+ displayCommand: `HERMESLINK_VERSION=${targetVersion} HERMESLINK_YES=1 HERMESLINK_REQUIRE_RESTART_VERIFY=1 sh -c 'curl -fsSL ${OFFICIAL_UNIX_INSTALLER_URL} | bash'`,
23326
+ env,
23327
+ source: "official-installer"
23538
23328
  };
23539
- process.once("SIGINT", forwardStop);
23540
- process.once("SIGTERM", forwardStop);
23541
- const result = await new Promise((resolve, reject) => {
23542
- child.once("error", reject);
23543
- child.once("exit", (code, signal) => resolve({ code, signal }));
23544
- }).catch((error) => {
23545
- write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor failed: ${error instanceof Error ? error.message : String(error)}
23546
- `);
23547
- return { code: 1, signal: null };
23548
- });
23549
- process.off("SIGINT", forwardStop);
23550
- process.off("SIGTERM", forwardStop);
23551
- write(
23552
- `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${result.code ?? "null"} signal=${result.signal ?? "null"}
23553
- `
23554
- );
23555
- await log.flush();
23556
- return result.code ?? (result.signal ? 0 : 1);
23557
- }
23558
- async function probeLocalLinkService(options) {
23559
- const unreachable = {
23560
- reachable: false,
23561
- reusable: false,
23562
- linkId: null,
23563
- version: null
23329
+ }
23330
+ function spawnInstallCommand(input) {
23331
+ const env = {
23332
+ ...process.env,
23333
+ ...input.env
23564
23334
  };
23565
- let response;
23566
- try {
23567
- response = await fetch(`http://127.0.0.1:${options.port}/api/v1/bootstrap`, {
23568
- headers: { accept: "application/json" },
23569
- signal: AbortSignal.timeout(options.timeoutMs ?? 1e3)
23570
- });
23571
- } catch {
23572
- return unreachable;
23573
- }
23574
- if (!response.ok) {
23575
- return unreachable;
23576
- }
23577
- const payload = await response.json().catch(() => null);
23578
- if (!payload || payload.api_version !== 1) {
23579
- return unreachable;
23335
+ if (process.platform === "win32") {
23336
+ return spawn4(
23337
+ "powershell.exe",
23338
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", input.command],
23339
+ {
23340
+ env,
23341
+ stdio: ["ignore", "pipe", "pipe"],
23342
+ windowsHide: true,
23343
+ detached: false,
23344
+ shell: false
23345
+ }
23346
+ );
23580
23347
  }
23581
- const linkId = typeof payload.link_id === "string" ? payload.link_id : null;
23348
+ return spawn4("/bin/sh", ["-lc", input.command], {
23349
+ env,
23350
+ stdio: ["ignore", "pipe", "pipe"],
23351
+ windowsHide: true,
23352
+ detached: false,
23353
+ shell: false
23354
+ });
23355
+ }
23356
+ async function readLinkReleaseCheckContext(paths) {
23357
+ const config = await loadConfig(paths);
23358
+ const url = new URL(SERVER_LINK_CURRENT_RELEASE_PATH, config.serverBaseUrl);
23359
+ url.searchParams.set("channel", "stable");
23360
+ url.searchParams.set("lang", "en");
23582
23361
  return {
23583
- reachable: true,
23584
- reusable: options.linkId ? linkId === options.linkId : true,
23585
- linkId,
23586
- version: typeof payload.version === "string" ? payload.version : null
23362
+ serverBaseUrl: config.serverBaseUrl,
23363
+ releaseCheckUrl: url.toString()
23587
23364
  };
23588
23365
  }
23589
- async function stopDaemonProcess(paths = resolveRuntimePaths()) {
23590
- const status = await getDaemonStatus(paths);
23591
- if (!status.running || !status.pid) {
23592
- return status;
23593
- }
23594
- try {
23595
- process.kill(status.pid, "SIGTERM");
23596
- } catch {
23597
- await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
23598
- return await getDaemonStatus(paths);
23366
+ function computeLinkUpdateState(localVersion, remote) {
23367
+ if (!remote?.current_version) {
23368
+ return "unknown";
23599
23369
  }
23600
- for (let index = 0; index < 20; index += 1) {
23601
- await wait(250);
23602
- if (!isProcessAlive3(status.pid)) {
23603
- break;
23604
- }
23370
+ if (remote.min_safe_version && compareSemver3(localVersion, remote.min_safe_version) < 0) {
23371
+ return "unsafe";
23605
23372
  }
23606
- if (isProcessAlive3(status.pid)) {
23607
- try {
23608
- process.kill(status.pid, "SIGKILL");
23609
- } catch {
23610
- }
23611
- for (let index = 0; index < 10; index += 1) {
23612
- await wait(250);
23613
- if (!isProcessAlive3(status.pid)) {
23614
- break;
23615
- }
23616
- }
23373
+ const diff = compareSemver3(localVersion, remote.current_version);
23374
+ if (diff < 0) {
23375
+ return "update_available";
23617
23376
  }
23618
- if (!isProcessAlive3(status.pid) || !await pidBackedServiceIsReachable(paths)) {
23619
- await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
23377
+ if (diff > 0) {
23378
+ return "ahead_of_current";
23620
23379
  }
23621
- return await getDaemonStatus(paths);
23380
+ return "current";
23622
23381
  }
23623
- async function getDaemonStatus(paths = resolveRuntimePaths()) {
23624
- const pidFile = pidFilePath(paths);
23625
- const pid = await readPid(pidFile);
23626
- if (pid && !isProcessAlive3(pid)) {
23627
- await rm9(pidFile, { force: true }).catch(() => void 0);
23628
- return {
23629
- running: false,
23630
- pid: null,
23631
- pidFile,
23632
- logFile: daemonLogFile(paths)
23633
- };
23382
+ async function emitUpdateStatus2(paths) {
23383
+ updateEvents2.emit("status", await readLinkUpdateStatus(paths));
23384
+ }
23385
+ async function writeUpdateState2(paths, state) {
23386
+ await writeJsonFile(updateStatePath2(paths), state);
23387
+ }
23388
+ async function readUpdateLogLines2(paths) {
23389
+ const raw = await readFile17(updateLogPath2(paths), "utf8").catch(() => "");
23390
+ if (!raw.trim()) {
23391
+ return [];
23634
23392
  }
23635
- return {
23636
- running: Boolean(pid),
23637
- pid,
23638
- pidFile,
23639
- logFile: daemonLogFile(paths)
23640
- };
23393
+ return raw.split(/\r?\n/u).map((line) => line.trimEnd()).filter(Boolean).slice(-MAX_UPDATE_LOG_LINES2).map(
23394
+ (line) => line.length > MAX_OUTPUT_LINE_LENGTH3 ? `${line.slice(0, MAX_OUTPUT_LINE_LENGTH3)}...` : line
23395
+ );
23641
23396
  }
23642
- function daemonLogFile(paths = resolveRuntimePaths()) {
23643
- return getDaemonLogFile(paths);
23397
+ function updateStatePath2(paths) {
23398
+ return path24.join(paths.runDir, "link-update-state.json");
23644
23399
  }
23645
- function currentCliScriptPath() {
23646
- return process.argv[1];
23400
+ function updateLogPath2(paths) {
23401
+ return path24.join(paths.logsDir, UPDATE_LOG_FILE2);
23647
23402
  }
23648
- async function readPid(filePath) {
23649
- const raw = await readFile17(filePath, "utf8").catch(() => null);
23650
- if (!raw) {
23651
- return null;
23652
- }
23653
- const pid = Number.parseInt(raw.trim(), 10);
23654
- return Number.isInteger(pid) && pid > 0 ? pid : null;
23403
+ async function clearUpdateLogFiles2(paths) {
23404
+ const primary = updateLogPath2(paths);
23405
+ await Promise.all([
23406
+ rm8(primary, { force: true }).catch(() => void 0),
23407
+ ...Array.from(
23408
+ { length: UPDATE_LOG_MAX_FILES2 },
23409
+ (_, index) => rm8(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
23410
+ )
23411
+ ]);
23655
23412
  }
23656
- function isProcessAlive3(pid) {
23657
- try {
23658
- process.kill(pid, 0);
23659
- return true;
23660
- } catch {
23661
- return false;
23662
- }
23413
+ function manualInstallCommand(version) {
23414
+ return `npm install -g ${LINK_NPM_PACKAGE}@${version}`;
23663
23415
  }
23664
- async function pidBackedServiceIsReachable(paths) {
23665
- const config = await loadConfig(paths).catch(() => null);
23666
- if (!config) {
23667
- return false;
23416
+ function isValidReleaseVersion(version) {
23417
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(
23418
+ version
23419
+ );
23420
+ }
23421
+ function compareSemver3(left, right) {
23422
+ const leftParts = parseSemver(left);
23423
+ const rightParts = parseSemver(right);
23424
+ for (let index = 0; index < 3; index += 1) {
23425
+ const diff = leftParts[index] - rightParts[index];
23426
+ if (diff !== 0) {
23427
+ return diff;
23428
+ }
23668
23429
  }
23669
- return (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable;
23430
+ return 0;
23670
23431
  }
23671
- function wait(ms) {
23672
- return new Promise((resolve) => setTimeout(resolve, ms));
23432
+ function parseSemver(value) {
23433
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/u.exec(value.trim());
23434
+ return [
23435
+ Number.parseInt(match?.[1] ?? "0", 10),
23436
+ Number.parseInt(match?.[2] ?? "0", 10),
23437
+ Number.parseInt(match?.[3] ?? "0", 10)
23438
+ ];
23673
23439
  }
23674
-
23675
- // src/link/updates.ts
23676
- var SERVER_LINK_CURRENT_RELEASE_PATH = "/api/v1/link/releases/current";
23677
- var LINK_NPM_PACKAGE = "@hermespilot/link";
23678
- var UPDATE_LOG_FILE2 = "link-update.log";
23679
- var UPDATE_LOG_MAX_FILES2 = 3;
23680
- var UPDATE_FETCH_TIMEOUT_MS = 5e3;
23681
- var MAX_UPDATE_LOG_LINES2 = 240;
23682
- var MAX_OUTPUT_LINE_LENGTH3 = 1200;
23683
- var AUTO_RESTART_DELAY_MS = 1500;
23684
- var updateEvents2 = new EventEmitter4();
23685
- var runningUpdate2 = null;
23686
- async function readLinkUpdateCheck(options) {
23687
- const remoteResult = await readRemoteLinkPolicy(options);
23688
- const remote = remoteResult.remote;
23689
- const state = computeLinkUpdateState(LINK_VERSION, remote);
23690
- const targetVersion = remote?.target_version ?? null;
23691
- return {
23692
- ok: true,
23693
- local: {
23694
- version: LINK_VERSION,
23695
- raw: LINK_VERSION
23696
- },
23697
- remote,
23698
- state,
23699
- update_available: state === "update_available" || state === "unsafe" || state === "blocked",
23700
- unsafe: state === "unsafe",
23701
- blocked: state === "blocked",
23702
- check_state: remoteResult.state,
23703
- issue: remoteResult.issue,
23704
- manual: {
23705
- command: targetVersion ? manualInstallCommand(targetVersion) : null,
23706
- package: LINK_NPM_PACKAGE,
23707
- version: targetVersion
23708
- }
23709
- };
23440
+ function isRecentRunningState3(state, now = Date.now()) {
23441
+ const startedAt = state.started_at ? Date.parse(state.started_at) : Number.NaN;
23442
+ return Number.isFinite(startedAt) && now - startedAt < 1e4;
23710
23443
  }
23711
- async function startLinkUpdate(options) {
23712
- const current = await readLinkUpdateStatus(options.paths);
23713
- if (runningUpdate2 || current.state === "running") {
23714
- return current;
23715
- }
23716
- const check = await readLinkUpdateCheck(options);
23717
- const targetVersion = check.remote?.target_version ?? null;
23718
- if (!targetVersion) {
23719
- return writeFailedStartState(
23720
- options,
23721
- "HermesPilot Server has no Link target version."
23722
- );
23444
+ function isProcessAlive3(pid) {
23445
+ if (!pid || pid <= 0) {
23446
+ return false;
23723
23447
  }
23724
- if (options.targetVersion && options.targetVersion !== targetVersion) {
23725
- return writeFailedStartState(
23726
- options,
23727
- `Requested target ${options.targetVersion} does not match current Link target ${targetVersion}.`,
23728
- targetVersion
23729
- );
23448
+ try {
23449
+ process.kill(pid, 0);
23450
+ return true;
23451
+ } catch {
23452
+ return false;
23730
23453
  }
23731
- if (check.state === "current") {
23732
- return writeFailedStartState(
23733
- options,
23734
- "Hermes Link is already on the current version.",
23735
- targetVersion
23454
+ }
23455
+ function toRecord17(value) {
23456
+ return typeof value === "object" && value !== null ? value : {};
23457
+ }
23458
+ function toNullableRecord2(value) {
23459
+ return typeof value === "object" && value !== null ? value : null;
23460
+ }
23461
+ function readString18(payload, key) {
23462
+ const value = payload[key];
23463
+ return typeof value === "string" && value.trim() ? value.trim() : null;
23464
+ }
23465
+
23466
+ // src/pairing/pairing.ts
23467
+ import path25 from "path";
23468
+ import { rm as rm9 } from "fs/promises";
23469
+
23470
+ // src/relay/bootstrap.ts
23471
+ var RelayNetworkError = class extends Error {
23472
+ constructor(relayBaseUrl, causeMessage) {
23473
+ super(
23474
+ `Cannot reach Hermes Relay at ${relayBaseUrl}. Please check your network connection, VPN, or proxy settings, then try again.`
23736
23475
  );
23476
+ this.relayBaseUrl = relayBaseUrl;
23477
+ this.causeMessage = causeMessage;
23737
23478
  }
23738
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
23739
- const jobId = `link_update_${now().getTime().toString(36)}`;
23740
- await clearUpdateLogFiles2(options.paths);
23741
- const writer = createRotatingTextLogWriter({
23742
- paths: options.paths,
23743
- fileName: UPDATE_LOG_FILE2,
23744
- maxFileBytes: 512 * 1024,
23745
- maxFiles: UPDATE_LOG_MAX_FILES2
23746
- });
23747
- const startedAt = now().toISOString();
23748
- const manualCommand = manualInstallCommand(targetVersion);
23749
- const started = {
23750
- state: "running",
23751
- job_id: jobId,
23752
- pid: null,
23753
- target_version: targetVersion,
23754
- started_at: startedAt,
23755
- finished_at: null,
23756
- exit_code: null,
23757
- signal: null,
23758
- error: null,
23759
- manual_command: manualCommand
23479
+ relayBaseUrl;
23480
+ causeMessage;
23481
+ };
23482
+ async function bootstrapRelayLink(options) {
23483
+ const fetcher = options.fetchImpl ?? fetch;
23484
+ const baseUrl = options.relayBaseUrl.replace(/\/+$/u, "");
23485
+ const commonPayload = {
23486
+ install_id: options.identity.install_id,
23487
+ link_id: options.identity.link_id ?? void 0,
23488
+ public_key_pem: options.identity.public_key_pem
23760
23489
  };
23761
- await mkdir15(options.paths.runDir, { recursive: true, mode: 448 });
23762
- await writer.write(
23763
- `
23764
- === link update started ${startedAt} target=${targetVersion} ===
23765
- `
23490
+ const challenge = await postJson(
23491
+ fetcher,
23492
+ `${baseUrl}/api/v1/relay/link/challenge`,
23493
+ options.relayBootstrapToken,
23494
+ commonPayload
23766
23495
  );
23767
- await writer.write(`$ ${manualCommand}
23768
- `);
23769
- await writeUpdateState2(options.paths, started);
23770
- const child = spawn5(
23771
- resolveNpmBin(),
23772
- ["install", "-g", `${LINK_NPM_PACKAGE}@${targetVersion}`],
23496
+ if (challenge.ok !== true || typeof challenge.nonce !== "string") {
23497
+ throw new Error("Relay did not return a valid install challenge");
23498
+ }
23499
+ const proof = {
23500
+ nonce: challenge.nonce,
23501
+ signature: signRelayNonce(options.identity, challenge.nonce)
23502
+ };
23503
+ const assigned = await postJson(
23504
+ fetcher,
23505
+ `${baseUrl}/api/v1/relay/link/bootstrap`,
23506
+ options.relayBootstrapToken,
23773
23507
  {
23774
- stdio: ["ignore", "pipe", "pipe"],
23775
- windowsHide: true,
23776
- detached: false,
23777
- shell: false
23508
+ ...commonPayload,
23509
+ proof
23778
23510
  }
23779
23511
  );
23780
- started.pid = child.pid ?? null;
23781
- await writeUpdateState2(options.paths, started);
23782
- const appendChunk = async (chunk) => {
23783
- await writer.write(chunk);
23784
- await emitUpdateStatus2(options.paths);
23512
+ if (assigned.ok !== true || typeof assigned.link_id !== "string") {
23513
+ throw new Error("Relay did not return a valid link_id");
23514
+ }
23515
+ await saveAssignedLinkId(assigned.link_id, options.paths ?? resolveRuntimePaths());
23516
+ return {
23517
+ linkId: assigned.link_id,
23518
+ reused: assigned.reused === true
23785
23519
  };
23786
- child.stdout?.on("data", (chunk) => {
23787
- void appendChunk(chunk);
23788
- });
23789
- child.stderr?.on("data", (chunk) => {
23790
- void appendChunk(chunk);
23791
- });
23792
- runningUpdate2 = new Promise((resolve) => {
23793
- child.on("error", (error) => {
23794
- void (async () => {
23795
- const failed = {
23796
- ...started,
23797
- state: "failed",
23798
- finished_at: now().toISOString(),
23799
- error: error.message
23800
- };
23801
- await writer.write(
23802
- `
23803
- [failed] link update failed to start: ${error.message}
23804
- `
23805
- );
23806
- await writeUpdateState2(options.paths, failed);
23807
- await emitUpdateStatus2(options.paths);
23808
- void options.logger?.error("link_update_spawn_failed", {
23809
- job_id: jobId,
23810
- target_version: targetVersion,
23811
- error: error.message
23812
- });
23813
- resolve(await readLinkUpdateStatus(options.paths));
23814
- })();
23815
- });
23816
- child.on("close", (code, signal) => {
23817
- void (async () => {
23818
- const succeeded = code === 0;
23819
- const state = {
23820
- ...started,
23821
- state: succeeded ? "restart_required" : "failed",
23822
- finished_at: now().toISOString(),
23823
- exit_code: code,
23824
- signal,
23825
- error: succeeded ? null : `npm install exited with code ${code ?? "unknown"}`
23826
- };
23827
- await writer.write(
23828
- `
23829
- === link update finished ${state.finished_at} exit=${code ?? "null"} signal=${signal ?? "null"} ===
23830
- `
23831
- );
23832
- if (succeeded) {
23833
- await writer.write(
23834
- `
23835
- [restart-scheduled] Hermes Link will restart automatically. If it does not reconnect, run \`${LINK_COMMAND} restart\` on this computer.
23836
- `
23837
- );
23838
- }
23839
- await writeUpdateState2(options.paths, state);
23840
- await emitUpdateStatus2(options.paths);
23841
- if (succeeded) {
23842
- await writer.flush();
23843
- scheduleAutomaticRestart(options);
23844
- }
23845
- void options.logger?.info(
23846
- succeeded ? "link_update_restart_required" : "link_update_failed",
23847
- {
23848
- job_id: jobId,
23849
- target_version: targetVersion,
23850
- exit_code: code,
23851
- signal: signal ?? null
23852
- }
23853
- );
23854
- resolve(await readLinkUpdateStatus(options.paths));
23855
- })();
23856
- });
23857
- }).finally(() => {
23858
- runningUpdate2 = null;
23859
- });
23860
- await emitUpdateStatus2(options.paths);
23861
- void options.logger?.info("link_update_started", {
23862
- job_id: jobId,
23863
- pid: child.pid ?? null,
23864
- target_version: targetVersion,
23865
- log_path: writer.filePath
23866
- });
23867
- return readLinkUpdateStatus(options.paths);
23868
23520
  }
23869
- function scheduleAutomaticRestart(options) {
23870
- const scriptPath = currentCliScriptPath();
23871
- setTimeout(() => {
23872
- const child = spawn5(process.execPath, [scriptPath, "restart"], {
23873
- detached: true,
23874
- stdio: "ignore",
23875
- env: process.env,
23876
- windowsHide: true
23877
- });
23878
- child.unref();
23879
- void options.logger?.info("link_update_restart_scheduled", {
23880
- delay_ms: AUTO_RESTART_DELAY_MS,
23881
- command: `${LINK_COMMAND} restart`
23521
+ async function postJson(fetcher, url, token, body) {
23522
+ let response;
23523
+ try {
23524
+ response = await fetcher(url, {
23525
+ method: "POST",
23526
+ headers: {
23527
+ authorization: `Bearer ${token}`,
23528
+ "content-type": "application/json"
23529
+ },
23530
+ body: JSON.stringify(body)
23882
23531
  });
23883
- }, AUTO_RESTART_DELAY_MS).unref();
23532
+ } catch (error) {
23533
+ const baseUrl = new URL(url).origin;
23534
+ throw new RelayNetworkError(
23535
+ baseUrl,
23536
+ error instanceof Error ? error.message : String(error)
23537
+ );
23538
+ }
23539
+ const payload = await response.json().catch(() => null);
23540
+ if (!response.ok) {
23541
+ const message = readErrorMessage3(payload) ?? `Relay request failed with HTTP ${response.status}`;
23542
+ throw new Error(message);
23543
+ }
23544
+ if (!payload) {
23545
+ throw new Error("Relay returned an empty response");
23546
+ }
23547
+ return payload;
23884
23548
  }
23885
- async function readLinkUpdateStatus(paths) {
23886
- let state = await readJsonFile(updateStatePath2(paths));
23887
- if (state?.state === "restart_required" && state.target_version) {
23888
- if (compareSemver3(LINK_VERSION, state.target_version) >= 0) {
23889
- state = {
23890
- ...state,
23891
- state: "succeeded",
23892
- finished_at: state.finished_at ?? (/* @__PURE__ */ new Date()).toISOString()
23893
- };
23894
- await writeUpdateState2(paths, state);
23895
- }
23549
+ function readErrorMessage3(payload) {
23550
+ if (typeof payload !== "object" || payload === null) {
23551
+ return null;
23896
23552
  }
23897
- if (state?.state === "running" && !runningUpdate2 && !isRecentRunningState3(state) && !isProcessAlive4(state.pid)) {
23898
- state = {
23899
- ...state,
23900
- state: "failed",
23901
- finished_at: (/* @__PURE__ */ new Date()).toISOString(),
23902
- error: state.error ?? "Link update was interrupted before Hermes Link could observe completion."
23903
- };
23904
- await writeUpdateState2(paths, state);
23553
+ const error = payload.error;
23554
+ if (typeof error !== "object" || error === null) {
23555
+ return null;
23905
23556
  }
23906
- return {
23907
- ok: true,
23908
- state: state?.state ?? "idle",
23909
- job_id: state?.job_id ?? null,
23910
- pid: state?.pid ?? null,
23911
- target_version: state?.target_version ?? null,
23912
- started_at: state?.started_at ?? null,
23913
- finished_at: state?.finished_at ?? null,
23914
- exit_code: state?.exit_code ?? null,
23915
- signal: state?.signal ?? null,
23916
- log_path: updateLogPath2(paths),
23917
- lines: await readUpdateLogLines2(paths),
23918
- error: state?.error ?? null,
23919
- manual_command: state?.manual_command ?? null
23557
+ const message = error.message;
23558
+ return typeof message === "string" ? message : null;
23559
+ }
23560
+
23561
+ // src/runtime/system-info.ts
23562
+ import { execFileSync } from "child_process";
23563
+ import { readFileSync } from "fs";
23564
+ import os4 from "os";
23565
+ function readLinkSystemInfo() {
23566
+ const platform = process.platform;
23567
+ const hostname = readHostname(platform);
23568
+ const osLabel = readOsLabel(platform);
23569
+ const defaultDisplayName = buildDefaultDisplayName({ hostname, osLabel, platform });
23570
+ return {
23571
+ platform,
23572
+ hostname,
23573
+ osLabel,
23574
+ defaultDisplayName
23920
23575
  };
23921
23576
  }
23922
- function subscribeLinkUpdateStatus(listener) {
23923
- updateEvents2.on("status", listener);
23924
- return () => updateEvents2.off("status", listener);
23577
+ function buildDefaultDisplayName(input) {
23578
+ const hostname = normalizeText(input.hostname);
23579
+ const osLabel = normalizeText(input.osLabel);
23580
+ if (hostname) {
23581
+ return truncateText(hostname, 128);
23582
+ }
23583
+ return truncateText(hostname ?? osLabel ?? `Hermes Link ${input.platform}`, 128);
23925
23584
  }
23926
- async function writeFailedStartState(options, error, targetVersion = null) {
23927
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
23928
- const state = {
23929
- state: "failed",
23930
- job_id: `link_update_${now().getTime().toString(36)}`,
23931
- pid: null,
23932
- target_version: targetVersion,
23933
- started_at: now().toISOString(),
23934
- finished_at: now().toISOString(),
23935
- exit_code: null,
23936
- signal: null,
23937
- error,
23938
- manual_command: targetVersion ? manualInstallCommand(targetVersion) : null
23939
- };
23940
- await writeUpdateState2(options.paths, state);
23941
- await emitUpdateStatus2(options.paths);
23942
- return readLinkUpdateStatus(options.paths);
23585
+ function parseLinuxOsRelease(content) {
23586
+ const values = /* @__PURE__ */ new Map();
23587
+ for (const line of content.split(/\r?\n/u)) {
23588
+ const match = /^([A-Z0-9_]+)=(.*)$/u.exec(line.trim());
23589
+ if (!match) {
23590
+ continue;
23591
+ }
23592
+ values.set(match[1], unquoteOsReleaseValue(match[2]));
23593
+ }
23594
+ return normalizeText(values.get("PRETTY_NAME")) ?? buildLinuxName(values);
23943
23595
  }
23944
- async function readRemoteLinkPolicy(options) {
23945
- const context = await readLinkReleaseCheckContext(options.paths).catch(
23946
- () => null
23947
- );
23948
- try {
23949
- const response = await fetchCurrentLinkReleaseFromServer(
23950
- options,
23951
- options.fetchImpl ?? fetch
23952
- );
23953
- if (!response.ok) {
23954
- throw new Error(`HermesPilot Server returned HTTP ${response.status}`);
23596
+ function readHostname(platform) {
23597
+ if (platform === "darwin") {
23598
+ const computerName = readCommandOutput("scutil", ["--get", "ComputerName"]);
23599
+ if (computerName) {
23600
+ return computerName;
23955
23601
  }
23956
- const snapshot = normalizeServerSnapshot(await response.json());
23957
- if (!snapshot.remote) {
23958
- return {
23959
- remote: null,
23960
- state: "unavailable",
23961
- issue: snapshot.issue ?? "HermesPilot Server has no Link release policy"
23962
- };
23602
+ }
23603
+ return normalizeText(os4.hostname());
23604
+ }
23605
+ function readOsLabel(platform) {
23606
+ if (platform === "darwin") {
23607
+ const version = readCommandOutput("sw_vers", ["-productVersion"]);
23608
+ return version ? `macOS ${version}` : "macOS";
23609
+ }
23610
+ if (platform === "linux") {
23611
+ return readLinuxOsRelease() ?? `Linux ${os4.release()}`;
23612
+ }
23613
+ if (platform === "win32") {
23614
+ return `Windows ${os4.release()}`;
23615
+ }
23616
+ return `${os4.type()} ${os4.release()}`.trim();
23617
+ }
23618
+ function readLinuxOsRelease() {
23619
+ for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
23620
+ try {
23621
+ return parseLinuxOsRelease(readFileSync(file, "utf8"));
23622
+ } catch {
23963
23623
  }
23964
- return {
23965
- remote: snapshot.remote,
23966
- state: "fresh",
23967
- issue: snapshot.issue
23968
- };
23969
- } catch (error) {
23970
- const issue = error instanceof Error ? error.message : String(error);
23971
- void options.logger?.warn("link_release_server_check_failed", {
23972
- server_base_url: context?.serverBaseUrl ?? null,
23973
- release_check_url: context?.releaseCheckUrl ?? null,
23974
- error: issue
23624
+ }
23625
+ return null;
23626
+ }
23627
+ function readCommandOutput(command, args) {
23628
+ try {
23629
+ const output = execFileSync(command, args, {
23630
+ encoding: "utf8",
23631
+ stdio: ["ignore", "pipe", "ignore"],
23632
+ timeout: 1e3
23975
23633
  });
23976
- return { remote: null, state: "unavailable", issue };
23634
+ return normalizeText(output);
23635
+ } catch {
23636
+ return null;
23977
23637
  }
23978
23638
  }
23979
- function normalizeServerSnapshot(payload) {
23980
- const snapshot = toRecord17(payload);
23981
- const policy = toNullableRecord2(snapshot.policy);
23982
- if (!policy) {
23639
+ function buildLinuxName(values) {
23640
+ const name = normalizeText(values.get("NAME"));
23641
+ const version = normalizeText(values.get("VERSION_ID")) ?? normalizeText(values.get("VERSION"));
23642
+ if (name && version) {
23643
+ return `${name} ${version}`;
23644
+ }
23645
+ return name ?? version;
23646
+ }
23647
+ function unquoteOsReleaseValue(value) {
23648
+ const trimmed = value.trim();
23649
+ if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
23650
+ return trimmed.slice(1, -1).replace(/\\(["'`$\\])/gu, "$1");
23651
+ }
23652
+ return trimmed;
23653
+ }
23654
+ function normalizeText(value) {
23655
+ const normalized = value?.replace(/\s+/gu, " ").trim();
23656
+ return normalized ? normalized : null;
23657
+ }
23658
+ function truncateText(value, maxLength) {
23659
+ return value.length > maxLength ? value.slice(0, maxLength).trimEnd() : value;
23660
+ }
23661
+
23662
+ // src/topology/network.ts
23663
+ import os6 from "os";
23664
+
23665
+ // src/topology/environment.ts
23666
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
23667
+ import os5 from "os";
23668
+ function detectRuntimeEnvironment(env = process.env) {
23669
+ if (isWsl(env)) {
23983
23670
  return {
23984
- remote: null,
23985
- issue: readString18(snapshot, "issue")
23671
+ kind: "wsl",
23672
+ lanAutoDiscoveryUsable: false,
23673
+ warning: "Detected WSL. The LAN IP found inside WSL is usually a private VM address and is not reachable from your phone. Use Relay or set `hermeslink config set lan-host <Windows LAN IP>`."
23986
23674
  };
23987
23675
  }
23988
- const release = toNullableRecord2(snapshot.release);
23989
- const currentVersion = readString18(policy, "current_version") ?? readString18(policy, "currentVersion");
23990
- const minSafeVersion = readString18(policy, "min_safe_version") ?? readString18(policy, "minSafeVersion");
23991
- if (!currentVersion) {
23676
+ if (isContainer(env)) {
23992
23677
  return {
23993
- remote: null,
23994
- issue: readString18(snapshot, "issue")
23678
+ kind: "container",
23679
+ lanAutoDiscoveryUsable: false,
23680
+ warning: "Detected a container environment. Container LAN IPs are usually not reachable from your phone. Use Relay or set `hermeslink config set lan-host <host LAN IP>`."
23995
23681
  };
23996
23682
  }
23997
23683
  return {
23998
- remote: {
23999
- current_version: currentVersion,
24000
- min_safe_version: minSafeVersion,
24001
- target_version: currentVersion,
24002
- release_url: release ? readString18(release, "release_url") ?? readString18(release, "releaseUrl") : null,
24003
- published_at: release ? readString18(release, "published_at") ?? readString18(release, "publishedAt") : null
24004
- },
24005
- issue: readString18(snapshot, "issue")
23684
+ kind: "native",
23685
+ lanAutoDiscoveryUsable: true,
23686
+ warning: null
24006
23687
  };
24007
23688
  }
24008
- async function fetchCurrentLinkReleaseFromServer(options, fetcher) {
24009
- const config = await loadConfig(options.paths);
24010
- const url = new URL(SERVER_LINK_CURRENT_RELEASE_PATH, config.serverBaseUrl);
24011
- url.searchParams.set("channel", "stable");
24012
- url.searchParams.set("lang", "en");
24013
- const controller = new AbortController();
24014
- const timer = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
23689
+ function isWsl(env) {
23690
+ if (process.platform !== "linux") {
23691
+ return false;
23692
+ }
23693
+ if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
23694
+ return true;
23695
+ }
23696
+ const release = os5.release().toLowerCase();
23697
+ return release.includes("microsoft") || release.includes("wsl");
23698
+ }
23699
+ function isContainer(env) {
23700
+ if (env.container || env.CONTAINER || env.KUBERNETES_SERVICE_HOST) {
23701
+ return true;
23702
+ }
23703
+ if (existsSync("/.dockerenv")) {
23704
+ return true;
23705
+ }
24015
23706
  try {
24016
- return await fetcher(url, {
24017
- headers: {
24018
- accept: "application/json",
24019
- "user-agent": `HermesPilot-Link/${LINK_VERSION}`
24020
- },
24021
- signal: controller.signal
24022
- });
24023
- } catch (error) {
24024
- if (error instanceof Error && error.name === "AbortError") {
24025
- throw new Error("HermesPilot Server Link release check timed out");
23707
+ const cgroup = readFileSync2("/proc/1/cgroup", "utf8").toLowerCase();
23708
+ return /docker|containerd|kubepods|libpod|podman/u.test(cgroup);
23709
+ } catch {
23710
+ return false;
23711
+ }
23712
+ }
23713
+
23714
+ // src/topology/network.ts
23715
+ var VIRTUAL_INTERFACE_NAME_PATTERN = /(docker|veth|vmnet|vmenet|vbox|virtualbox|vmware|tailscale|zerotier|wireguard|utun|virbr|hyper-v|vethernet|loopback|\blo\b|^lo\d*$|^br-|^bridge\d+$|^zt|^tun|^tap|awdl|llw|anpi|gif|stf|ipsec|ppp)/iu;
23716
+ var MAX_LAN_IPS = 4;
23717
+ var MAX_PUBLIC_IPV4S = 2;
23718
+ var MAX_PUBLIC_IPV6S = 2;
23719
+ async function discoverRouteCandidates(options) {
23720
+ const environment = detectRuntimeEnvironment();
23721
+ const configuredLanHost = normalizeLanHost(options.configuredLanHost);
23722
+ const lanIps = configuredLanHost ? [configuredLanHost] : environment.lanAutoDiscoveryUsable ? discoverLanIps() : [];
23723
+ const publicIps = options.relayBootstrapToken || options.observePublicRoute ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
23724
+ const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
23725
+ const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
23726
+ const preferredUrls = [
23727
+ ...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
23728
+ ...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
23729
+ ...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
23730
+ `${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/links/${options.linkId}`
23731
+ ];
23732
+ return {
23733
+ lanIps,
23734
+ publicIpv4s,
23735
+ publicIpv6s,
23736
+ preferredUrls,
23737
+ environment
23738
+ };
23739
+ }
23740
+ function discoverLanIps() {
23741
+ return discoverLanIpsFromInterfaces(os6.networkInterfaces());
23742
+ }
23743
+ function discoverLanIpsFromInterfaces(interfaces) {
23744
+ const result = /* @__PURE__ */ new Set();
23745
+ const candidates = [];
23746
+ for (const [name, items] of Object.entries(interfaces)) {
23747
+ if (shouldIgnoreInterface(name)) {
23748
+ continue;
24026
23749
  }
24027
- throw error;
24028
- } finally {
24029
- clearTimeout(timer);
23750
+ for (const item of items ?? []) {
23751
+ if (!item.internal && item.address && item.family === "IPv4" && isUsableLanIpv42(item.address, item.netmask)) {
23752
+ candidates.push({ name, address: item.address });
23753
+ }
23754
+ }
23755
+ }
23756
+ for (const candidate of candidates.sort(compareLanCandidate)) {
23757
+ result.add(candidate.address);
24030
23758
  }
23759
+ return [...result].slice(0, MAX_LAN_IPS);
24031
23760
  }
24032
- async function readLinkReleaseCheckContext(paths) {
24033
- const config = await loadConfig(paths);
24034
- const url = new URL(SERVER_LINK_CURRENT_RELEASE_PATH, config.serverBaseUrl);
24035
- url.searchParams.set("channel", "stable");
24036
- url.searchParams.set("lang", "en");
23761
+ async function observePublicRoute(options) {
23762
+ const fetcher = options.fetchImpl ?? fetch;
23763
+ const response = await fetcher(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/public-route/observe`, {
23764
+ method: "POST",
23765
+ headers: {
23766
+ "content-type": "application/json",
23767
+ ...options.relayBootstrapToken ? { authorization: `Bearer ${options.relayBootstrapToken}` } : {}
23768
+ },
23769
+ body: JSON.stringify({
23770
+ install_id: options.installId,
23771
+ link_id: options.linkId,
23772
+ public_key_pem: options.publicKeyPem
23773
+ })
23774
+ });
23775
+ const payload = await response.json().catch(() => null);
23776
+ const record = typeof payload?.record === "object" && payload.record !== null ? payload.record : null;
23777
+ const observed = typeof payload?.observed === "object" && payload.observed !== null ? payload.observed : null;
23778
+ const values = [
23779
+ readIpRecord(record?.ipv4),
23780
+ readIpRecord(record?.ipv6),
23781
+ typeof observed?.ip === "string" ? observed.ip : null
23782
+ ].filter((value) => Boolean(value));
24037
23783
  return {
24038
- serverBaseUrl: config.serverBaseUrl,
24039
- releaseCheckUrl: url.toString()
23784
+ publicIpv4s: unique(values.filter(isUsablePublicIpv4)),
23785
+ publicIpv6s: unique(values.filter(isUsablePublicIpv6))
24040
23786
  };
24041
23787
  }
24042
- function computeLinkUpdateState(localVersion, remote) {
24043
- if (!remote?.current_version) {
24044
- return "unknown";
24045
- }
24046
- if (remote.min_safe_version && compareSemver3(localVersion, remote.min_safe_version) < 0) {
24047
- return "unsafe";
24048
- }
24049
- const diff = compareSemver3(localVersion, remote.current_version);
24050
- if (diff < 0) {
24051
- return "update_available";
24052
- }
24053
- if (diff > 0) {
24054
- return "ahead_of_current";
24055
- }
24056
- return "current";
24057
- }
24058
- async function emitUpdateStatus2(paths) {
24059
- updateEvents2.emit("status", await readLinkUpdateStatus(paths));
24060
- }
24061
- async function writeUpdateState2(paths, state) {
24062
- await writeJsonFile(updateStatePath2(paths), state);
24063
- }
24064
- async function readUpdateLogLines2(paths) {
24065
- const raw = await readFile18(updateLogPath2(paths), "utf8").catch(() => "");
24066
- if (!raw.trim()) {
24067
- return [];
23788
+ function readIpRecord(value) {
23789
+ if (typeof value !== "object" || value === null) {
23790
+ return null;
24068
23791
  }
24069
- return raw.split(/\r?\n/u).map((line) => line.trimEnd()).filter(Boolean).slice(-MAX_UPDATE_LOG_LINES2).map(
24070
- (line) => line.length > MAX_OUTPUT_LINE_LENGTH3 ? `${line.slice(0, MAX_OUTPUT_LINE_LENGTH3)}...` : line
24071
- );
24072
- }
24073
- function updateStatePath2(paths) {
24074
- return path25.join(paths.runDir, "link-update-state.json");
24075
- }
24076
- function updateLogPath2(paths) {
24077
- return path25.join(paths.logsDir, UPDATE_LOG_FILE2);
23792
+ const ip = value.ip;
23793
+ return typeof ip === "string" && ip.trim() ? ip.trim() : null;
24078
23794
  }
24079
- async function clearUpdateLogFiles2(paths) {
24080
- const primary = updateLogPath2(paths);
24081
- await Promise.all([
24082
- rm10(primary, { force: true }).catch(() => void 0),
24083
- ...Array.from(
24084
- { length: UPDATE_LOG_MAX_FILES2 },
24085
- (_, index) => rm10(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
24086
- )
24087
- ]);
23795
+ function buildDirectUrl(ip, port) {
23796
+ return `http://${ip.includes(":") ? `[${ip}]` : ip}:${port}`;
24088
23797
  }
24089
- function manualInstallCommand(version) {
24090
- return `npm install -g ${LINK_NPM_PACKAGE}@${version}`;
23798
+ function shouldIgnoreInterface(name) {
23799
+ return !name.trim() || VIRTUAL_INTERFACE_NAME_PATTERN.test(name);
24091
23800
  }
24092
- function resolveNpmBin() {
24093
- return process.platform === "win32" ? "npm.cmd" : "npm";
23801
+ function compareLanCandidate(left, right) {
23802
+ const priority = interfacePriority(left.name) - interfacePriority(right.name);
23803
+ return priority || left.name.localeCompare(right.name) || left.address.localeCompare(right.address);
24094
23804
  }
24095
- function compareSemver3(left, right) {
24096
- const leftParts = parseSemver(left);
24097
- const rightParts = parseSemver(right);
24098
- for (let index = 0; index < 3; index += 1) {
24099
- const diff = leftParts[index] - rightParts[index];
24100
- if (diff !== 0) {
24101
- return diff;
24102
- }
23805
+ function interfacePriority(name) {
23806
+ if (/^(en|eth|wlan|wi-fi|wifi)/iu.test(name)) {
23807
+ return 0;
24103
23808
  }
24104
- return 0;
23809
+ return 1;
24105
23810
  }
24106
- function parseSemver(value) {
24107
- const match = /^v?(\d+)\.(\d+)\.(\d+)/u.exec(value.trim());
24108
- return [
24109
- Number.parseInt(match?.[1] ?? "0", 10),
24110
- Number.parseInt(match?.[2] ?? "0", 10),
24111
- Number.parseInt(match?.[3] ?? "0", 10)
24112
- ];
23811
+ function isUsableLanIpv42(address, netmask) {
23812
+ return isPrivateIpv4(address) && !isNetworkOrBroadcastIpv4Address(address, netmask);
24113
23813
  }
24114
- function isRecentRunningState3(state, now = Date.now()) {
24115
- const startedAt = state.started_at ? Date.parse(state.started_at) : Number.NaN;
24116
- return Number.isFinite(startedAt) && now - startedAt < 1e4;
23814
+ function isUsablePublicIpv4(address) {
23815
+ return isValidIpv4(address) && !isSpecialIpv4(address);
24117
23816
  }
24118
- function isProcessAlive4(pid) {
24119
- if (!pid || pid <= 0) {
23817
+ function isUsablePublicIpv6(address) {
23818
+ const normalized = address.toLowerCase();
23819
+ return normalized.includes(":") && !normalized.startsWith("fe80:") && !normalized.startsWith("fc") && !normalized.startsWith("fd") && !normalized.startsWith("ff") && normalized !== "::" && normalized !== "::1";
23820
+ }
23821
+ function isPrivateIpv4(address) {
23822
+ const parts = parseIpv4Segments(address);
23823
+ if (!parts) {
24120
23824
  return false;
24121
23825
  }
24122
- try {
24123
- process.kill(pid, 0);
23826
+ const [first, second] = parts;
23827
+ return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
23828
+ }
23829
+ function isSpecialIpv4(address) {
23830
+ const parts = parseIpv4Segments(address);
23831
+ if (!parts) {
24124
23832
  return true;
24125
- } catch {
24126
- return false;
24127
23833
  }
23834
+ const [first, second, third, fourth] = parts;
23835
+ return first === 0 || first === 10 || first === 127 || first >= 224 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 192 && second === 0 && third === 2 || first === 198 && (second === 18 || second === 19) || first === 198 && second === 51 && third === 100 || first === 203 && second === 0 && third === 113 || first === 255 && second === 255 && third === 255 && fourth === 255;
24128
23836
  }
24129
- function toRecord17(value) {
24130
- return typeof value === "object" && value !== null ? value : {};
24131
- }
24132
- function toNullableRecord2(value) {
24133
- return typeof value === "object" && value !== null ? value : null;
24134
- }
24135
- function readString18(payload, key) {
24136
- const value = payload[key];
24137
- return typeof value === "string" && value.trim() ? value.trim() : null;
24138
- }
24139
-
24140
- // src/pairing/pairing.ts
24141
- import path26 from "path";
24142
- import { rm as rm11 } from "fs/promises";
24143
-
24144
- // src/relay/bootstrap.ts
24145
- var RelayNetworkError = class extends Error {
24146
- constructor(relayBaseUrl, causeMessage) {
24147
- super(
24148
- `Cannot reach Hermes Relay at ${relayBaseUrl}. Please check your network connection, VPN, or proxy settings, then try again.`
24149
- );
24150
- this.relayBaseUrl = relayBaseUrl;
24151
- this.causeMessage = causeMessage;
23837
+ function isNetworkOrBroadcastIpv4Address(address, netmask) {
23838
+ const addressParts = parseIpv4Segments(address);
23839
+ const netmaskParts = netmask ? parseIpv4Segments(netmask) : null;
23840
+ if (!addressParts) {
23841
+ return true;
24152
23842
  }
24153
- relayBaseUrl;
24154
- causeMessage;
24155
- };
24156
- async function bootstrapRelayLink(options) {
24157
- const fetcher = options.fetchImpl ?? fetch;
24158
- const baseUrl = options.relayBaseUrl.replace(/\/+$/u, "");
24159
- const commonPayload = {
24160
- install_id: options.identity.install_id,
24161
- link_id: options.identity.link_id ?? void 0,
24162
- public_key_pem: options.identity.public_key_pem
24163
- };
24164
- const challenge = await postJson(
24165
- fetcher,
24166
- `${baseUrl}/api/v1/relay/link/challenge`,
24167
- options.relayBootstrapToken,
24168
- commonPayload
24169
- );
24170
- if (challenge.ok !== true || typeof challenge.nonce !== "string") {
24171
- throw new Error("Relay did not return a valid install challenge");
23843
+ if (!netmaskParts) {
23844
+ const last = addressParts[3];
23845
+ return last === 0 || last === 255;
24172
23846
  }
24173
- const proof = {
24174
- nonce: challenge.nonce,
24175
- signature: signRelayNonce(options.identity, challenge.nonce)
24176
- };
24177
- const assigned = await postJson(
24178
- fetcher,
24179
- `${baseUrl}/api/v1/relay/link/bootstrap`,
24180
- options.relayBootstrapToken,
24181
- {
24182
- ...commonPayload,
24183
- proof
24184
- }
24185
- );
24186
- if (assigned.ok !== true || typeof assigned.link_id !== "string") {
24187
- throw new Error("Relay did not return a valid link_id");
23847
+ const addressInt = ipv4SegmentsToInt(addressParts);
23848
+ const netmaskInt = ipv4SegmentsToInt(netmaskParts);
23849
+ const hostMask = ~netmaskInt >>> 0;
23850
+ if (hostMask === 0) {
23851
+ return false;
24188
23852
  }
24189
- await saveAssignedLinkId(assigned.link_id, options.paths ?? resolveRuntimePaths());
24190
- return {
24191
- linkId: assigned.link_id,
24192
- reused: assigned.reused === true
24193
- };
23853
+ const networkInt = addressInt & netmaskInt;
23854
+ const broadcastInt = (networkInt | hostMask) >>> 0;
23855
+ return addressInt === networkInt || addressInt === broadcastInt;
24194
23856
  }
24195
- async function postJson(fetcher, url, token, body) {
24196
- let response;
24197
- try {
24198
- response = await fetcher(url, {
24199
- method: "POST",
24200
- headers: {
24201
- authorization: `Bearer ${token}`,
24202
- "content-type": "application/json"
24203
- },
24204
- body: JSON.stringify(body)
24205
- });
24206
- } catch (error) {
24207
- const baseUrl = new URL(url).origin;
24208
- throw new RelayNetworkError(
24209
- baseUrl,
24210
- error instanceof Error ? error.message : String(error)
24211
- );
24212
- }
24213
- const payload = await response.json().catch(() => null);
24214
- if (!response.ok) {
24215
- const message = readErrorMessage4(payload) ?? `Relay request failed with HTTP ${response.status}`;
24216
- throw new Error(message);
24217
- }
24218
- if (!payload) {
24219
- throw new Error("Relay returned an empty response");
24220
- }
24221
- return payload;
23857
+ function isValidIpv4(address) {
23858
+ return Boolean(parseIpv4Segments(address));
24222
23859
  }
24223
- function readErrorMessage4(payload) {
24224
- if (typeof payload !== "object" || payload === null) {
23860
+ function parseIpv4Segments(address) {
23861
+ if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(address)) {
24225
23862
  return null;
24226
23863
  }
24227
- const error = payload.error;
24228
- if (typeof error !== "object" || error === null) {
23864
+ const parts = address.split(".").map((part) => Number.parseInt(part, 10));
23865
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
24229
23866
  return null;
24230
23867
  }
24231
- const message = error.message;
24232
- return typeof message === "string" ? message : null;
23868
+ return parts;
23869
+ }
23870
+ function ipv4SegmentsToInt(parts) {
23871
+ return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
23872
+ }
23873
+ function unique(values) {
23874
+ return [...new Set(values)];
24233
23875
  }
24234
23876
 
24235
23877
  // src/pairing/pairing.ts
@@ -24401,7 +24043,7 @@ async function readPairingClaim(sessionId, paths = resolveRuntimePaths()) {
24401
24043
  };
24402
24044
  }
24403
24045
  async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
24404
- await rm11(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
24046
+ await rm9(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
24405
24047
  }
24406
24048
  async function claimPairing(input) {
24407
24049
  const paths = input.paths ?? resolveRuntimePaths();
@@ -24478,10 +24120,10 @@ async function loadRequiredIdentity2(paths) {
24478
24120
  }
24479
24121
  return identity;
24480
24122
  }
24481
- async function postServerJson(serverBaseUrl, path27, body, options) {
24123
+ async function postServerJson(serverBaseUrl, path26, body, options) {
24482
24124
  let response;
24483
24125
  try {
24484
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path27}`, {
24126
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
24485
24127
  method: "POST",
24486
24128
  headers: {
24487
24129
  accept: "application/json",
@@ -24529,10 +24171,10 @@ function pairingErrorSnapshot(stage, error) {
24529
24171
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
24530
24172
  };
24531
24173
  }
24532
- async function patchServerJson(serverBaseUrl, path27, token, body, options) {
24174
+ async function patchServerJson(serverBaseUrl, path26, token, body, options) {
24533
24175
  let response;
24534
24176
  try {
24535
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path27}`, {
24177
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
24536
24178
  method: "PATCH",
24537
24179
  headers: {
24538
24180
  accept: "application/json",
@@ -24554,12 +24196,12 @@ async function patchServerJson(serverBaseUrl, path27, token, body, options) {
24554
24196
  async function readJsonResponse2(response) {
24555
24197
  const payload = await response.json().catch(() => null);
24556
24198
  if (!response.ok || !payload) {
24557
- const message = readErrorMessage5(payload) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
24199
+ const message = readErrorMessage4(payload) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
24558
24200
  throw new LinkHttpError(response.status, "server_request_failed", message);
24559
24201
  }
24560
24202
  return payload;
24561
24203
  }
24562
- function readErrorMessage5(payload) {
24204
+ function readErrorMessage4(payload) {
24563
24205
  if (typeof payload !== "object" || payload === null) {
24564
24206
  return null;
24565
24207
  }
@@ -24580,10 +24222,10 @@ function createPairingNetworkError(input) {
24580
24222
  );
24581
24223
  }
24582
24224
  function pairingClaimPath(sessionId, paths) {
24583
- return path26.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
24225
+ return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
24584
24226
  }
24585
24227
  function pairingSessionPath(sessionId, paths) {
24586
- return path26.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
24228
+ return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
24587
24229
  }
24588
24230
  function qrPreferredUrls(routes) {
24589
24231
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -25757,14 +25399,20 @@ async function createApp(options = {}) {
25757
25399
  export {
25758
25400
  LINK_VERSION,
25759
25401
  LINK_COMMAND,
25402
+ migrateLinkDatabase,
25760
25403
  LinkHttpError,
25404
+ readJsonFile,
25405
+ writeJsonFile,
25761
25406
  resolveHermesProfileDir,
25762
25407
  resolveHermesConfigPath,
25763
25408
  readHermesApiServerConfig,
25764
25409
  ensureHermesApiServerConfig,
25410
+ syncHermesLinkCronDeliveries,
25765
25411
  resolveRuntimePaths,
25766
25412
  createFileLogger,
25767
25413
  getLinkLogFile,
25414
+ getDaemonLogFile,
25415
+ createRotatingTextLogWriter,
25768
25416
  ensureHermesApiServerAvailable,
25769
25417
  readHermesVersion,
25770
25418
  defaultLinkConfig,
@@ -25774,22 +25422,15 @@ export {
25774
25422
  normalizeLanHost,
25775
25423
  loadIdentity,
25776
25424
  ensureIdentity,
25425
+ signIdentityPayload,
25777
25426
  getIdentityStatus,
25778
25427
  ConversationService,
25779
25428
  hasActiveDevices,
25429
+ readLinkSystemInfo,
25780
25430
  detectRuntimeEnvironment,
25431
+ discoverRouteCandidates,
25781
25432
  preparePairing,
25782
25433
  readPairingClaim,
25783
25434
  clearPairingClaim,
25784
- createApp,
25785
- connectRelayControl,
25786
- reportLinkStatusToServer,
25787
- startLinkService,
25788
- startDaemonProcess,
25789
- runDaemonSupervisor,
25790
- probeLocalLinkService,
25791
- stopDaemonProcess,
25792
- getDaemonStatus,
25793
- daemonLogFile,
25794
- currentCliScriptPath
25435
+ createApp
25795
25436
  };