@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.
- package/README.md +8 -2
- package/dist/{chunk-PULX22HX.js → chunk-5JBXQ3VC.js} +1144 -53
- package/dist/cli/index.js +301 -11
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.mjs +6 -0
|
@@ -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
|
|
3062
|
+
return [provider, normalizeBaseUrl(baseUrl), modelId].join("\n").toLowerCase();
|
|
2748
3063
|
}
|
|
2749
3064
|
function modelEndpointKey(baseUrl, modelId) {
|
|
2750
|
-
return [baseUrl
|
|
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.
|
|
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
|
|
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\
|
|
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\
|
|
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
|
|
19375
|
-
configurable:
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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\
|
|
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
|
-
|
|
22297
|
-
|
|
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
|
-
},
|
|
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 >=
|
|
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
|
|
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
|
-
|
|
23771
|
-
|
|
23772
|
-
|
|
23773
|
-
|
|
23774
|
-
|
|
23775
|
-
|
|
23776
|
-
|
|
23777
|
-
|
|
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 : `
|
|
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
|
-
|
|
23889
|
-
state
|
|
23890
|
-
|
|
23891
|
-
|
|
23892
|
-
|
|
23893
|
-
|
|
23894
|
-
|
|
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
|
-
|
|
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
|
|
24093
|
-
return
|
|
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,
|