@hermespilot/link 0.5.1 → 0.5.3

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();
@@ -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.3";
4188
4506
  var LINK_COMMAND = "hermeslink";
4189
4507
  var LINK_DEFAULT_PORT = 52379;
4190
4508
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -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,6 +20995,95 @@ function normalizeHindsightMode(value) {
20455
20995
  const mode = readString15(value) ?? "cloud";
20456
20996
  return mode === "local" ? "local_embedded" : mode;
20457
20997
  }
20998
+ function normalizeHttpUrl(value) {
20999
+ try {
21000
+ const url = new URL(value);
21001
+ return url.protocol === "http:" || url.protocol === "https:" ? url : null;
21002
+ } catch {
21003
+ return null;
21004
+ }
21005
+ }
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
+ }
20458
21087
  async function readActiveMemoryProvider(profileName) {
20459
21088
  const raw = await readFile14(
20460
21089
  resolveHermesConfigPath(profileName),
@@ -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);
@@ -22293,8 +22965,73 @@ import { mkdir as mkdir13, rm as rm8, writeFile as writeFile3 } from "fs/promise
22293
22965
 
22294
22966
  // src/relay/control-client.ts
22295
22967
  import WebSocket from "ws";
22296
- var RELAY_SSE_BATCH_FLUSH_INTERVAL_MS = 50;
22297
- var RELAY_SSE_BATCH_FLUSH_BYTES = 2 * 1024;
22968
+
22969
+ // src/relay/stream-policy.ts
22970
+ var DEFAULT_RELAY_STREAM_BATCH_POLICY = {
22971
+ flushIntervalMs: 50,
22972
+ flushBytes: 2 * 1024
22973
+ };
22974
+ var RELAY_STREAM_POLICY_CONSTRAINTS = {
22975
+ flushIntervalMs: {
22976
+ min: 50,
22977
+ max: 1e3
22978
+ },
22979
+ flushBytes: {
22980
+ min: 1024,
22981
+ max: 64 * 1024
22982
+ }
22983
+ };
22984
+ async function fetchRelayStreamBatchPolicy(serverBaseUrl, options = {}) {
22985
+ const fetchImpl = options.fetchImpl ?? fetch;
22986
+ const controller = new AbortController();
22987
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
22988
+ timeout.unref?.();
22989
+ try {
22990
+ const response = await fetchImpl(`${serverBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/stream-policy`, {
22991
+ headers: {
22992
+ accept: "application/json"
22993
+ },
22994
+ signal: controller.signal
22995
+ });
22996
+ if (!response.ok) {
22997
+ return null;
22998
+ }
22999
+ const payload = await response.json().catch(() => null);
23000
+ return readRelayStreamBatchPolicy(payload);
23001
+ } catch {
23002
+ return null;
23003
+ } finally {
23004
+ clearTimeout(timeout);
23005
+ }
23006
+ }
23007
+ function readRelayStreamBatchPolicy(input) {
23008
+ const record = readRecord(input);
23009
+ const body = readRecord(record?.policy) ?? readRecord(record?.stream_batching) ?? record;
23010
+ return normalizeRelayStreamBatchPolicy(body);
23011
+ }
23012
+ function normalizeRelayStreamBatchPolicy(input) {
23013
+ const record = readRecord(input);
23014
+ if (!record) {
23015
+ return null;
23016
+ }
23017
+ const flushIntervalMs = readInteger4(record.flushIntervalMs ?? record.flush_interval_ms);
23018
+ const flushBytes = readInteger4(record.flushBytes ?? record.flush_bytes);
23019
+ if (flushIntervalMs === null || flushBytes === null || flushIntervalMs < RELAY_STREAM_POLICY_CONSTRAINTS.flushIntervalMs.min || flushIntervalMs > RELAY_STREAM_POLICY_CONSTRAINTS.flushIntervalMs.max || flushBytes < RELAY_STREAM_POLICY_CONSTRAINTS.flushBytes.min || flushBytes > RELAY_STREAM_POLICY_CONSTRAINTS.flushBytes.max) {
23020
+ return null;
23021
+ }
23022
+ return {
23023
+ flushIntervalMs,
23024
+ flushBytes
23025
+ };
23026
+ }
23027
+ function readRecord(value) {
23028
+ return value && typeof value === "object" ? value : null;
23029
+ }
23030
+ function readInteger4(value) {
23031
+ return typeof value === "number" && Number.isInteger(value) ? value : null;
23032
+ }
23033
+
23034
+ // src/relay/control-client.ts
22298
23035
  function connectRelayControl(options) {
22299
23036
  const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
22300
23037
  wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
@@ -22309,6 +23046,10 @@ function connectRelayControl(options) {
22309
23046
  let abortControllers = /* @__PURE__ */ new Map();
22310
23047
  let fatalRelayRejection = null;
22311
23048
  let latestNetworkRoutes = null;
23049
+ const streamBatchPolicy = {
23050
+ current: options.initialStreamBatchPolicy ?? DEFAULT_RELAY_STREAM_BATCH_POLICY,
23051
+ onUpdate: options.onStreamBatchPolicy
23052
+ };
22312
23053
  const connect = () => {
22313
23054
  options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
22314
23055
  fatalRelayRejection = null;
@@ -22329,7 +23070,7 @@ function connectRelayControl(options) {
22329
23070
  if (!socket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
22330
23071
  return;
22331
23072
  }
22332
- void handleFrame(socket, String(raw), options.localPort, abortControllers).catch((error) => {
23073
+ void handleFrame(socket, String(raw), options.localPort, abortControllers, streamBatchPolicy).catch((error) => {
22333
23074
  const message = error instanceof Error ? error.message : "Relay request failed";
22334
23075
  socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
22335
23076
  });
@@ -22377,6 +23118,10 @@ function connectRelayControl(options) {
22377
23118
  sendNetworkRoutes(socket, options.linkId, routes);
22378
23119
  }
22379
23120
  },
23121
+ updateStreamBatchPolicy(policy) {
23122
+ streamBatchPolicy.current = policy;
23123
+ streamBatchPolicy.onUpdate?.(policy);
23124
+ },
22380
23125
  close() {
22381
23126
  closedByUser = true;
22382
23127
  if (retryTimer) {
@@ -22418,8 +23163,16 @@ function computeBackoffMs(attempt, baseMs, maxMs) {
22418
23163
  const jitter = Math.floor(Math.random() * Math.min(1e3, exponential * 0.2));
22419
23164
  return exponential + jitter;
22420
23165
  }
22421
- async function handleFrame(socket, raw, localPort, abortControllers) {
23166
+ async function handleFrame(socket, raw, localPort, abortControllers, streamBatchPolicy) {
22422
23167
  const frame = JSON.parse(raw);
23168
+ if (frame.type === "relay.config.update") {
23169
+ const nextPolicy = readRelayStreamBatchPolicy(frame.payload);
23170
+ if (nextPolicy) {
23171
+ streamBatchPolicy.current = nextPolicy;
23172
+ streamBatchPolicy.onUpdate?.(nextPolicy);
23173
+ }
23174
+ return;
23175
+ }
22423
23176
  if (frame.type === "http.cancel") {
22424
23177
  abortControllers.get(frame.id)?.abort();
22425
23178
  abortControllers.delete(frame.id);
@@ -22442,7 +23195,7 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
22442
23195
  const contentType = response.headers.get("content-type") ?? "";
22443
23196
  if (response.body && contentType.includes("text/event-stream")) {
22444
23197
  socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
22445
- sseBatcher = createRelayStreamChunkBatcher(socket, frame.id);
23198
+ sseBatcher = createRelayStreamChunkBatcher(socket, frame.id, streamBatchPolicy);
22446
23199
  const reader = response.body.getReader();
22447
23200
  while (true) {
22448
23201
  const next = await reader.read();
@@ -22472,7 +23225,7 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
22472
23225
  function isAbortError2(error) {
22473
23226
  return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
22474
23227
  }
22475
- function createRelayStreamChunkBatcher(socket, id) {
23228
+ function createRelayStreamChunkBatcher(socket, id, streamBatchPolicy) {
22476
23229
  let chunks = [];
22477
23230
  let totalBytes = 0;
22478
23231
  let flushTimer = null;
@@ -22503,7 +23256,7 @@ function createRelayStreamChunkBatcher(socket, id) {
22503
23256
  flushTimer = setTimeout(() => {
22504
23257
  flushTimer = null;
22505
23258
  flush();
22506
- }, RELAY_SSE_BATCH_FLUSH_INTERVAL_MS);
23259
+ }, streamBatchPolicy.current.flushIntervalMs);
22507
23260
  flushTimer.unref?.();
22508
23261
  };
22509
23262
  return {
@@ -22514,7 +23267,7 @@ function createRelayStreamChunkBatcher(socket, id) {
22514
23267
  const buffer = Buffer.from(chunk);
22515
23268
  chunks.push(buffer);
22516
23269
  totalBytes += buffer.byteLength;
22517
- if (totalBytes >= RELAY_SSE_BATCH_FLUSH_BYTES) {
23270
+ if (totalBytes >= streamBatchPolicy.current.flushBytes) {
22518
23271
  flush();
22519
23272
  return;
22520
23273
  }
@@ -23282,6 +24035,21 @@ async function startLinkService(options = {}) {
23282
24035
  }
23283
24036
  const conversations = new ConversationService(paths, logger);
23284
24037
  await conversations.rebuildStatisticsIndex();
24038
+ let relay = null;
24039
+ let lanIpMonitor = null;
24040
+ const loadRelayStreamBatchPolicy = async (source) => {
24041
+ const streamBatchPolicy = await fetchRelayStreamBatchPolicy(config.serverBaseUrl);
24042
+ if (!streamBatchPolicy) {
24043
+ return null;
24044
+ }
24045
+ relay?.updateStreamBatchPolicy(streamBatchPolicy);
24046
+ void logger.info("relay_stream_policy_loaded", {
24047
+ source,
24048
+ flushIntervalMs: streamBatchPolicy.flushIntervalMs,
24049
+ flushBytes: streamBatchPolicy.flushBytes
24050
+ });
24051
+ return streamBatchPolicy;
24052
+ };
23285
24053
  let hermesSessionSync = Promise.resolve();
23286
24054
  const triggerHermesSessionSync = () => {
23287
24055
  hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
@@ -23297,6 +24065,7 @@ async function startLinkService(options = {}) {
23297
24065
  conversations,
23298
24066
  onPairingClaimed: async () => {
23299
24067
  triggerHermesSessionSync();
24068
+ void loadRelayStreamBatchPolicy("pairing_claimed");
23300
24069
  await options.onPairingClaimed?.();
23301
24070
  }
23302
24071
  });
@@ -23333,8 +24102,6 @@ async function startLinkService(options = {}) {
23333
24102
  conversations,
23334
24103
  logger
23335
24104
  });
23336
- let relay = null;
23337
- let lanIpMonitor = null;
23338
24105
  let hasSeenRelayConnected = false;
23339
24106
  let lastRelayReconnectPublicRouteRefreshAt = 0;
23340
24107
  if (identity?.link_id) {
@@ -23349,6 +24116,12 @@ async function startLinkService(options = {}) {
23349
24116
  maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
23350
24117
  backoffBaseMs: 1e3,
23351
24118
  backoffMaxMs: 3e4,
24119
+ onStreamBatchPolicy: (policy) => {
24120
+ void logger.info("relay_stream_policy_updated", {
24121
+ flushIntervalMs: policy.flushIntervalMs,
24122
+ flushBytes: policy.flushBytes
24123
+ });
24124
+ },
23352
24125
  onStatus: (status) => {
23353
24126
  void logger.info("relay_status", status);
23354
24127
  if (status.state === "connected") {
@@ -23366,6 +24139,7 @@ async function startLinkService(options = {}) {
23366
24139
  }
23367
24140
  }
23368
24141
  });
24142
+ void loadRelayStreamBatchPolicy("service_startup");
23369
24143
  if (options.waitForRelayReady) {
23370
24144
  await Promise.race([
23371
24145
  relayReady,
@@ -23674,7 +24448,11 @@ function wait(ms) {
23674
24448
 
23675
24449
  // src/link/updates.ts
23676
24450
  var SERVER_LINK_CURRENT_RELEASE_PATH = "/api/v1/link/releases/current";
24451
+ var SERVER_LINK_INSTALL_SCRIPTS_PATH = "/api/v1/link/install-scripts";
23677
24452
  var LINK_NPM_PACKAGE = "@hermespilot/link";
24453
+ var OFFICIAL_INSTALLER_BASE_URL = "https://hs.clawpilot.me/install";
24454
+ var OFFICIAL_UNIX_INSTALLER_URL = `${OFFICIAL_INSTALLER_BASE_URL}/install.sh`;
24455
+ var OFFICIAL_WINDOWS_INSTALLER_URL = `${OFFICIAL_INSTALLER_BASE_URL}/install.ps1`;
23678
24456
  var UPDATE_LOG_FILE2 = "link-update.log";
23679
24457
  var UPDATE_LOG_MAX_FILES2 = 3;
23680
24458
  var UPDATE_FETCH_TIMEOUT_MS = 5e3;
@@ -23721,6 +24499,13 @@ async function startLinkUpdate(options) {
23721
24499
  "HermesPilot Server has no Link target version."
23722
24500
  );
23723
24501
  }
24502
+ if (!isValidReleaseVersion(targetVersion)) {
24503
+ return writeFailedStartState(
24504
+ options,
24505
+ `HermesPilot Server returned invalid Link target version: ${targetVersion}.`,
24506
+ targetVersion
24507
+ );
24508
+ }
23724
24509
  if (options.targetVersion && options.targetVersion !== targetVersion) {
23725
24510
  return writeFailedStartState(
23726
24511
  options,
@@ -23745,7 +24530,8 @@ async function startLinkUpdate(options) {
23745
24530
  maxFiles: UPDATE_LOG_MAX_FILES2
23746
24531
  });
23747
24532
  const startedAt = now().toISOString();
23748
- const manualCommand = manualInstallCommand(targetVersion);
24533
+ const installCommand = await buildOfficialInstallCommand(options, targetVersion);
24534
+ const manualCommand = installCommand.displayCommand;
23749
24535
  const started = {
23750
24536
  state: "running",
23751
24537
  job_id: jobId,
@@ -23767,16 +24553,50 @@ async function startLinkUpdate(options) {
23767
24553
  await writer.write(`$ ${manualCommand}
23768
24554
  `);
23769
24555
  await writeUpdateState2(options.paths, started);
23770
- const child = spawn5(
23771
- resolveNpmBin(),
23772
- ["install", "-g", `${LINK_NPM_PACKAGE}@${targetVersion}`],
23773
- {
23774
- stdio: ["ignore", "pipe", "pipe"],
23775
- windowsHide: true,
23776
- detached: false,
23777
- shell: false
23778
- }
23779
- );
24556
+ if (process.platform === "win32") {
24557
+ await writer.write(
24558
+ "[windows-updater] A detached updater will stop Hermes Link before replacing the npm package, then start it again.\n"
24559
+ );
24560
+ await writer.flush();
24561
+ const child2 = spawnWindowsDetachedUpdater({
24562
+ installCommand,
24563
+ statePath: updateStatePath2(options.paths),
24564
+ logPath: writer.filePath,
24565
+ jobId,
24566
+ targetVersion,
24567
+ startedAt,
24568
+ manualCommand
24569
+ });
24570
+ started.pid = child2.pid ?? null;
24571
+ await writeUpdateState2(options.paths, started);
24572
+ child2.on("error", (error) => {
24573
+ void (async () => {
24574
+ const failed = {
24575
+ ...started,
24576
+ state: "failed",
24577
+ finished_at: now().toISOString(),
24578
+ error: error.message
24579
+ };
24580
+ await writer.write(
24581
+ `
24582
+ [failed] Windows detached updater failed to start: ${error.message}
24583
+ `
24584
+ );
24585
+ await writeUpdateState2(options.paths, failed);
24586
+ await emitUpdateStatus2(options.paths);
24587
+ })();
24588
+ });
24589
+ await emitUpdateStatus2(options.paths);
24590
+ void options.logger?.info("link_update_started", {
24591
+ job_id: jobId,
24592
+ pid: child2.pid ?? null,
24593
+ target_version: targetVersion,
24594
+ log_path: writer.filePath,
24595
+ strategy: "windows_detached_updater"
24596
+ });
24597
+ return readLinkUpdateStatus(options.paths);
24598
+ }
24599
+ const child = spawnInstallCommand(installCommand);
23780
24600
  started.pid = child.pid ?? null;
23781
24601
  await writeUpdateState2(options.paths, started);
23782
24602
  const appendChunk = async (chunk) => {
@@ -23822,7 +24642,7 @@ async function startLinkUpdate(options) {
23822
24642
  finished_at: now().toISOString(),
23823
24643
  exit_code: code,
23824
24644
  signal,
23825
- error: succeeded ? null : `npm install exited with code ${code ?? "unknown"}`
24645
+ error: succeeded ? null : `install script exited with code ${code ?? "unknown"}`
23826
24646
  };
23827
24647
  await writer.write(
23828
24648
  `
@@ -23884,18 +24704,23 @@ function scheduleAutomaticRestart(options) {
23884
24704
  }
23885
24705
  async function readLinkUpdateStatus(paths) {
23886
24706
  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
- }
24707
+ if ((state?.state === "running" || state?.state === "restart_required") && state.target_version && compareSemver3(LINK_VERSION, state.target_version) >= 0) {
24708
+ state = {
24709
+ ...state,
24710
+ state: "succeeded",
24711
+ finished_at: state.finished_at ?? (/* @__PURE__ */ new Date()).toISOString(),
24712
+ error: null
24713
+ };
24714
+ await writeUpdateState2(paths, state);
23896
24715
  }
23897
24716
  if (state?.state === "running" && !runningUpdate2 && !isRecentRunningState3(state) && !isProcessAlive4(state.pid)) {
23898
- state = {
24717
+ const reachedTarget = state.target_version && compareSemver3(LINK_VERSION, state.target_version) >= 0;
24718
+ state = reachedTarget ? {
24719
+ ...state,
24720
+ state: "succeeded",
24721
+ finished_at: (/* @__PURE__ */ new Date()).toISOString(),
24722
+ error: null
24723
+ } : {
23899
24724
  ...state,
23900
24725
  state: "failed",
23901
24726
  finished_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -24029,6 +24854,260 @@ async function fetchCurrentLinkReleaseFromServer(options, fetcher) {
24029
24854
  clearTimeout(timer);
24030
24855
  }
24031
24856
  }
24857
+ async function buildOfficialInstallCommand(options, targetVersion) {
24858
+ const installer = await readOfficialInstallerUrls(options).catch((error) => {
24859
+ options.logger?.warn?.(
24860
+ `[link-update] failed to read installer config from server: ${error instanceof Error ? error.message : String(error)}`
24861
+ );
24862
+ return defaultInstallerUrls();
24863
+ });
24864
+ const env = {
24865
+ HERMESLINK_VERSION: targetVersion,
24866
+ HERMESLINK_YES: "1",
24867
+ HERMESLINK_NO_PROFILE_EDIT: "1",
24868
+ HERMESLINK_NO_PATH_PROMPT: "1",
24869
+ HERMESLINK_SKIP_RESTART: "1"
24870
+ };
24871
+ if (process.platform === "win32") {
24872
+ const windowsCommand = `& { $ErrorActionPreference = "Stop"; Invoke-RestMethod ${quotePowerShellString(installer.windowsUrl)} | Invoke-Expression }`;
24873
+ return {
24874
+ command: windowsCommand,
24875
+ displayCommand: `$env:HERMESLINK_VERSION="${targetVersion}"; $env:HERMESLINK_YES="1"; $env:HERMESLINK_NO_PATH_PROMPT="1"; $env:HERMESLINK_SKIP_RESTART="1"; ${windowsCommand}; hermeslink restart`,
24876
+ env,
24877
+ source: "official-installer",
24878
+ installerUrl: installer.windowsUrl
24879
+ };
24880
+ }
24881
+ const unixCommand = buildUnixInstallCommand(installer.unixUrl);
24882
+ return {
24883
+ command: unixCommand,
24884
+ displayCommand: `HERMESLINK_VERSION=${targetVersion} HERMESLINK_YES=1 HERMESLINK_NO_PROFILE_EDIT=1 HERMESLINK_NO_PATH_PROMPT=1 HERMESLINK_SKIP_RESTART=1 sh -c ${quoteShellToken(unixCommand)} && hermeslink restart`,
24885
+ env,
24886
+ source: "official-installer",
24887
+ installerUrl: installer.unixUrl
24888
+ };
24889
+ }
24890
+ function buildUnixInstallCommand(installerUrl) {
24891
+ const fetchScript = [
24892
+ quoteShellToken(process.execPath),
24893
+ "--input-type=module",
24894
+ "-e",
24895
+ quoteShellToken(
24896
+ "const url = process.env.HERMESLINK_INSTALLER_URL; const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); process.stdout.write(await response.text());"
24897
+ )
24898
+ ].join(" ");
24899
+ return [
24900
+ "set -e;",
24901
+ 'tmp="${TMPDIR:-/tmp}/hermespilot-link-install.$$.sh";',
24902
+ `trap 'rm -f "$tmp"' EXIT;`,
24903
+ "umask 077;",
24904
+ "if command -v curl >/dev/null 2>&1; then",
24905
+ `curl -fsSL ${quoteShellToken(installerUrl)} -o "$tmp";`,
24906
+ "else",
24907
+ `HERMESLINK_INSTALLER_URL=${quoteShellToken(installerUrl)} ${fetchScript} > "$tmp";`,
24908
+ "fi",
24909
+ 'bash "$tmp"'
24910
+ ].join(" ");
24911
+ }
24912
+ async function readOfficialInstallerUrls(options) {
24913
+ const config = await loadConfig(options.paths);
24914
+ const url = new URL(SERVER_LINK_INSTALL_SCRIPTS_PATH, config.serverBaseUrl);
24915
+ const response = await fetchInstallScriptsFromServer(
24916
+ options.fetchImpl ?? fetch,
24917
+ url
24918
+ );
24919
+ if (!response.ok) {
24920
+ throw new Error(`HermesPilot Server returned HTTP ${response.status}`);
24921
+ }
24922
+ const snapshot = await response.json();
24923
+ const commands = snapshot.commands;
24924
+ const unixUrl = readInstallerUrl(commands?.unix, "install.sh");
24925
+ const windowsUrl = readInstallerUrl(commands?.windows, "install.ps1");
24926
+ if (!unixUrl || !windowsUrl) {
24927
+ throw new Error("HermesPilot Server did not return official installer URLs");
24928
+ }
24929
+ return {
24930
+ unixUrl,
24931
+ windowsUrl
24932
+ };
24933
+ }
24934
+ function defaultInstallerUrls() {
24935
+ return {
24936
+ unixUrl: OFFICIAL_UNIX_INSTALLER_URL,
24937
+ windowsUrl: OFFICIAL_WINDOWS_INSTALLER_URL
24938
+ };
24939
+ }
24940
+ function readInstallerUrl(value, expectedFileName) {
24941
+ if (typeof value !== "string") {
24942
+ return null;
24943
+ }
24944
+ const match = /https:\/\/[^\s'"|]+/u.exec(value);
24945
+ if (!match) {
24946
+ return null;
24947
+ }
24948
+ const url = match[0];
24949
+ if (!isOfficialInstallerUrl(url, expectedFileName)) {
24950
+ return null;
24951
+ }
24952
+ return url;
24953
+ }
24954
+ function isOfficialInstallerUrl(url, expectedFileName) {
24955
+ try {
24956
+ const parsed = new URL(url);
24957
+ if (parsed.protocol !== "https:") {
24958
+ return false;
24959
+ }
24960
+ return parsed.hostname === "hs.clawpilot.me" && parsed.pathname === `/install/${expectedFileName}`;
24961
+ } catch {
24962
+ return false;
24963
+ }
24964
+ }
24965
+ async function fetchInstallScriptsFromServer(fetcher, url) {
24966
+ const controller = new AbortController();
24967
+ const timer = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
24968
+ try {
24969
+ return await fetcher(url, {
24970
+ headers: {
24971
+ accept: "application/json",
24972
+ "user-agent": `HermesPilot-Link/${LINK_VERSION}`
24973
+ },
24974
+ signal: controller.signal
24975
+ });
24976
+ } catch (error) {
24977
+ if (error instanceof Error && error.name === "AbortError") {
24978
+ throw new Error("HermesPilot Server installer config check timed out");
24979
+ }
24980
+ throw error;
24981
+ } finally {
24982
+ clearTimeout(timer);
24983
+ }
24984
+ }
24985
+ function spawnWindowsDetachedUpdater(input) {
24986
+ const child = spawn5(
24987
+ "powershell.exe",
24988
+ [
24989
+ "-NoProfile",
24990
+ "-ExecutionPolicy",
24991
+ "Bypass",
24992
+ "-Command",
24993
+ buildWindowsDetachedUpdaterScript(input)
24994
+ ],
24995
+ {
24996
+ detached: true,
24997
+ stdio: "ignore",
24998
+ cwd: process.env.SystemRoot ?? process.env.TEMP ?? process.cwd(),
24999
+ env: {
25000
+ ...process.env,
25001
+ ...input.installCommand.env
25002
+ },
25003
+ windowsHide: true,
25004
+ shell: false
25005
+ }
25006
+ );
25007
+ child.unref();
25008
+ return child;
25009
+ }
25010
+ function buildWindowsDetachedUpdaterScript(input) {
25011
+ return [
25012
+ '$ErrorActionPreference = "Stop"',
25013
+ "$Utf8NoBom = New-Object System.Text.UTF8Encoding -ArgumentList $false",
25014
+ `$NodePath = ${quotePowerShellString(process.execPath)}`,
25015
+ `$CliScriptPath = ${quotePowerShellString(currentCliScriptPath())}`,
25016
+ `$InstallerUrl = ${quotePowerShellString(input.installCommand.installerUrl)}`,
25017
+ `$StatePath = ${quotePowerShellString(input.statePath)}`,
25018
+ `$LogPath = ${quotePowerShellString(input.logPath)}`,
25019
+ `$JobId = ${quotePowerShellString(input.jobId)}`,
25020
+ `$TargetVersion = ${quotePowerShellString(input.targetVersion)}`,
25021
+ `$StartedAt = ${quotePowerShellString(input.startedAt)}`,
25022
+ `$ManualCommand = ${quotePowerShellString(input.manualCommand)}`,
25023
+ "$InstallerPath = $null",
25024
+ "function Ensure-ParentDirectory { param([string]$PathValue) $parent = Split-Path -Parent $PathValue; if ($parent) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } }",
25025
+ "function Add-UpdateLog { param([string]$Message) Ensure-ParentDirectory $LogPath; [System.IO.File]::AppendAllText($LogPath, $Message + [Environment]::NewLine, $Utf8NoBom) }",
25026
+ "function Write-UpdateState {",
25027
+ " param([string]$State, $ExitCode, $ErrorText)",
25028
+ " Ensure-ParentDirectory $StatePath",
25029
+ " $finishedAt = $null",
25030
+ ' if ($State -ne "running") { $finishedAt = (Get-Date).ToUniversalTime().ToString("o") }',
25031
+ " $payload = [ordered]@{",
25032
+ " state = $State",
25033
+ " job_id = $JobId",
25034
+ " pid = $PID",
25035
+ " target_version = $TargetVersion",
25036
+ " started_at = $StartedAt",
25037
+ " finished_at = $finishedAt",
25038
+ " exit_code = $ExitCode",
25039
+ " signal = $null",
25040
+ " error = $ErrorText",
25041
+ " manual_command = $ManualCommand",
25042
+ " }",
25043
+ " $json = $payload | ConvertTo-Json -Compress",
25044
+ " [System.IO.File]::WriteAllText($StatePath, $json, $Utf8NoBom)",
25045
+ "}",
25046
+ "function Invoke-Step {",
25047
+ " param([string]$Label, [scriptblock]$Block, [switch]$AllowFailure)",
25048
+ ' Add-UpdateLog ""',
25049
+ ' Add-UpdateLog "=> $Label"',
25050
+ " & $Block *>> $LogPath",
25051
+ " $code = if ($null -eq $LASTEXITCODE) { 0 } else { [int]$LASTEXITCODE }",
25052
+ ' if ($code -ne 0 -and -not $AllowFailure) { throw "$Label exited with code $code" }',
25053
+ " return $code",
25054
+ "}",
25055
+ "try {",
25056
+ ' Write-UpdateState "running" $null $null',
25057
+ " Start-Sleep -Milliseconds 1500",
25058
+ ' Invoke-Step "Stopping Hermes Link before Windows package replacement" { & $NodePath $CliScriptPath stop } -AllowFailure | Out-Null',
25059
+ ' $InstallerPath = Join-Path ([System.IO.Path]::GetTempPath()) ("hermespilot-link-install-" + [Guid]::NewGuid().ToString("N") + ".ps1")',
25060
+ ' Add-UpdateLog ""',
25061
+ ' Add-UpdateLog "=> Downloading official installer"',
25062
+ ' Invoke-RestMethod -Uri $InstallerUrl -Headers @{ "User-Agent" = "HermesPilot-Link-Updater" } -OutFile $InstallerPath',
25063
+ " $env:HERMESLINK_VERSION = $TargetVersion",
25064
+ ' $env:HERMESLINK_YES = "1"',
25065
+ ' $env:HERMESLINK_NO_PATH_PROMPT = "1"',
25066
+ ' $env:HERMESLINK_SKIP_RESTART = "1"',
25067
+ ' Invoke-Step "Installing Hermes Link $TargetVersion" { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $InstallerPath -Version $TargetVersion -NoPathPrompt -SkipRestart } | Out-Null',
25068
+ ' Invoke-Step "Starting Hermes Link after update" { & $NodePath $CliScriptPath start } | Out-Null',
25069
+ ' Add-UpdateLog ""',
25070
+ ' Add-UpdateLog ("=== link update finished " + (Get-Date).ToUniversalTime().ToString("o") + " exit=0 signal=null ===")',
25071
+ ' Write-UpdateState "restart_required" 0 $null',
25072
+ " exit 0",
25073
+ "} catch {",
25074
+ " $message = if ($_.Exception) { $_.Exception.Message } else { [string]$_ }",
25075
+ ' Add-UpdateLog ""',
25076
+ ' Add-UpdateLog "[failed] $message"',
25077
+ " try { & $NodePath $CliScriptPath start *>> $LogPath } catch {}",
25078
+ ' Write-UpdateState "failed" 1 $message',
25079
+ " exit 1",
25080
+ "} finally {",
25081
+ " if ($InstallerPath -and (Test-Path -LiteralPath $InstallerPath)) { Remove-Item -LiteralPath $InstallerPath -Force -ErrorAction SilentlyContinue }",
25082
+ "}"
25083
+ ].join("\n");
25084
+ }
25085
+ function spawnInstallCommand(input) {
25086
+ const env = {
25087
+ ...process.env,
25088
+ ...input.env
25089
+ };
25090
+ if (process.platform === "win32") {
25091
+ return spawn5(
25092
+ "powershell.exe",
25093
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", input.command],
25094
+ {
25095
+ env,
25096
+ stdio: ["ignore", "pipe", "pipe"],
25097
+ windowsHide: true,
25098
+ detached: false,
25099
+ shell: false
25100
+ }
25101
+ );
25102
+ }
25103
+ return spawn5("/bin/sh", ["-lc", input.command], {
25104
+ env,
25105
+ stdio: ["ignore", "pipe", "pipe"],
25106
+ windowsHide: true,
25107
+ detached: false,
25108
+ shell: false
25109
+ });
25110
+ }
24032
25111
  async function readLinkReleaseCheckContext(paths) {
24033
25112
  const config = await loadConfig(paths);
24034
25113
  const url = new URL(SERVER_LINK_CURRENT_RELEASE_PATH, config.serverBaseUrl);
@@ -24089,8 +25168,10 @@ async function clearUpdateLogFiles2(paths) {
24089
25168
  function manualInstallCommand(version) {
24090
25169
  return `npm install -g ${LINK_NPM_PACKAGE}@${version}`;
24091
25170
  }
24092
- function resolveNpmBin() {
24093
- return process.platform === "win32" ? "npm.cmd" : "npm";
25171
+ function isValidReleaseVersion(version) {
25172
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(
25173
+ version
25174
+ );
24094
25175
  }
24095
25176
  function compareSemver3(left, right) {
24096
25177
  const leftParts = parseSemver(left);
@@ -24111,6 +25192,15 @@ function parseSemver(value) {
24111
25192
  Number.parseInt(match?.[3] ?? "0", 10)
24112
25193
  ];
24113
25194
  }
25195
+ function quoteShellToken(value) {
25196
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(value)) {
25197
+ return value;
25198
+ }
25199
+ return `'${value.replaceAll("'", "'\\''")}'`;
25200
+ }
25201
+ function quotePowerShellString(value) {
25202
+ return `'${value.replaceAll("'", "''")}'`;
25203
+ }
24114
25204
  function isRecentRunningState3(state, now = Date.now()) {
24115
25205
  const startedAt = state.started_at ? Date.parse(state.started_at) : Number.NaN;
24116
25206
  return Number.isFinite(startedAt) && now - startedAt < 1e4;
@@ -25782,6 +26872,7 @@ export {
25782
26872
  readPairingClaim,
25783
26873
  clearPairingClaim,
25784
26874
  createApp,
26875
+ fetchRelayStreamBatchPolicy,
25785
26876
  connectRelayControl,
25786
26877
  reportLinkStatusToServer,
25787
26878
  startLinkService,