@hermespilot/link 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/{chunk-PULX22HX.js → chunk-DZMN5RIV.js} +1553 -1912
- package/dist/cli/index.js +1508 -41
- 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();
|
|
@@ -4107,7 +4425,7 @@ async function listCronOutputFiles(profileName, jobId) {
|
|
|
4107
4425
|
mtimeMs: fileStat.mtimeMs
|
|
4108
4426
|
});
|
|
4109
4427
|
}
|
|
4110
|
-
return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path:
|
|
4428
|
+
return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path26, mtime }) => ({ path: path26, mtime }));
|
|
4111
4429
|
}
|
|
4112
4430
|
async function readCronOutput(outputPath) {
|
|
4113
4431
|
const content = await readFile3(outputPath, "utf8");
|
|
@@ -4184,7 +4502,7 @@ import os2 from "os";
|
|
|
4184
4502
|
import path5 from "path";
|
|
4185
4503
|
|
|
4186
4504
|
// src/constants.ts
|
|
4187
|
-
var LINK_VERSION = "0.5.
|
|
4505
|
+
var LINK_VERSION = "0.5.2";
|
|
4188
4506
|
var LINK_COMMAND = "hermeslink";
|
|
4189
4507
|
var LINK_DEFAULT_PORT = 52379;
|
|
4190
4508
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -12677,10 +12995,10 @@ function parseHermesApiCapabilities(payload) {
|
|
|
12677
12995
|
sessionKeyHeader: readString10(features, "session_key_header")
|
|
12678
12996
|
};
|
|
12679
12997
|
}
|
|
12680
|
-
async function callHermesApi(
|
|
12998
|
+
async function callHermesApi(path26, init, options) {
|
|
12681
12999
|
const method = init.method ?? "GET";
|
|
12682
13000
|
const startedAt = Date.now();
|
|
12683
|
-
void options.logger?.debug("hermes_api_request_started", { method, path:
|
|
13001
|
+
void options.logger?.debug("hermes_api_request_started", { method, path: path26 });
|
|
12684
13002
|
const availability = await ensureHermesApiServerAvailable({
|
|
12685
13003
|
fetchImpl: options.fetchImpl,
|
|
12686
13004
|
logger: options.logger,
|
|
@@ -12688,7 +13006,7 @@ async function callHermesApi(path27, init, options) {
|
|
|
12688
13006
|
});
|
|
12689
13007
|
let config = availability.configResult.apiServer;
|
|
12690
13008
|
const fetcher = options.fetchImpl ?? fetch;
|
|
12691
|
-
const request = () => fetchHermesApi(fetcher, config,
|
|
13009
|
+
const request = () => fetchHermesApi(fetcher, config, path26, init, options);
|
|
12692
13010
|
let response;
|
|
12693
13011
|
try {
|
|
12694
13012
|
response = await request();
|
|
@@ -12696,7 +13014,7 @@ async function callHermesApi(path27, init, options) {
|
|
|
12696
13014
|
logHermesApiError(
|
|
12697
13015
|
options.logger,
|
|
12698
13016
|
method,
|
|
12699
|
-
|
|
13017
|
+
path26,
|
|
12700
13018
|
options.profileName,
|
|
12701
13019
|
startedAt,
|
|
12702
13020
|
error
|
|
@@ -12707,7 +13025,7 @@ async function callHermesApi(path27, init, options) {
|
|
|
12707
13025
|
logHermesApiResponse(
|
|
12708
13026
|
options.logger,
|
|
12709
13027
|
method,
|
|
12710
|
-
|
|
13028
|
+
path26,
|
|
12711
13029
|
options.profileName,
|
|
12712
13030
|
startedAt,
|
|
12713
13031
|
response
|
|
@@ -12716,7 +13034,7 @@ async function callHermesApi(path27, init, options) {
|
|
|
12716
13034
|
}
|
|
12717
13035
|
void options.logger?.warn("hermes_api_request_retrying_after_401", {
|
|
12718
13036
|
method,
|
|
12719
|
-
path:
|
|
13037
|
+
path: path26,
|
|
12720
13038
|
profile: options.profileName ?? "default",
|
|
12721
13039
|
port: config.port ?? null,
|
|
12722
13040
|
duration_ms: Date.now() - startedAt
|
|
@@ -12734,7 +13052,7 @@ async function callHermesApi(path27, init, options) {
|
|
|
12734
13052
|
logHermesApiError(
|
|
12735
13053
|
options.logger,
|
|
12736
13054
|
method,
|
|
12737
|
-
|
|
13055
|
+
path26,
|
|
12738
13056
|
options.profileName,
|
|
12739
13057
|
startedAt,
|
|
12740
13058
|
error
|
|
@@ -12744,7 +13062,7 @@ async function callHermesApi(path27, init, options) {
|
|
|
12744
13062
|
logHermesApiResponse(
|
|
12745
13063
|
options.logger,
|
|
12746
13064
|
method,
|
|
12747
|
-
|
|
13065
|
+
path26,
|
|
12748
13066
|
options.profileName,
|
|
12749
13067
|
startedAt,
|
|
12750
13068
|
response
|
|
@@ -12754,7 +13072,7 @@ async function callHermesApi(path27, init, options) {
|
|
|
12754
13072
|
}
|
|
12755
13073
|
void options.logger?.warn("hermes_api_request_repairing_after_401", {
|
|
12756
13074
|
method,
|
|
12757
|
-
path:
|
|
13075
|
+
path: path26,
|
|
12758
13076
|
profile: options.profileName ?? "default",
|
|
12759
13077
|
port: config.port ?? null,
|
|
12760
13078
|
duration_ms: Date.now() - startedAt
|
|
@@ -12774,7 +13092,7 @@ async function callHermesApi(path27, init, options) {
|
|
|
12774
13092
|
logHermesApiError(
|
|
12775
13093
|
options.logger,
|
|
12776
13094
|
method,
|
|
12777
|
-
|
|
13095
|
+
path26,
|
|
12778
13096
|
options.profileName,
|
|
12779
13097
|
startedAt,
|
|
12780
13098
|
error
|
|
@@ -12784,21 +13102,21 @@ async function callHermesApi(path27, init, options) {
|
|
|
12784
13102
|
logHermesApiResponse(
|
|
12785
13103
|
options.logger,
|
|
12786
13104
|
method,
|
|
12787
|
-
|
|
13105
|
+
path26,
|
|
12788
13106
|
options.profileName,
|
|
12789
13107
|
startedAt,
|
|
12790
13108
|
response
|
|
12791
13109
|
);
|
|
12792
13110
|
return response;
|
|
12793
13111
|
}
|
|
12794
|
-
async function fetchHermesApi(fetcher, config,
|
|
13112
|
+
async function fetchHermesApi(fetcher, config, path26, init, options) {
|
|
12795
13113
|
const headers = new Headers(init.headers);
|
|
12796
13114
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
12797
13115
|
if (config.key) {
|
|
12798
13116
|
headers.set("x-api-key", config.key);
|
|
12799
13117
|
headers.set("authorization", `Bearer ${config.key}`);
|
|
12800
13118
|
}
|
|
12801
|
-
return await fetcher(`http://127.0.0.1:${config.port}${
|
|
13119
|
+
return await fetcher(`http://127.0.0.1:${config.port}${path26}`, {
|
|
12802
13120
|
...init,
|
|
12803
13121
|
headers
|
|
12804
13122
|
}).catch((error) => {
|
|
@@ -12807,10 +13125,10 @@ async function fetchHermesApi(fetcher, config, path27, init, options) {
|
|
|
12807
13125
|
}
|
|
12808
13126
|
void options.logger?.warn("hermes_api_server_connect_failed", {
|
|
12809
13127
|
method: String(init.method ?? "GET").toUpperCase(),
|
|
12810
|
-
path:
|
|
13128
|
+
path: path26,
|
|
12811
13129
|
profile: options.profileName ?? "default",
|
|
12812
13130
|
port: config.port ?? null,
|
|
12813
|
-
url: `http://127.0.0.1:${config.port}${
|
|
13131
|
+
url: `http://127.0.0.1:${config.port}${path26}`,
|
|
12814
13132
|
error: error instanceof Error ? error.message : String(error)
|
|
12815
13133
|
});
|
|
12816
13134
|
throw new LinkHttpError(
|
|
@@ -12820,10 +13138,10 @@ async function fetchHermesApi(fetcher, config, path27, init, options) {
|
|
|
12820
13138
|
);
|
|
12821
13139
|
});
|
|
12822
13140
|
}
|
|
12823
|
-
function logHermesApiResponse(logger, method,
|
|
13141
|
+
function logHermesApiResponse(logger, method, path26, profileName, startedAt, response) {
|
|
12824
13142
|
const fields = {
|
|
12825
13143
|
method,
|
|
12826
|
-
path:
|
|
13144
|
+
path: path26,
|
|
12827
13145
|
profile: profileName ?? "default",
|
|
12828
13146
|
status: response.status,
|
|
12829
13147
|
duration_ms: Date.now() - startedAt
|
|
@@ -12844,10 +13162,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
|
|
|
12844
13162
|
...upstreamError ? { upstream_error: upstreamError } : {}
|
|
12845
13163
|
});
|
|
12846
13164
|
}
|
|
12847
|
-
function logHermesApiError(logger, method,
|
|
13165
|
+
function logHermesApiError(logger, method, path26, profileName, startedAt, error) {
|
|
12848
13166
|
void logger?.warn("hermes_api_request_failed", {
|
|
12849
13167
|
method,
|
|
12850
|
-
path:
|
|
13168
|
+
path: path26,
|
|
12851
13169
|
profile: profileName ?? "default",
|
|
12852
13170
|
duration_ms: Date.now() - startedAt,
|
|
12853
13171
|
...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
|
|
@@ -18228,6 +18546,22 @@ function registerModelConfigRoutes(router, options) {
|
|
|
18228
18546
|
ctx.set("cache-control", "no-store");
|
|
18229
18547
|
ctx.body = await listHermesModelConfigs();
|
|
18230
18548
|
});
|
|
18549
|
+
router.get("/api/v1/model-configs/catalog", async (ctx) => {
|
|
18550
|
+
await authenticateRequest(ctx, paths);
|
|
18551
|
+
const targetProfileName = readQueryString(ctx.query.target_profile);
|
|
18552
|
+
if (targetProfileName) {
|
|
18553
|
+
await getHermesProfileStatus(targetProfileName, paths);
|
|
18554
|
+
}
|
|
18555
|
+
ctx.set("cache-control", "no-store");
|
|
18556
|
+
const profiles = await listHermesProfiles(paths);
|
|
18557
|
+
ctx.body = await listHermesModelConfigCatalog({
|
|
18558
|
+
targetProfileName,
|
|
18559
|
+
profiles: profiles.map((profile) => ({
|
|
18560
|
+
name: profile.name,
|
|
18561
|
+
displayName: profile.displayName
|
|
18562
|
+
}))
|
|
18563
|
+
});
|
|
18564
|
+
});
|
|
18231
18565
|
router.post("/api/v1/model-configs", async (ctx) => {
|
|
18232
18566
|
await authenticateRequest(ctx, paths);
|
|
18233
18567
|
const body = await readJsonBody(ctx.req);
|
|
@@ -18291,6 +18625,23 @@ function registerModelConfigRoutes(router, options) {
|
|
|
18291
18625
|
throw toModelConfigHttpError(error);
|
|
18292
18626
|
}
|
|
18293
18627
|
});
|
|
18628
|
+
router.post("/api/v1/profiles/:name/model-configs/import", async (ctx) => {
|
|
18629
|
+
await authenticateRequest(ctx, paths);
|
|
18630
|
+
await getHermesProfileStatus(ctx.params.name, paths);
|
|
18631
|
+
const body = await readJsonBody(ctx.req);
|
|
18632
|
+
const input = readModelConfigImportInput(body);
|
|
18633
|
+
await getHermesProfileStatus(input.sourceProfileName, paths);
|
|
18634
|
+
try {
|
|
18635
|
+
const result = await importHermesModelConfig(input, ctx.params.name);
|
|
18636
|
+
ctx.body = shouldReloadGatewayAfterModelConfigChange(body) ? await reloadGatewayAfterProfileModelConfigChange(result, {
|
|
18637
|
+
paths,
|
|
18638
|
+
logger,
|
|
18639
|
+
profileName: ctx.params.name
|
|
18640
|
+
}) : markModelConfigAppliedWithoutGatewayReload(result);
|
|
18641
|
+
} catch (error) {
|
|
18642
|
+
throw toModelConfigHttpError(error);
|
|
18643
|
+
}
|
|
18644
|
+
});
|
|
18294
18645
|
router.patch("/api/v1/profiles/:name/model-configs/defaults", async (ctx) => {
|
|
18295
18646
|
await authenticateRequest(ctx, paths);
|
|
18296
18647
|
await getHermesProfileStatus(ctx.params.name, paths);
|
|
@@ -18357,6 +18708,25 @@ function readModelDefaultsInput(body) {
|
|
|
18357
18708
|
compressionModelId: readString14(body, "compression_model_id") ?? readString14(body, "compressionModelId") ?? void 0
|
|
18358
18709
|
};
|
|
18359
18710
|
}
|
|
18711
|
+
function readModelConfigImportInput(body) {
|
|
18712
|
+
const sourceProfileName = readString14(body, "source_profile") ?? readString14(body, "sourceProfile") ?? readString14(body, "source_profile_name") ?? readString14(body, "sourceProfileName");
|
|
18713
|
+
const modelId = readString14(body, "model_id") ?? readString14(body, "modelId") ?? readString14(body, "id");
|
|
18714
|
+
if (!sourceProfileName || !modelId) {
|
|
18715
|
+
throw new LinkHttpError(
|
|
18716
|
+
400,
|
|
18717
|
+
"model_import_invalid",
|
|
18718
|
+
"source_profile and model_id are required"
|
|
18719
|
+
);
|
|
18720
|
+
}
|
|
18721
|
+
return {
|
|
18722
|
+
sourceProfileName,
|
|
18723
|
+
modelId,
|
|
18724
|
+
provider: readString14(body, "provider") ?? readString14(body, "provider_key") ?? readString14(body, "providerKey") ?? void 0,
|
|
18725
|
+
baseUrl: readString14(body, "base_url") ?? readString14(body, "baseUrl") ?? void 0,
|
|
18726
|
+
apiMode: readString14(body, "api_mode") ?? readString14(body, "apiMode") ?? void 0,
|
|
18727
|
+
setDefault: readBoolean3(body.set_default ?? body.setDefault)
|
|
18728
|
+
};
|
|
18729
|
+
}
|
|
18360
18730
|
function shouldReloadGatewayAfterModelConfigChange(body) {
|
|
18361
18731
|
const explicit = readBoolean3(body.reload_gateway ?? body.reloadGateway) ?? (readBoolean3(body.skip_gateway_reload ?? body.skipGatewayReload) === true ? false : void 0);
|
|
18362
18732
|
return explicit ?? true;
|
|
@@ -19312,6 +19682,7 @@ var CUSTOM_PROVIDER_CARD_ID = "__custom__";
|
|
|
19312
19682
|
var CUSTOM_PROVIDER_REGISTRY_FILE = "memory-providers.json";
|
|
19313
19683
|
var HINDSIGHT_DEFAULT_API_URL = "https://api.hindsight.vectorize.io";
|
|
19314
19684
|
var HINDSIGHT_DEFAULT_LOCAL_URL = "http://localhost:8888";
|
|
19685
|
+
var MEMORY_PROVIDER_TEST_TIMEOUT_MS = 4e3;
|
|
19315
19686
|
var OPENVIKING_DEFAULT_ENDPOINT = "http://127.0.0.1:1933";
|
|
19316
19687
|
var RETAINDB_DEFAULT_BASE_URL = "https://api.retaindb.com";
|
|
19317
19688
|
var HINDSIGHT_LLM_PROVIDERS = [
|
|
@@ -19335,25 +19706,25 @@ var MEMORY_PROVIDER_CATALOG = [
|
|
|
19335
19706
|
{
|
|
19336
19707
|
id: "honcho",
|
|
19337
19708
|
label: "Honcho",
|
|
19338
|
-
description: "AI-native cross-session user modeling provider\uFF1B\u8FD9\u91CC\u53EF\u7F16\u8F91 workspace\u3001peer \u4E0E\u53EC\u56DE\u7B56\u7565\u3002",
|
|
19709
|
+
description: "AI-native cross-session user modeling provider\uFF1B\u8FD9\u91CC\u53EF\u586B\u5199 API Key\uFF0C\u5E76\u7F16\u8F91 workspace\u3001peer \u4E0E\u53EC\u56DE\u7B56\u7565\u3002",
|
|
19339
19710
|
configurable: true
|
|
19340
19711
|
},
|
|
19341
19712
|
{
|
|
19342
19713
|
id: "openviking",
|
|
19343
19714
|
label: "OpenViking",
|
|
19344
|
-
description: "\u4E0A\u4E0B\u6587\u6570\u636E\u5E93\u4E0E\u5C42\u7EA7\u77E5\u8BC6\u5E93 provider\uFF1B\u8FD9\u91CC\u53EF\u7F16\u8F91
|
|
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,30 +20995,119 @@ function normalizeHindsightMode(value) {
|
|
|
20455
20995
|
const mode = readString15(value) ?? "cloud";
|
|
20456
20996
|
return mode === "local" ? "local_embedded" : mode;
|
|
20457
20997
|
}
|
|
20458
|
-
|
|
20459
|
-
|
|
20460
|
-
|
|
20461
|
-
"
|
|
20462
|
-
|
|
20463
|
-
if (isNodeError16(error, "ENOENT")) {
|
|
20464
|
-
return "";
|
|
20465
|
-
}
|
|
20466
|
-
throw error;
|
|
20467
|
-
});
|
|
20468
|
-
const config = raw ? toRecord14(YAML4.parse(raw)) : {};
|
|
20469
|
-
const memory = toRecord14(config.memory);
|
|
20470
|
-
const provider = readString15(memory.provider);
|
|
20471
|
-
if (!provider || provider === "built-in" || provider === "builtin" || provider === "built_in") {
|
|
20998
|
+
function normalizeHttpUrl(value) {
|
|
20999
|
+
try {
|
|
21000
|
+
const url = new URL(value);
|
|
21001
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url : null;
|
|
21002
|
+
} catch {
|
|
20472
21003
|
return null;
|
|
20473
21004
|
}
|
|
20474
|
-
return provider;
|
|
20475
21005
|
}
|
|
20476
|
-
|
|
20477
|
-
const
|
|
20478
|
-
|
|
20479
|
-
|
|
20480
|
-
|
|
20481
|
-
|
|
21006
|
+
function hindsightRequestHeaders(apiKey) {
|
|
21007
|
+
const headers = { accept: "application/json" };
|
|
21008
|
+
if (isConfiguredEnvValue(apiKey)) {
|
|
21009
|
+
const secret = String(apiKey).trim();
|
|
21010
|
+
headers.authorization = `Bearer ${secret}`;
|
|
21011
|
+
headers["x-api-key"] = secret;
|
|
21012
|
+
}
|
|
21013
|
+
return headers;
|
|
21014
|
+
}
|
|
21015
|
+
async function probeHindsightJson(baseUrl, pathName, headers) {
|
|
21016
|
+
const url = joinHindsightUrl(baseUrl, pathName);
|
|
21017
|
+
const controller = new AbortController();
|
|
21018
|
+
const timer = setTimeout(
|
|
21019
|
+
() => controller.abort(),
|
|
21020
|
+
MEMORY_PROVIDER_TEST_TIMEOUT_MS
|
|
21021
|
+
);
|
|
21022
|
+
try {
|
|
21023
|
+
const response = await fetch(url, {
|
|
21024
|
+
headers,
|
|
21025
|
+
signal: controller.signal
|
|
21026
|
+
});
|
|
21027
|
+
const text = await response.text();
|
|
21028
|
+
const json = parseJsonObject2(text);
|
|
21029
|
+
if (!response.ok) {
|
|
21030
|
+
return {
|
|
21031
|
+
ok: false,
|
|
21032
|
+
detail: `HTTP ${response.status}${readHindsightError(json)}`
|
|
21033
|
+
};
|
|
21034
|
+
}
|
|
21035
|
+
const semanticIssue = hindsightSemanticIssue(pathName, json);
|
|
21036
|
+
if (semanticIssue) {
|
|
21037
|
+
return { ok: false, detail: semanticIssue };
|
|
21038
|
+
}
|
|
21039
|
+
return { ok: true, detail: summarizeHindsightProbe(pathName, json) };
|
|
21040
|
+
} catch (error) {
|
|
21041
|
+
return {
|
|
21042
|
+
ok: false,
|
|
21043
|
+
detail: error instanceof Error && error.name === "AbortError" ? "\u8BF7\u6C42\u8D85\u65F6" : error instanceof Error ? error.message : "\u8BF7\u6C42\u5931\u8D25"
|
|
21044
|
+
};
|
|
21045
|
+
} finally {
|
|
21046
|
+
clearTimeout(timer);
|
|
21047
|
+
}
|
|
21048
|
+
}
|
|
21049
|
+
function joinHindsightUrl(baseUrl, pathName) {
|
|
21050
|
+
const base = new URL(baseUrl.toString());
|
|
21051
|
+
if (!base.pathname.endsWith("/")) {
|
|
21052
|
+
base.pathname = `${base.pathname}/`;
|
|
21053
|
+
}
|
|
21054
|
+
return new URL(pathName.replace(/^\/+/u, ""), base);
|
|
21055
|
+
}
|
|
21056
|
+
function parseJsonObject2(text) {
|
|
21057
|
+
try {
|
|
21058
|
+
return toRecord14(JSON.parse(text));
|
|
21059
|
+
} catch {
|
|
21060
|
+
return {};
|
|
21061
|
+
}
|
|
21062
|
+
}
|
|
21063
|
+
function readHindsightError(json) {
|
|
21064
|
+
const detail = readString15(json.detail) ?? readString15(json.error);
|
|
21065
|
+
return detail ? `\uFF1A${detail}` : "";
|
|
21066
|
+
}
|
|
21067
|
+
function hindsightSemanticIssue(pathName, json) {
|
|
21068
|
+
if (pathName === "/health") {
|
|
21069
|
+
const status = readString15(json.status);
|
|
21070
|
+
return status && ["healthy", "ok"].includes(status.toLowerCase()) ? null : `\u5065\u5EB7\u72B6\u6001\u5F02\u5E38\uFF1A${status ?? "unknown"}`;
|
|
21071
|
+
}
|
|
21072
|
+
return null;
|
|
21073
|
+
}
|
|
21074
|
+
function summarizeHindsightProbe(pathName, json) {
|
|
21075
|
+
if (pathName === "/health") {
|
|
21076
|
+
const status = readString15(json.status) ?? "ok";
|
|
21077
|
+
const database = readString15(json.database);
|
|
21078
|
+
return database ? `${status}, database ${database}` : status;
|
|
21079
|
+
}
|
|
21080
|
+
if (pathName === "/version") {
|
|
21081
|
+
const version = readString15(json.api_version);
|
|
21082
|
+
return version ? `API ${version}` : "version endpoint reachable";
|
|
21083
|
+
}
|
|
21084
|
+
const bankId = readString15(json.bank_id);
|
|
21085
|
+
return bankId ? `bank ${bankId} reachable` : "bank config reachable";
|
|
21086
|
+
}
|
|
21087
|
+
async function readActiveMemoryProvider(profileName) {
|
|
21088
|
+
const raw = await readFile14(
|
|
21089
|
+
resolveHermesConfigPath(profileName),
|
|
21090
|
+
"utf8"
|
|
21091
|
+
).catch((error) => {
|
|
21092
|
+
if (isNodeError16(error, "ENOENT")) {
|
|
21093
|
+
return "";
|
|
21094
|
+
}
|
|
21095
|
+
throw error;
|
|
21096
|
+
});
|
|
21097
|
+
const config = raw ? toRecord14(YAML4.parse(raw)) : {};
|
|
21098
|
+
const memory = toRecord14(config.memory);
|
|
21099
|
+
const provider = readString15(memory.provider);
|
|
21100
|
+
if (!provider || provider === "built-in" || provider === "builtin" || provider === "built_in") {
|
|
21101
|
+
return null;
|
|
21102
|
+
}
|
|
21103
|
+
return provider;
|
|
21104
|
+
}
|
|
21105
|
+
async function patchJsonProviderConfig(profileName, relativePath, patch) {
|
|
21106
|
+
const configPath = path21.join(
|
|
21107
|
+
resolveHermesProfileDir(profileName),
|
|
21108
|
+
relativePath
|
|
21109
|
+
);
|
|
21110
|
+
const current = await readJsonObject(configPath);
|
|
20482
21111
|
const next = { ...current };
|
|
20483
21112
|
for (const [key, value] of Object.entries(patch)) {
|
|
20484
21113
|
if (value !== void 0) {
|
|
@@ -20529,6 +21158,16 @@ function stringSetting(key, label, value, editable = true) {
|
|
|
20529
21158
|
kind: "string"
|
|
20530
21159
|
};
|
|
20531
21160
|
}
|
|
21161
|
+
function secretSetting(key, label, value, configured) {
|
|
21162
|
+
return {
|
|
21163
|
+
key,
|
|
21164
|
+
label,
|
|
21165
|
+
value: "",
|
|
21166
|
+
editable: true,
|
|
21167
|
+
kind: "secret",
|
|
21168
|
+
configured: configured || isConfiguredEnvValue(readString15(value))
|
|
21169
|
+
};
|
|
21170
|
+
}
|
|
20532
21171
|
function textSetting(key, label, value, editable = true) {
|
|
20533
21172
|
return {
|
|
20534
21173
|
key,
|
|
@@ -20752,6 +21391,23 @@ function registerProfileMemoryRoutes(router, options) {
|
|
|
20752
21391
|
}
|
|
20753
21392
|
}
|
|
20754
21393
|
);
|
|
21394
|
+
router.post(
|
|
21395
|
+
"/api/v1/profiles/:name/memory/providers/:provider/test",
|
|
21396
|
+
async (ctx) => {
|
|
21397
|
+
await authenticateRequest(ctx, paths);
|
|
21398
|
+
const body = await readJsonBody(ctx.req);
|
|
21399
|
+
await getHermesProfileStatus(ctx.params.name, paths);
|
|
21400
|
+
try {
|
|
21401
|
+
ctx.body = await testHermesMemoryProviderSettings(
|
|
21402
|
+
ctx.params.name,
|
|
21403
|
+
ctx.params.provider,
|
|
21404
|
+
readMemorySettingsPatch(body)
|
|
21405
|
+
);
|
|
21406
|
+
} catch (error) {
|
|
21407
|
+
throw toMemoryHttpError(error);
|
|
21408
|
+
}
|
|
21409
|
+
}
|
|
21410
|
+
);
|
|
20755
21411
|
}
|
|
20756
21412
|
function readMemoryTarget(body) {
|
|
20757
21413
|
const raw = readString14(body, "target");
|
|
@@ -20818,6 +21474,10 @@ function readMemorySettingsPatch(body) {
|
|
|
20818
21474
|
if (apiUrl !== void 0) {
|
|
20819
21475
|
input.apiUrl = apiUrl;
|
|
20820
21476
|
}
|
|
21477
|
+
const apiKey = readOptionalSecretString(body, "api_key", "apiKey");
|
|
21478
|
+
if (apiKey !== void 0) {
|
|
21479
|
+
input.apiKey = apiKey;
|
|
21480
|
+
}
|
|
20821
21481
|
const bankId = readOptionalString(body, "bank_id", "bankId");
|
|
20822
21482
|
if (bankId !== void 0) {
|
|
20823
21483
|
input.bankId = bankId;
|
|
@@ -20834,6 +21494,14 @@ function readMemorySettingsPatch(body) {
|
|
|
20834
21494
|
if (llmBaseUrl !== void 0) {
|
|
20835
21495
|
input.llmBaseUrl = llmBaseUrl;
|
|
20836
21496
|
}
|
|
21497
|
+
const llmApiKey = readOptionalSecretString(
|
|
21498
|
+
body,
|
|
21499
|
+
"llm_api_key",
|
|
21500
|
+
"llmApiKey"
|
|
21501
|
+
);
|
|
21502
|
+
if (llmApiKey !== void 0) {
|
|
21503
|
+
input.llmApiKey = llmApiKey;
|
|
21504
|
+
}
|
|
20837
21505
|
const containerTag = readOptionalString(body, "container_tag", "containerTag");
|
|
20838
21506
|
if (containerTag !== void 0) {
|
|
20839
21507
|
input.containerTag = containerTag;
|
|
@@ -21034,6 +21702,10 @@ function readOptionalString(body, ...keys) {
|
|
|
21034
21702
|
}
|
|
21035
21703
|
return void 0;
|
|
21036
21704
|
}
|
|
21705
|
+
function readOptionalSecretString(body, ...keys) {
|
|
21706
|
+
const value = readOptionalString(body, ...keys);
|
|
21707
|
+
return value && value.trim().length > 0 ? value : void 0;
|
|
21708
|
+
}
|
|
21037
21709
|
function toMemoryHttpError(error) {
|
|
21038
21710
|
if (error instanceof HermesMemoryError) {
|
|
21039
21711
|
return new LinkHttpError(400, error.code, error.message);
|
|
@@ -22277,1959 +22949,929 @@ function readString17(payload, key) {
|
|
|
22277
22949
|
}
|
|
22278
22950
|
|
|
22279
22951
|
// src/link/updates.ts
|
|
22280
|
-
import { spawn as spawn5 } from "child_process";
|
|
22281
|
-
import { EventEmitter as EventEmitter4 } from "events";
|
|
22282
|
-
import { mkdir as mkdir15, readFile as readFile18, rm as rm10 } from "fs/promises";
|
|
22283
|
-
import path25 from "path";
|
|
22284
|
-
|
|
22285
|
-
// src/daemon/process.ts
|
|
22286
22952
|
import { spawn as spawn4 } from "child_process";
|
|
22287
|
-
import {
|
|
22953
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
22954
|
+
import { mkdir as mkdir13, readFile as readFile17, rm as rm8 } from "fs/promises";
|
|
22288
22955
|
import path24 from "path";
|
|
22289
|
-
|
|
22290
|
-
|
|
22291
|
-
|
|
22292
|
-
|
|
22293
|
-
|
|
22294
|
-
|
|
22295
|
-
|
|
22296
|
-
var
|
|
22297
|
-
var
|
|
22298
|
-
|
|
22299
|
-
|
|
22300
|
-
|
|
22301
|
-
|
|
22302
|
-
const
|
|
22303
|
-
const
|
|
22304
|
-
const
|
|
22305
|
-
|
|
22306
|
-
let closedByUser = false;
|
|
22307
|
-
let socket = null;
|
|
22308
|
-
let retryTimer = null;
|
|
22309
|
-
let abortControllers = /* @__PURE__ */ new Map();
|
|
22310
|
-
let fatalRelayRejection = null;
|
|
22311
|
-
let latestNetworkRoutes = null;
|
|
22312
|
-
const connect = () => {
|
|
22313
|
-
options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
|
|
22314
|
-
fatalRelayRejection = null;
|
|
22315
|
-
socket = new WebSocket(wsUrl, {
|
|
22316
|
-
headers: {
|
|
22317
|
-
"x-hermes-link-version": LINK_VERSION
|
|
22318
|
-
}
|
|
22319
|
-
});
|
|
22320
|
-
socket.on("open", () => {
|
|
22321
|
-
reconnectAttempts = 0;
|
|
22322
|
-
options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
|
|
22323
|
-
const currentSocket = socket;
|
|
22324
|
-
if (currentSocket && latestNetworkRoutes) {
|
|
22325
|
-
sendNetworkRoutes(currentSocket, options.linkId, latestNetworkRoutes);
|
|
22326
|
-
}
|
|
22327
|
-
});
|
|
22328
|
-
socket.on("message", (raw) => {
|
|
22329
|
-
if (!socket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
|
|
22330
|
-
return;
|
|
22331
|
-
}
|
|
22332
|
-
void handleFrame(socket, String(raw), options.localPort, abortControllers).catch((error) => {
|
|
22333
|
-
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
22334
|
-
socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
|
|
22335
|
-
});
|
|
22336
|
-
});
|
|
22337
|
-
socket.on("error", (error) => {
|
|
22338
|
-
const message = error instanceof Error ? error.message : "Relay websocket error";
|
|
22339
|
-
fatalRelayRejection = resolveFatalRelayRejection(message);
|
|
22340
|
-
options.onStatus?.({
|
|
22341
|
-
state: "disconnected",
|
|
22342
|
-
attempt: reconnectAttempts,
|
|
22343
|
-
message: fatalRelayRejection ?? message
|
|
22344
|
-
});
|
|
22345
|
-
});
|
|
22346
|
-
socket.on("close", () => {
|
|
22347
|
-
abortAll(abortControllers);
|
|
22348
|
-
abortControllers = /* @__PURE__ */ new Map();
|
|
22349
|
-
if (fatalRelayRejection) {
|
|
22350
|
-
options.onStatus?.({
|
|
22351
|
-
state: "failed",
|
|
22352
|
-
attempt: reconnectAttempts,
|
|
22353
|
-
message: fatalRelayRejection
|
|
22354
|
-
});
|
|
22355
|
-
return;
|
|
22356
|
-
}
|
|
22357
|
-
if (closedByUser) {
|
|
22358
|
-
options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
|
|
22359
|
-
return;
|
|
22360
|
-
}
|
|
22361
|
-
if (reconnectAttempts >= maxReconnectAttempts) {
|
|
22362
|
-
options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
|
|
22363
|
-
return;
|
|
22364
|
-
}
|
|
22365
|
-
reconnectAttempts += 1;
|
|
22366
|
-
const delay3 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
|
|
22367
|
-
options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay3}ms` });
|
|
22368
|
-
retryTimer = setTimeout(connect, delay3);
|
|
22369
|
-
retryTimer.unref?.();
|
|
22370
|
-
});
|
|
22371
|
-
};
|
|
22372
|
-
connect();
|
|
22956
|
+
var SERVER_LINK_CURRENT_RELEASE_PATH = "/api/v1/link/releases/current";
|
|
22957
|
+
var LINK_NPM_PACKAGE = "@hermespilot/link";
|
|
22958
|
+
var OFFICIAL_INSTALLER_BASE_URL = "https://raw.githubusercontent.com/HangbinYang/hermespilot-install/main";
|
|
22959
|
+
var OFFICIAL_UNIX_INSTALLER_URL = `${OFFICIAL_INSTALLER_BASE_URL}/install.sh`;
|
|
22960
|
+
var OFFICIAL_WINDOWS_INSTALLER_URL = `${OFFICIAL_INSTALLER_BASE_URL}/install.ps1`;
|
|
22961
|
+
var UPDATE_LOG_FILE2 = "link-update.log";
|
|
22962
|
+
var UPDATE_LOG_MAX_FILES2 = 3;
|
|
22963
|
+
var UPDATE_FETCH_TIMEOUT_MS = 5e3;
|
|
22964
|
+
var MAX_UPDATE_LOG_LINES2 = 240;
|
|
22965
|
+
var MAX_OUTPUT_LINE_LENGTH3 = 1200;
|
|
22966
|
+
var updateEvents2 = new EventEmitter4();
|
|
22967
|
+
var runningUpdate2 = null;
|
|
22968
|
+
async function readLinkUpdateCheck(options) {
|
|
22969
|
+
const remoteResult = await readRemoteLinkPolicy(options);
|
|
22970
|
+
const remote = remoteResult.remote;
|
|
22971
|
+
const state = computeLinkUpdateState(LINK_VERSION, remote);
|
|
22972
|
+
const targetVersion = remote?.target_version ?? null;
|
|
22373
22973
|
return {
|
|
22374
|
-
|
|
22375
|
-
|
|
22376
|
-
|
|
22377
|
-
|
|
22378
|
-
}
|
|
22974
|
+
ok: true,
|
|
22975
|
+
local: {
|
|
22976
|
+
version: LINK_VERSION,
|
|
22977
|
+
raw: LINK_VERSION
|
|
22379
22978
|
},
|
|
22380
|
-
|
|
22381
|
-
|
|
22382
|
-
|
|
22383
|
-
|
|
22384
|
-
|
|
22385
|
-
|
|
22386
|
-
|
|
22387
|
-
|
|
22979
|
+
remote,
|
|
22980
|
+
state,
|
|
22981
|
+
update_available: state === "update_available" || state === "unsafe" || state === "blocked",
|
|
22982
|
+
unsafe: state === "unsafe",
|
|
22983
|
+
blocked: state === "blocked",
|
|
22984
|
+
check_state: remoteResult.state,
|
|
22985
|
+
issue: remoteResult.issue,
|
|
22986
|
+
manual: {
|
|
22987
|
+
command: targetVersion ? manualInstallCommand(targetVersion) : null,
|
|
22988
|
+
package: LINK_NPM_PACKAGE,
|
|
22989
|
+
version: targetVersion
|
|
22388
22990
|
}
|
|
22389
22991
|
};
|
|
22390
22992
|
}
|
|
22391
|
-
function
|
|
22392
|
-
|
|
22393
|
-
|
|
22394
|
-
|
|
22395
|
-
payload: {
|
|
22396
|
-
link_id: linkId,
|
|
22397
|
-
lan_ips: routes.lanIps,
|
|
22398
|
-
public_ipv4s: routes.publicIpv4s,
|
|
22399
|
-
public_ipv6s: routes.publicIpv6s,
|
|
22400
|
-
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
22401
|
-
}
|
|
22402
|
-
}));
|
|
22403
|
-
}
|
|
22404
|
-
function resolveFatalRelayRejection(message) {
|
|
22405
|
-
if (!/Unexpected server response:\s*(400|401|403|426)\b/u.test(message)) {
|
|
22406
|
-
return null;
|
|
22993
|
+
async function startLinkUpdate(options) {
|
|
22994
|
+
const current = await readLinkUpdateStatus(options.paths);
|
|
22995
|
+
if (runningUpdate2 || current.state === "running") {
|
|
22996
|
+
return current;
|
|
22407
22997
|
}
|
|
22408
|
-
|
|
22409
|
-
|
|
22410
|
-
|
|
22411
|
-
|
|
22412
|
-
|
|
22998
|
+
const check = await readLinkUpdateCheck(options);
|
|
22999
|
+
const targetVersion = check.remote?.target_version ?? null;
|
|
23000
|
+
if (!targetVersion) {
|
|
23001
|
+
return writeFailedStartState(
|
|
23002
|
+
options,
|
|
23003
|
+
"HermesPilot Server has no Link target version."
|
|
23004
|
+
);
|
|
22413
23005
|
}
|
|
22414
|
-
|
|
22415
|
-
|
|
22416
|
-
|
|
22417
|
-
|
|
22418
|
-
|
|
22419
|
-
|
|
22420
|
-
}
|
|
22421
|
-
async function handleFrame(socket, raw, localPort, abortControllers) {
|
|
22422
|
-
const frame = JSON.parse(raw);
|
|
22423
|
-
if (frame.type === "http.cancel") {
|
|
22424
|
-
abortControllers.get(frame.id)?.abort();
|
|
22425
|
-
abortControllers.delete(frame.id);
|
|
22426
|
-
return;
|
|
23006
|
+
if (!isValidReleaseVersion(targetVersion)) {
|
|
23007
|
+
return writeFailedStartState(
|
|
23008
|
+
options,
|
|
23009
|
+
`HermesPilot Server returned invalid Link target version: ${targetVersion}.`,
|
|
23010
|
+
targetVersion
|
|
23011
|
+
);
|
|
22427
23012
|
}
|
|
22428
|
-
if (
|
|
22429
|
-
return
|
|
23013
|
+
if (options.targetVersion && options.targetVersion !== targetVersion) {
|
|
23014
|
+
return writeFailedStartState(
|
|
23015
|
+
options,
|
|
23016
|
+
`Requested target ${options.targetVersion} does not match current Link target ${targetVersion}.`,
|
|
23017
|
+
targetVersion
|
|
23018
|
+
);
|
|
22430
23019
|
}
|
|
22431
|
-
|
|
22432
|
-
|
|
22433
|
-
|
|
22434
|
-
|
|
22435
|
-
|
|
22436
|
-
|
|
22437
|
-
headers: frame.headers ?? {},
|
|
22438
|
-
body: frame.bodyBase64 ? Buffer.from(frame.bodyBase64, "base64") : void 0,
|
|
22439
|
-
signal: abortController.signal
|
|
22440
|
-
});
|
|
22441
|
-
const headers = Object.fromEntries(response.headers.entries());
|
|
22442
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
22443
|
-
if (response.body && contentType.includes("text/event-stream")) {
|
|
22444
|
-
socket.send(JSON.stringify({ type: "http.stream.start", id: frame.id, status: response.status, headers }));
|
|
22445
|
-
sseBatcher = createRelayStreamChunkBatcher(socket, frame.id);
|
|
22446
|
-
const reader = response.body.getReader();
|
|
22447
|
-
while (true) {
|
|
22448
|
-
const next = await reader.read();
|
|
22449
|
-
if (next.done) {
|
|
22450
|
-
break;
|
|
22451
|
-
}
|
|
22452
|
-
sseBatcher.push(next.value);
|
|
22453
|
-
}
|
|
22454
|
-
sseBatcher.flush();
|
|
22455
|
-
socket.send(JSON.stringify({ type: "http.stream.end", id: frame.id }));
|
|
22456
|
-
return;
|
|
22457
|
-
}
|
|
22458
|
-
const body = Buffer.from(await response.arrayBuffer()).toString("base64");
|
|
22459
|
-
socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
|
|
22460
|
-
} catch (error) {
|
|
22461
|
-
if (abortController.signal.aborted || isAbortError2(error)) {
|
|
22462
|
-
return;
|
|
22463
|
-
}
|
|
22464
|
-
sseBatcher?.flush();
|
|
22465
|
-
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
22466
|
-
socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
|
|
22467
|
-
} finally {
|
|
22468
|
-
sseBatcher?.dispose();
|
|
22469
|
-
abortControllers.delete(frame.id);
|
|
23020
|
+
if (check.state === "current") {
|
|
23021
|
+
return writeFailedStartState(
|
|
23022
|
+
options,
|
|
23023
|
+
"Hermes Link is already on the current version.",
|
|
23024
|
+
targetVersion
|
|
23025
|
+
);
|
|
22470
23026
|
}
|
|
22471
|
-
|
|
22472
|
-
|
|
22473
|
-
|
|
22474
|
-
|
|
22475
|
-
|
|
22476
|
-
|
|
22477
|
-
|
|
22478
|
-
|
|
22479
|
-
|
|
22480
|
-
|
|
22481
|
-
|
|
22482
|
-
|
|
22483
|
-
|
|
22484
|
-
|
|
22485
|
-
|
|
22486
|
-
|
|
22487
|
-
|
|
22488
|
-
|
|
22489
|
-
|
|
22490
|
-
|
|
22491
|
-
|
|
22492
|
-
|
|
22493
|
-
|
|
22494
|
-
if (socket.readyState !== WebSocket.OPEN) {
|
|
22495
|
-
return;
|
|
22496
|
-
}
|
|
22497
|
-
socket.send(JSON.stringify({ type: "http.stream.chunk", id, bodyBase64 }));
|
|
22498
|
-
};
|
|
22499
|
-
const scheduleFlush = () => {
|
|
22500
|
-
if (flushTimer != null) {
|
|
22501
|
-
return;
|
|
22502
|
-
}
|
|
22503
|
-
flushTimer = setTimeout(() => {
|
|
22504
|
-
flushTimer = null;
|
|
22505
|
-
flush();
|
|
22506
|
-
}, RELAY_SSE_BATCH_FLUSH_INTERVAL_MS);
|
|
22507
|
-
flushTimer.unref?.();
|
|
23027
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
23028
|
+
const jobId = `link_update_${now().getTime().toString(36)}`;
|
|
23029
|
+
await clearUpdateLogFiles2(options.paths);
|
|
23030
|
+
const writer = createRotatingTextLogWriter({
|
|
23031
|
+
paths: options.paths,
|
|
23032
|
+
fileName: UPDATE_LOG_FILE2,
|
|
23033
|
+
maxFileBytes: 512 * 1024,
|
|
23034
|
+
maxFiles: UPDATE_LOG_MAX_FILES2
|
|
23035
|
+
});
|
|
23036
|
+
const startedAt = now().toISOString();
|
|
23037
|
+
const installCommand = buildOfficialInstallCommand(targetVersion);
|
|
23038
|
+
const manualCommand = installCommand.displayCommand;
|
|
23039
|
+
const started = {
|
|
23040
|
+
state: "running",
|
|
23041
|
+
job_id: jobId,
|
|
23042
|
+
pid: null,
|
|
23043
|
+
target_version: targetVersion,
|
|
23044
|
+
started_at: startedAt,
|
|
23045
|
+
finished_at: null,
|
|
23046
|
+
exit_code: null,
|
|
23047
|
+
signal: null,
|
|
23048
|
+
error: null,
|
|
23049
|
+
manual_command: manualCommand
|
|
22508
23050
|
};
|
|
22509
|
-
|
|
22510
|
-
|
|
22511
|
-
|
|
22512
|
-
|
|
22513
|
-
|
|
22514
|
-
|
|
22515
|
-
|
|
22516
|
-
|
|
22517
|
-
|
|
22518
|
-
|
|
22519
|
-
|
|
22520
|
-
|
|
22521
|
-
|
|
22522
|
-
|
|
22523
|
-
|
|
22524
|
-
dispose() {
|
|
22525
|
-
clearFlushTimer();
|
|
22526
|
-
chunks = [];
|
|
22527
|
-
totalBytes = 0;
|
|
22528
|
-
}
|
|
23051
|
+
await mkdir13(options.paths.runDir, { recursive: true, mode: 448 });
|
|
23052
|
+
await writer.write(
|
|
23053
|
+
`
|
|
23054
|
+
=== link update started ${startedAt} target=${targetVersion} ===
|
|
23055
|
+
`
|
|
23056
|
+
);
|
|
23057
|
+
await writer.write(`$ ${manualCommand}
|
|
23058
|
+
`);
|
|
23059
|
+
await writeUpdateState2(options.paths, started);
|
|
23060
|
+
const child = spawnInstallCommand(installCommand);
|
|
23061
|
+
started.pid = child.pid ?? null;
|
|
23062
|
+
await writeUpdateState2(options.paths, started);
|
|
23063
|
+
const appendChunk = async (chunk) => {
|
|
23064
|
+
await writer.write(chunk);
|
|
23065
|
+
await emitUpdateStatus2(options.paths);
|
|
22529
23066
|
};
|
|
22530
|
-
|
|
22531
|
-
|
|
22532
|
-
// src/runtime/system-info.ts
|
|
22533
|
-
import { execFileSync } from "child_process";
|
|
22534
|
-
import { readFileSync } from "fs";
|
|
22535
|
-
import os4 from "os";
|
|
22536
|
-
function readLinkSystemInfo() {
|
|
22537
|
-
const platform = process.platform;
|
|
22538
|
-
const hostname = readHostname(platform);
|
|
22539
|
-
const osLabel = readOsLabel(platform);
|
|
22540
|
-
const defaultDisplayName = buildDefaultDisplayName({ hostname, osLabel, platform });
|
|
22541
|
-
return {
|
|
22542
|
-
platform,
|
|
22543
|
-
hostname,
|
|
22544
|
-
osLabel,
|
|
22545
|
-
defaultDisplayName
|
|
22546
|
-
};
|
|
22547
|
-
}
|
|
22548
|
-
function buildDefaultDisplayName(input) {
|
|
22549
|
-
const hostname = normalizeText(input.hostname);
|
|
22550
|
-
const osLabel = normalizeText(input.osLabel);
|
|
22551
|
-
if (hostname) {
|
|
22552
|
-
return truncateText(hostname, 128);
|
|
22553
|
-
}
|
|
22554
|
-
return truncateText(hostname ?? osLabel ?? `Hermes Link ${input.platform}`, 128);
|
|
22555
|
-
}
|
|
22556
|
-
function parseLinuxOsRelease(content) {
|
|
22557
|
-
const values = /* @__PURE__ */ new Map();
|
|
22558
|
-
for (const line of content.split(/\r?\n/u)) {
|
|
22559
|
-
const match = /^([A-Z0-9_]+)=(.*)$/u.exec(line.trim());
|
|
22560
|
-
if (!match) {
|
|
22561
|
-
continue;
|
|
22562
|
-
}
|
|
22563
|
-
values.set(match[1], unquoteOsReleaseValue(match[2]));
|
|
22564
|
-
}
|
|
22565
|
-
return normalizeText(values.get("PRETTY_NAME")) ?? buildLinuxName(values);
|
|
22566
|
-
}
|
|
22567
|
-
function readHostname(platform) {
|
|
22568
|
-
if (platform === "darwin") {
|
|
22569
|
-
const computerName = readCommandOutput("scutil", ["--get", "ComputerName"]);
|
|
22570
|
-
if (computerName) {
|
|
22571
|
-
return computerName;
|
|
22572
|
-
}
|
|
22573
|
-
}
|
|
22574
|
-
return normalizeText(os4.hostname());
|
|
22575
|
-
}
|
|
22576
|
-
function readOsLabel(platform) {
|
|
22577
|
-
if (platform === "darwin") {
|
|
22578
|
-
const version = readCommandOutput("sw_vers", ["-productVersion"]);
|
|
22579
|
-
return version ? `macOS ${version}` : "macOS";
|
|
22580
|
-
}
|
|
22581
|
-
if (platform === "linux") {
|
|
22582
|
-
return readLinuxOsRelease() ?? `Linux ${os4.release()}`;
|
|
22583
|
-
}
|
|
22584
|
-
if (platform === "win32") {
|
|
22585
|
-
return `Windows ${os4.release()}`;
|
|
22586
|
-
}
|
|
22587
|
-
return `${os4.type()} ${os4.release()}`.trim();
|
|
22588
|
-
}
|
|
22589
|
-
function readLinuxOsRelease() {
|
|
22590
|
-
for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
|
|
22591
|
-
try {
|
|
22592
|
-
return parseLinuxOsRelease(readFileSync(file, "utf8"));
|
|
22593
|
-
} catch {
|
|
22594
|
-
}
|
|
22595
|
-
}
|
|
22596
|
-
return null;
|
|
22597
|
-
}
|
|
22598
|
-
function readCommandOutput(command, args) {
|
|
22599
|
-
try {
|
|
22600
|
-
const output = execFileSync(command, args, {
|
|
22601
|
-
encoding: "utf8",
|
|
22602
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
22603
|
-
timeout: 1e3
|
|
22604
|
-
});
|
|
22605
|
-
return normalizeText(output);
|
|
22606
|
-
} catch {
|
|
22607
|
-
return null;
|
|
22608
|
-
}
|
|
22609
|
-
}
|
|
22610
|
-
function buildLinuxName(values) {
|
|
22611
|
-
const name = normalizeText(values.get("NAME"));
|
|
22612
|
-
const version = normalizeText(values.get("VERSION_ID")) ?? normalizeText(values.get("VERSION"));
|
|
22613
|
-
if (name && version) {
|
|
22614
|
-
return `${name} ${version}`;
|
|
22615
|
-
}
|
|
22616
|
-
return name ?? version;
|
|
22617
|
-
}
|
|
22618
|
-
function unquoteOsReleaseValue(value) {
|
|
22619
|
-
const trimmed = value.trim();
|
|
22620
|
-
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
22621
|
-
return trimmed.slice(1, -1).replace(/\\(["'`$\\])/gu, "$1");
|
|
22622
|
-
}
|
|
22623
|
-
return trimmed;
|
|
22624
|
-
}
|
|
22625
|
-
function normalizeText(value) {
|
|
22626
|
-
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
22627
|
-
return normalized ? normalized : null;
|
|
22628
|
-
}
|
|
22629
|
-
function truncateText(value, maxLength) {
|
|
22630
|
-
return value.length > maxLength ? value.slice(0, maxLength).trimEnd() : value;
|
|
22631
|
-
}
|
|
22632
|
-
|
|
22633
|
-
// src/topology/network.ts
|
|
22634
|
-
import os6 from "os";
|
|
22635
|
-
|
|
22636
|
-
// src/topology/environment.ts
|
|
22637
|
-
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
22638
|
-
import os5 from "os";
|
|
22639
|
-
function detectRuntimeEnvironment(env = process.env) {
|
|
22640
|
-
if (isWsl(env)) {
|
|
22641
|
-
return {
|
|
22642
|
-
kind: "wsl",
|
|
22643
|
-
lanAutoDiscoveryUsable: false,
|
|
22644
|
-
warning: "Detected WSL. The LAN IP found inside WSL is usually a private VM address and is not reachable from your phone. Use Relay or set `hermeslink config set lan-host <Windows LAN IP>`."
|
|
22645
|
-
};
|
|
22646
|
-
}
|
|
22647
|
-
if (isContainer(env)) {
|
|
22648
|
-
return {
|
|
22649
|
-
kind: "container",
|
|
22650
|
-
lanAutoDiscoveryUsable: false,
|
|
22651
|
-
warning: "Detected a container environment. Container LAN IPs are usually not reachable from your phone. Use Relay or set `hermeslink config set lan-host <host LAN IP>`."
|
|
22652
|
-
};
|
|
22653
|
-
}
|
|
22654
|
-
return {
|
|
22655
|
-
kind: "native",
|
|
22656
|
-
lanAutoDiscoveryUsable: true,
|
|
22657
|
-
warning: null
|
|
22658
|
-
};
|
|
22659
|
-
}
|
|
22660
|
-
function isWsl(env) {
|
|
22661
|
-
if (process.platform !== "linux") {
|
|
22662
|
-
return false;
|
|
22663
|
-
}
|
|
22664
|
-
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
|
|
22665
|
-
return true;
|
|
22666
|
-
}
|
|
22667
|
-
const release = os5.release().toLowerCase();
|
|
22668
|
-
return release.includes("microsoft") || release.includes("wsl");
|
|
22669
|
-
}
|
|
22670
|
-
function isContainer(env) {
|
|
22671
|
-
if (env.container || env.CONTAINER || env.KUBERNETES_SERVICE_HOST) {
|
|
22672
|
-
return true;
|
|
22673
|
-
}
|
|
22674
|
-
if (existsSync("/.dockerenv")) {
|
|
22675
|
-
return true;
|
|
22676
|
-
}
|
|
22677
|
-
try {
|
|
22678
|
-
const cgroup = readFileSync2("/proc/1/cgroup", "utf8").toLowerCase();
|
|
22679
|
-
return /docker|containerd|kubepods|libpod|podman/u.test(cgroup);
|
|
22680
|
-
} catch {
|
|
22681
|
-
return false;
|
|
22682
|
-
}
|
|
22683
|
-
}
|
|
22684
|
-
|
|
22685
|
-
// src/topology/network.ts
|
|
22686
|
-
var VIRTUAL_INTERFACE_NAME_PATTERN = /(docker|veth|vmnet|vmenet|vbox|virtualbox|vmware|tailscale|zerotier|wireguard|utun|virbr|hyper-v|vethernet|loopback|\blo\b|^lo\d*$|^br-|^bridge\d+$|^zt|^tun|^tap|awdl|llw|anpi|gif|stf|ipsec|ppp)/iu;
|
|
22687
|
-
var MAX_LAN_IPS = 4;
|
|
22688
|
-
var MAX_PUBLIC_IPV4S = 2;
|
|
22689
|
-
var MAX_PUBLIC_IPV6S = 2;
|
|
22690
|
-
async function discoverRouteCandidates(options) {
|
|
22691
|
-
const environment = detectRuntimeEnvironment();
|
|
22692
|
-
const configuredLanHost = normalizeLanHost(options.configuredLanHost);
|
|
22693
|
-
const lanIps = configuredLanHost ? [configuredLanHost] : environment.lanAutoDiscoveryUsable ? discoverLanIps() : [];
|
|
22694
|
-
const publicIps = options.relayBootstrapToken || options.observePublicRoute ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
|
|
22695
|
-
const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
|
|
22696
|
-
const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
|
|
22697
|
-
const preferredUrls = [
|
|
22698
|
-
...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
|
|
22699
|
-
...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
22700
|
-
...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
22701
|
-
`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/links/${options.linkId}`
|
|
22702
|
-
];
|
|
22703
|
-
return {
|
|
22704
|
-
lanIps,
|
|
22705
|
-
publicIpv4s,
|
|
22706
|
-
publicIpv6s,
|
|
22707
|
-
preferredUrls,
|
|
22708
|
-
environment
|
|
22709
|
-
};
|
|
22710
|
-
}
|
|
22711
|
-
function discoverLanIps() {
|
|
22712
|
-
return discoverLanIpsFromInterfaces(os6.networkInterfaces());
|
|
22713
|
-
}
|
|
22714
|
-
function discoverLanIpsFromInterfaces(interfaces) {
|
|
22715
|
-
const result = /* @__PURE__ */ new Set();
|
|
22716
|
-
const candidates = [];
|
|
22717
|
-
for (const [name, items] of Object.entries(interfaces)) {
|
|
22718
|
-
if (shouldIgnoreInterface(name)) {
|
|
22719
|
-
continue;
|
|
22720
|
-
}
|
|
22721
|
-
for (const item of items ?? []) {
|
|
22722
|
-
if (!item.internal && item.address && item.family === "IPv4" && isUsableLanIpv42(item.address, item.netmask)) {
|
|
22723
|
-
candidates.push({ name, address: item.address });
|
|
22724
|
-
}
|
|
22725
|
-
}
|
|
22726
|
-
}
|
|
22727
|
-
for (const candidate of candidates.sort(compareLanCandidate)) {
|
|
22728
|
-
result.add(candidate.address);
|
|
22729
|
-
}
|
|
22730
|
-
return [...result].slice(0, MAX_LAN_IPS);
|
|
22731
|
-
}
|
|
22732
|
-
async function observePublicRoute(options) {
|
|
22733
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
22734
|
-
const response = await fetcher(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/public-route/observe`, {
|
|
22735
|
-
method: "POST",
|
|
22736
|
-
headers: {
|
|
22737
|
-
"content-type": "application/json",
|
|
22738
|
-
...options.relayBootstrapToken ? { authorization: `Bearer ${options.relayBootstrapToken}` } : {}
|
|
22739
|
-
},
|
|
22740
|
-
body: JSON.stringify({
|
|
22741
|
-
install_id: options.installId,
|
|
22742
|
-
link_id: options.linkId,
|
|
22743
|
-
public_key_pem: options.publicKeyPem
|
|
22744
|
-
})
|
|
22745
|
-
});
|
|
22746
|
-
const payload = await response.json().catch(() => null);
|
|
22747
|
-
const record = typeof payload?.record === "object" && payload.record !== null ? payload.record : null;
|
|
22748
|
-
const observed = typeof payload?.observed === "object" && payload.observed !== null ? payload.observed : null;
|
|
22749
|
-
const values = [
|
|
22750
|
-
readIpRecord(record?.ipv4),
|
|
22751
|
-
readIpRecord(record?.ipv6),
|
|
22752
|
-
typeof observed?.ip === "string" ? observed.ip : null
|
|
22753
|
-
].filter((value) => Boolean(value));
|
|
22754
|
-
return {
|
|
22755
|
-
publicIpv4s: unique(values.filter(isUsablePublicIpv4)),
|
|
22756
|
-
publicIpv6s: unique(values.filter(isUsablePublicIpv6))
|
|
22757
|
-
};
|
|
22758
|
-
}
|
|
22759
|
-
function readIpRecord(value) {
|
|
22760
|
-
if (typeof value !== "object" || value === null) {
|
|
22761
|
-
return null;
|
|
22762
|
-
}
|
|
22763
|
-
const ip = value.ip;
|
|
22764
|
-
return typeof ip === "string" && ip.trim() ? ip.trim() : null;
|
|
22765
|
-
}
|
|
22766
|
-
function buildDirectUrl(ip, port) {
|
|
22767
|
-
return `http://${ip.includes(":") ? `[${ip}]` : ip}:${port}`;
|
|
22768
|
-
}
|
|
22769
|
-
function shouldIgnoreInterface(name) {
|
|
22770
|
-
return !name.trim() || VIRTUAL_INTERFACE_NAME_PATTERN.test(name);
|
|
22771
|
-
}
|
|
22772
|
-
function compareLanCandidate(left, right) {
|
|
22773
|
-
const priority = interfacePriority(left.name) - interfacePriority(right.name);
|
|
22774
|
-
return priority || left.name.localeCompare(right.name) || left.address.localeCompare(right.address);
|
|
22775
|
-
}
|
|
22776
|
-
function interfacePriority(name) {
|
|
22777
|
-
if (/^(en|eth|wlan|wi-fi|wifi)/iu.test(name)) {
|
|
22778
|
-
return 0;
|
|
22779
|
-
}
|
|
22780
|
-
return 1;
|
|
22781
|
-
}
|
|
22782
|
-
function isUsableLanIpv42(address, netmask) {
|
|
22783
|
-
return isPrivateIpv4(address) && !isNetworkOrBroadcastIpv4Address(address, netmask);
|
|
22784
|
-
}
|
|
22785
|
-
function isUsablePublicIpv4(address) {
|
|
22786
|
-
return isValidIpv4(address) && !isSpecialIpv4(address);
|
|
22787
|
-
}
|
|
22788
|
-
function isUsablePublicIpv6(address) {
|
|
22789
|
-
const normalized = address.toLowerCase();
|
|
22790
|
-
return normalized.includes(":") && !normalized.startsWith("fe80:") && !normalized.startsWith("fc") && !normalized.startsWith("fd") && !normalized.startsWith("ff") && normalized !== "::" && normalized !== "::1";
|
|
22791
|
-
}
|
|
22792
|
-
function isPrivateIpv4(address) {
|
|
22793
|
-
const parts = parseIpv4Segments(address);
|
|
22794
|
-
if (!parts) {
|
|
22795
|
-
return false;
|
|
22796
|
-
}
|
|
22797
|
-
const [first, second] = parts;
|
|
22798
|
-
return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
22799
|
-
}
|
|
22800
|
-
function isSpecialIpv4(address) {
|
|
22801
|
-
const parts = parseIpv4Segments(address);
|
|
22802
|
-
if (!parts) {
|
|
22803
|
-
return true;
|
|
22804
|
-
}
|
|
22805
|
-
const [first, second, third, fourth] = parts;
|
|
22806
|
-
return first === 0 || first === 10 || first === 127 || first >= 224 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 192 && second === 0 && third === 2 || first === 198 && (second === 18 || second === 19) || first === 198 && second === 51 && third === 100 || first === 203 && second === 0 && third === 113 || first === 255 && second === 255 && third === 255 && fourth === 255;
|
|
22807
|
-
}
|
|
22808
|
-
function isNetworkOrBroadcastIpv4Address(address, netmask) {
|
|
22809
|
-
const addressParts = parseIpv4Segments(address);
|
|
22810
|
-
const netmaskParts = netmask ? parseIpv4Segments(netmask) : null;
|
|
22811
|
-
if (!addressParts) {
|
|
22812
|
-
return true;
|
|
22813
|
-
}
|
|
22814
|
-
if (!netmaskParts) {
|
|
22815
|
-
const last = addressParts[3];
|
|
22816
|
-
return last === 0 || last === 255;
|
|
22817
|
-
}
|
|
22818
|
-
const addressInt = ipv4SegmentsToInt(addressParts);
|
|
22819
|
-
const netmaskInt = ipv4SegmentsToInt(netmaskParts);
|
|
22820
|
-
const hostMask = ~netmaskInt >>> 0;
|
|
22821
|
-
if (hostMask === 0) {
|
|
22822
|
-
return false;
|
|
22823
|
-
}
|
|
22824
|
-
const networkInt = addressInt & netmaskInt;
|
|
22825
|
-
const broadcastInt = (networkInt | hostMask) >>> 0;
|
|
22826
|
-
return addressInt === networkInt || addressInt === broadcastInt;
|
|
22827
|
-
}
|
|
22828
|
-
function isValidIpv4(address) {
|
|
22829
|
-
return Boolean(parseIpv4Segments(address));
|
|
22830
|
-
}
|
|
22831
|
-
function parseIpv4Segments(address) {
|
|
22832
|
-
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(address)) {
|
|
22833
|
-
return null;
|
|
22834
|
-
}
|
|
22835
|
-
const parts = address.split(".").map((part) => Number.parseInt(part, 10));
|
|
22836
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
22837
|
-
return null;
|
|
22838
|
-
}
|
|
22839
|
-
return parts;
|
|
22840
|
-
}
|
|
22841
|
-
function ipv4SegmentsToInt(parts) {
|
|
22842
|
-
return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
|
|
22843
|
-
}
|
|
22844
|
-
function unique(values) {
|
|
22845
|
-
return [...new Set(values)];
|
|
22846
|
-
}
|
|
22847
|
-
|
|
22848
|
-
// src/link/network-report-state.ts
|
|
22849
|
-
var DEFAULT_AUTO_DAILY_LIMIT = 20;
|
|
22850
|
-
async function readNetworkReportState(paths) {
|
|
22851
|
-
const state = await readLinkState(paths);
|
|
22852
|
-
return normalizeNetworkReportState(state.networkReport);
|
|
22853
|
-
}
|
|
22854
|
-
async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
|
|
22855
|
-
const snapshot = normalizeNetworkSnapshot(snapshotInput);
|
|
22856
|
-
await updateNetworkReportState(paths, (current) => ({
|
|
22857
|
-
...current,
|
|
22858
|
-
lastReportedLanIps: snapshot.lanIps,
|
|
22859
|
-
lastReportedPublicIpv4s: snapshot.publicIpv4s,
|
|
22860
|
-
lastReportedPublicIpv6s: snapshot.publicIpv6s,
|
|
22861
|
-
lastReportedAt: reportedAt.toISOString(),
|
|
22862
|
-
lastAutoAttempt: current.lastAutoAttempt ? { ...current.lastAutoAttempt, ...snapshot, success: true } : null
|
|
22863
|
-
}));
|
|
22864
|
-
}
|
|
22865
|
-
async function reserveAutomaticNetworkReport(paths, snapshotInput, options = {}) {
|
|
22866
|
-
const snapshot = normalizeNetworkSnapshot(snapshotInput);
|
|
22867
|
-
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
22868
|
-
const dailyLimit = Math.max(0, Math.floor(options.dailyLimit ?? DEFAULT_AUTO_DAILY_LIMIT));
|
|
22869
|
-
let reservation = { allowed: false, reason: "unchanged" };
|
|
22870
|
-
await updateNetworkReportState(paths, (current) => {
|
|
22871
|
-
if (sameNetworkSnapshot(readReportedSnapshot(current), snapshot)) {
|
|
22872
|
-
const lastReportedAt = current.lastReportedAt ? Date.parse(current.lastReportedAt) : Number.NaN;
|
|
22873
|
-
const unchangedMinIntervalMs = Math.max(0, Math.floor(options.unchangedMinIntervalMs ?? 0));
|
|
22874
|
-
if (!options.force || Number.isFinite(lastReportedAt) && now.getTime() - lastReportedAt < unchangedMinIntervalMs) {
|
|
22875
|
-
reservation = { allowed: false, reason: "unchanged" };
|
|
22876
|
-
return current;
|
|
22877
|
-
}
|
|
22878
|
-
}
|
|
22879
|
-
if (current.lastAutoAttempt && !current.lastAutoAttempt.success && sameNetworkSnapshot(readAttemptSnapshot(current.lastAutoAttempt), snapshot)) {
|
|
22880
|
-
reservation = { allowed: false, reason: "failed_snapshot_not_retried" };
|
|
22881
|
-
return current;
|
|
22882
|
-
}
|
|
22883
|
-
const quotaDay = formatUtcDay(now);
|
|
22884
|
-
const reportsToday = current.autoQuotaDay === quotaDay ? current.autoReportsToday : 0;
|
|
22885
|
-
if (reportsToday >= dailyLimit) {
|
|
22886
|
-
reservation = { allowed: false, reason: "daily_limit_reached" };
|
|
22887
|
-
return current;
|
|
22888
|
-
}
|
|
22889
|
-
reservation = { allowed: true };
|
|
22890
|
-
return {
|
|
22891
|
-
...current,
|
|
22892
|
-
autoQuotaDay: quotaDay,
|
|
22893
|
-
autoReportsToday: reportsToday + 1,
|
|
22894
|
-
lastAutoAttempt: {
|
|
22895
|
-
...snapshot,
|
|
22896
|
-
attemptedAt: now.toISOString(),
|
|
22897
|
-
success: false
|
|
22898
|
-
}
|
|
22899
|
-
};
|
|
22900
|
-
});
|
|
22901
|
-
return reservation;
|
|
22902
|
-
}
|
|
22903
|
-
async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
|
|
22904
|
-
const state = await readNetworkReportState(paths);
|
|
22905
|
-
return {
|
|
22906
|
-
...snapshotInput,
|
|
22907
|
-
publicIpv4s: uniqueStrings([
|
|
22908
|
-
...snapshotInput.publicIpv4s,
|
|
22909
|
-
...state.lastReportedPublicIpv4s
|
|
22910
|
-
]).slice(0, 2),
|
|
22911
|
-
publicIpv6s: uniqueStrings([
|
|
22912
|
-
...snapshotInput.publicIpv6s,
|
|
22913
|
-
...state.lastReportedPublicIpv6s
|
|
22914
|
-
]).slice(0, 2)
|
|
22915
|
-
};
|
|
22916
|
-
}
|
|
22917
|
-
async function updateNetworkReportState(paths, update) {
|
|
22918
|
-
const state = await readLinkState(paths);
|
|
22919
|
-
const next = {
|
|
22920
|
-
...state,
|
|
22921
|
-
networkReport: update(normalizeNetworkReportState(state.networkReport))
|
|
22922
|
-
};
|
|
22923
|
-
await writeJsonFile(paths.stateFile, next);
|
|
22924
|
-
}
|
|
22925
|
-
async function readLinkState(paths) {
|
|
22926
|
-
const state = await readJsonFile(paths.stateFile);
|
|
22927
|
-
return state && typeof state === "object" ? state : {};
|
|
22928
|
-
}
|
|
22929
|
-
function normalizeNetworkReportState(value) {
|
|
22930
|
-
const record = value && typeof value === "object" ? value : {};
|
|
22931
|
-
return {
|
|
22932
|
-
lastReportedLanIps: normalizeLanIps(record.lastReportedLanIps),
|
|
22933
|
-
lastReportedPublicIpv4s: normalizeLanIps(record.lastReportedPublicIpv4s),
|
|
22934
|
-
lastReportedPublicIpv6s: normalizeLanIps(record.lastReportedPublicIpv6s),
|
|
22935
|
-
lastReportedAt: typeof record.lastReportedAt === "string" ? record.lastReportedAt : null,
|
|
22936
|
-
autoQuotaDay: typeof record.autoQuotaDay === "string" ? record.autoQuotaDay : null,
|
|
22937
|
-
autoReportsToday: Number.isFinite(record.autoReportsToday) ? Math.max(0, Math.floor(record.autoReportsToday ?? 0)) : 0,
|
|
22938
|
-
lastAutoAttempt: normalizeAttempt(record.lastAutoAttempt)
|
|
22939
|
-
};
|
|
22940
|
-
}
|
|
22941
|
-
function normalizeAttempt(value) {
|
|
22942
|
-
if (!value || typeof value !== "object") {
|
|
22943
|
-
return null;
|
|
22944
|
-
}
|
|
22945
|
-
const record = value;
|
|
22946
|
-
if (typeof record.attemptedAt !== "string") {
|
|
22947
|
-
return null;
|
|
22948
|
-
}
|
|
22949
|
-
return {
|
|
22950
|
-
lanIps: normalizeLanIps(record.lanIps),
|
|
22951
|
-
publicIpv4s: normalizeLanIps(record.publicIpv4s),
|
|
22952
|
-
publicIpv6s: normalizeLanIps(record.publicIpv6s),
|
|
22953
|
-
attemptedAt: record.attemptedAt,
|
|
22954
|
-
success: record.success === true
|
|
22955
|
-
};
|
|
22956
|
-
}
|
|
22957
|
-
function normalizeNetworkSnapshot(value) {
|
|
22958
|
-
if (Array.isArray(value)) {
|
|
22959
|
-
return {
|
|
22960
|
-
lanIps: normalizeLanIps(value),
|
|
22961
|
-
publicIpv4s: [],
|
|
22962
|
-
publicIpv6s: []
|
|
22963
|
-
};
|
|
22964
|
-
}
|
|
22965
|
-
const record = value && typeof value === "object" ? value : {};
|
|
22966
|
-
return {
|
|
22967
|
-
lanIps: normalizeLanIps(record.lanIps),
|
|
22968
|
-
publicIpv4s: normalizeLanIps(record.publicIpv4s),
|
|
22969
|
-
publicIpv6s: normalizeLanIps(record.publicIpv6s)
|
|
22970
|
-
};
|
|
22971
|
-
}
|
|
22972
|
-
function readReportedSnapshot(state) {
|
|
22973
|
-
return {
|
|
22974
|
-
lanIps: state.lastReportedLanIps,
|
|
22975
|
-
publicIpv4s: state.lastReportedPublicIpv4s,
|
|
22976
|
-
publicIpv6s: state.lastReportedPublicIpv6s
|
|
22977
|
-
};
|
|
22978
|
-
}
|
|
22979
|
-
function readAttemptSnapshot(attempt) {
|
|
22980
|
-
return {
|
|
22981
|
-
lanIps: attempt.lanIps,
|
|
22982
|
-
publicIpv4s: attempt.publicIpv4s,
|
|
22983
|
-
publicIpv6s: attempt.publicIpv6s
|
|
22984
|
-
};
|
|
22985
|
-
}
|
|
22986
|
-
function normalizeLanIps(value) {
|
|
22987
|
-
if (!Array.isArray(value)) {
|
|
22988
|
-
return [];
|
|
22989
|
-
}
|
|
22990
|
-
return [
|
|
22991
|
-
...new Set(
|
|
22992
|
-
value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
|
|
22993
|
-
)
|
|
22994
|
-
];
|
|
22995
|
-
}
|
|
22996
|
-
function sameNetworkSnapshot(left, right) {
|
|
22997
|
-
return sameStringList(left.lanIps, right.lanIps) && sameStringList(left.publicIpv4s, right.publicIpv4s) && sameStringList(left.publicIpv6s, right.publicIpv6s);
|
|
22998
|
-
}
|
|
22999
|
-
function sameStringList(left, right) {
|
|
23000
|
-
if (left.length !== right.length) {
|
|
23001
|
-
return false;
|
|
23002
|
-
}
|
|
23003
|
-
return left.every((value, index) => value === right[index]);
|
|
23004
|
-
}
|
|
23005
|
-
function uniqueStrings(values) {
|
|
23006
|
-
return [...new Set(values)];
|
|
23007
|
-
}
|
|
23008
|
-
function formatUtcDay(date) {
|
|
23009
|
-
return date.toISOString().slice(0, 10);
|
|
23010
|
-
}
|
|
23011
|
-
|
|
23012
|
-
// src/link/server-report.ts
|
|
23013
|
-
async function reportLinkStatusToServer(options = {}) {
|
|
23014
|
-
const paths = options.paths ?? resolveRuntimePaths();
|
|
23015
|
-
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
23016
|
-
if (!identity?.link_id) {
|
|
23017
|
-
return null;
|
|
23018
|
-
}
|
|
23019
|
-
const discoveredRoutes = options.routes ?? await discoverRouteCandidates({
|
|
23020
|
-
port: config.port,
|
|
23021
|
-
relayBaseUrl: config.relayBaseUrl,
|
|
23022
|
-
linkId: identity.link_id,
|
|
23023
|
-
installId: identity.install_id,
|
|
23024
|
-
publicKeyPem: identity.public_key_pem,
|
|
23025
|
-
observePublicRoute: true,
|
|
23026
|
-
configuredLanHost: config.lanHost,
|
|
23027
|
-
fetchImpl: options.fetchImpl
|
|
23028
|
-
});
|
|
23029
|
-
const routes = await mergeLastReportedPublicRoutes(paths, discoveredRoutes);
|
|
23030
|
-
const systemInfo = readLinkSystemInfo();
|
|
23031
|
-
const payload = {
|
|
23032
|
-
type: "hermes_link_status_report",
|
|
23033
|
-
link_id: identity.link_id,
|
|
23034
|
-
install_id: identity.install_id,
|
|
23035
|
-
link_version: LINK_VERSION,
|
|
23036
|
-
display_name: systemInfo.defaultDisplayName,
|
|
23037
|
-
platform: systemInfo.platform,
|
|
23038
|
-
hostname: systemInfo.hostname ?? void 0,
|
|
23039
|
-
lan_ips: routes.lanIps,
|
|
23040
|
-
public_ipv4s: routes.publicIpv4s,
|
|
23041
|
-
public_ipv6s: routes.publicIpv6s,
|
|
23042
|
-
reported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
23043
|
-
};
|
|
23044
|
-
const signature = signIdentityPayload(identity, canonicalJson(payload));
|
|
23045
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
23046
|
-
const response = await fetcher(
|
|
23047
|
-
`${config.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/links/${encodeURIComponent(identity.link_id)}/report`,
|
|
23048
|
-
{
|
|
23049
|
-
method: "POST",
|
|
23050
|
-
headers: {
|
|
23051
|
-
accept: "application/json",
|
|
23052
|
-
"content-type": "application/json"
|
|
23053
|
-
},
|
|
23054
|
-
body: JSON.stringify({
|
|
23055
|
-
...payload,
|
|
23056
|
-
public_key_pem: identity.public_key_pem,
|
|
23057
|
-
signature
|
|
23058
|
-
})
|
|
23059
|
-
}
|
|
23060
|
-
);
|
|
23061
|
-
const body = await response.json().catch(() => null);
|
|
23062
|
-
if (!response.ok || !body) {
|
|
23063
|
-
const message = readErrorMessage3(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
23064
|
-
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
23065
|
-
}
|
|
23066
|
-
await markNetworkStatusReported(paths, routes);
|
|
23067
|
-
return body;
|
|
23068
|
-
}
|
|
23069
|
-
function canonicalJson(value) {
|
|
23070
|
-
return JSON.stringify(sortJsonValue(value));
|
|
23071
|
-
}
|
|
23072
|
-
function sortJsonValue(value) {
|
|
23073
|
-
if (Array.isArray(value)) {
|
|
23074
|
-
return value.map(sortJsonValue);
|
|
23075
|
-
}
|
|
23076
|
-
if (value && typeof value === "object") {
|
|
23077
|
-
const record = value;
|
|
23078
|
-
const sorted = {};
|
|
23079
|
-
for (const key of Object.keys(record).sort()) {
|
|
23080
|
-
sorted[key] = sortJsonValue(record[key]);
|
|
23081
|
-
}
|
|
23082
|
-
return sorted;
|
|
23083
|
-
}
|
|
23084
|
-
return value;
|
|
23085
|
-
}
|
|
23086
|
-
function readErrorMessage3(payload) {
|
|
23087
|
-
if (typeof payload !== "object" || payload === null) {
|
|
23088
|
-
return null;
|
|
23089
|
-
}
|
|
23090
|
-
const error = payload.error;
|
|
23091
|
-
if (typeof error !== "object" || error === null) {
|
|
23092
|
-
return null;
|
|
23093
|
-
}
|
|
23094
|
-
const message = error.message;
|
|
23095
|
-
return typeof message === "string" ? message : null;
|
|
23096
|
-
}
|
|
23097
|
-
|
|
23098
|
-
// src/daemon/lan-ip-monitor.ts
|
|
23099
|
-
var DEFAULT_INTERVAL_MS = 5 * 6e4;
|
|
23100
|
-
var DEFAULT_DAILY_REPORT_LIMIT = 20;
|
|
23101
|
-
var DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS = 15 * 6e4;
|
|
23102
|
-
function startLanIpMonitor(options) {
|
|
23103
|
-
let running = false;
|
|
23104
|
-
let closed = false;
|
|
23105
|
-
let current = Promise.resolve();
|
|
23106
|
-
const check = async (context = {}) => {
|
|
23107
|
-
if (running || closed) {
|
|
23108
|
-
return;
|
|
23109
|
-
}
|
|
23110
|
-
running = true;
|
|
23111
|
-
try {
|
|
23112
|
-
await checkLanIpChange(options, context);
|
|
23113
|
-
} catch (error) {
|
|
23114
|
-
void options.logger.warn("lan_ip_monitor_failed", {
|
|
23115
|
-
error: error instanceof Error ? error.message : String(error)
|
|
23116
|
-
});
|
|
23117
|
-
} finally {
|
|
23118
|
-
running = false;
|
|
23119
|
-
}
|
|
23120
|
-
};
|
|
23121
|
-
current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
|
|
23122
|
-
const timer = setInterval(() => {
|
|
23123
|
-
current = check({ observePublicRoute: false });
|
|
23124
|
-
}, options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
23125
|
-
timer.unref?.();
|
|
23126
|
-
return {
|
|
23127
|
-
async refreshPublicRoutes() {
|
|
23128
|
-
current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
|
|
23129
|
-
await current;
|
|
23130
|
-
},
|
|
23131
|
-
async close() {
|
|
23132
|
-
closed = true;
|
|
23133
|
-
clearInterval(timer);
|
|
23134
|
-
await current.catch(() => void 0);
|
|
23135
|
-
}
|
|
23136
|
-
};
|
|
23137
|
-
}
|
|
23138
|
-
async function checkLanIpChange(options, context = {}) {
|
|
23139
|
-
const [identity, config] = await Promise.all([
|
|
23140
|
-
loadIdentity(options.paths),
|
|
23141
|
-
loadConfig(options.paths)
|
|
23142
|
-
]);
|
|
23143
|
-
if (!identity?.link_id) {
|
|
23144
|
-
return;
|
|
23145
|
-
}
|
|
23146
|
-
const discoveredRoutes = await discoverRouteCandidates({
|
|
23147
|
-
port: config.port,
|
|
23148
|
-
relayBaseUrl: config.relayBaseUrl,
|
|
23149
|
-
linkId: identity.link_id,
|
|
23150
|
-
installId: identity.install_id,
|
|
23151
|
-
publicKeyPem: identity.public_key_pem,
|
|
23152
|
-
observePublicRoute: context.observePublicRoute === true,
|
|
23153
|
-
configuredLanHost: config.lanHost,
|
|
23154
|
-
fetchImpl: options.fetchImpl
|
|
23155
|
-
});
|
|
23156
|
-
const routes = await mergeLastReportedPublicRoutes(options.paths, discoveredRoutes);
|
|
23157
|
-
if (context.publishToRelay) {
|
|
23158
|
-
options.onNetworkRoutes?.(routes);
|
|
23159
|
-
}
|
|
23160
|
-
const reservation = await reserveAutomaticNetworkReport(options.paths, routes, {
|
|
23161
|
-
dailyLimit: options.dailyReportLimit ?? DEFAULT_DAILY_REPORT_LIMIT,
|
|
23162
|
-
force: context.forceReport === true,
|
|
23163
|
-
unchangedMinIntervalMs: options.startupReportMinIntervalMs ?? DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS
|
|
23164
|
-
});
|
|
23165
|
-
if (!reservation.allowed) {
|
|
23166
|
-
const logFields = {
|
|
23167
|
-
lan_ips: routes.lanIps,
|
|
23168
|
-
public_ipv4s: routes.publicIpv4s,
|
|
23169
|
-
public_ipv6s: routes.publicIpv6s,
|
|
23170
|
-
reason: reservation.reason
|
|
23171
|
-
};
|
|
23172
|
-
void options.logger.debug("lan_ip_report_skipped", logFields);
|
|
23173
|
-
return;
|
|
23174
|
-
}
|
|
23175
|
-
try {
|
|
23176
|
-
const result = await reportLinkStatusToServer({
|
|
23177
|
-
paths: options.paths,
|
|
23178
|
-
fetchImpl: options.fetchImpl,
|
|
23179
|
-
routes
|
|
23180
|
-
});
|
|
23181
|
-
if (result) {
|
|
23182
|
-
options.onNetworkRoutes?.(routes);
|
|
23183
|
-
void options.logger.info("lan_ip_change_reported", {
|
|
23184
|
-
link_id: result.linkId,
|
|
23185
|
-
lan_ips: routes.lanIps,
|
|
23186
|
-
public_ipv4s: routes.publicIpv4s,
|
|
23187
|
-
public_ipv6s: routes.publicIpv6s
|
|
23188
|
-
});
|
|
23189
|
-
}
|
|
23190
|
-
} catch (error) {
|
|
23191
|
-
void options.logger.warn("lan_ip_change_report_failed", {
|
|
23192
|
-
lan_ips: routes.lanIps,
|
|
23193
|
-
error: error instanceof Error ? error.message : String(error)
|
|
23194
|
-
});
|
|
23195
|
-
}
|
|
23196
|
-
}
|
|
23197
|
-
|
|
23198
|
-
// src/daemon/scheduler.ts
|
|
23199
|
-
function startCronDeliveryScheduler(options) {
|
|
23200
|
-
let running = false;
|
|
23201
|
-
let current = Promise.resolve();
|
|
23202
|
-
const syncCronDeliveries = async () => {
|
|
23203
|
-
if (running) {
|
|
23204
|
-
return;
|
|
23205
|
-
}
|
|
23206
|
-
running = true;
|
|
23207
|
-
try {
|
|
23208
|
-
await syncHermesLinkCronDeliveries(
|
|
23209
|
-
options.paths,
|
|
23210
|
-
options.conversations,
|
|
23211
|
-
options.logger
|
|
23212
|
-
);
|
|
23213
|
-
} catch (error) {
|
|
23214
|
-
void options.logger.warn("cron_link_delivery_sync_failed", {
|
|
23215
|
-
source: "daemon_scheduler",
|
|
23216
|
-
error: error instanceof Error ? error.message : String(error)
|
|
23217
|
-
});
|
|
23218
|
-
} finally {
|
|
23219
|
-
running = false;
|
|
23220
|
-
}
|
|
23221
|
-
};
|
|
23222
|
-
const timer = setInterval(() => {
|
|
23223
|
-
current = syncCronDeliveries();
|
|
23224
|
-
}, options.intervalMs ?? 3e4);
|
|
23225
|
-
timer.unref?.();
|
|
23226
|
-
return {
|
|
23227
|
-
async close() {
|
|
23228
|
-
clearInterval(timer);
|
|
23229
|
-
await current.catch(() => void 0);
|
|
23230
|
-
}
|
|
23231
|
-
};
|
|
23232
|
-
}
|
|
23233
|
-
function startHermesSessionSyncScheduler(options) {
|
|
23234
|
-
let running = false;
|
|
23235
|
-
let current = Promise.resolve();
|
|
23236
|
-
const syncSessions = async () => {
|
|
23237
|
-
if (running) {
|
|
23238
|
-
return;
|
|
23239
|
-
}
|
|
23240
|
-
running = true;
|
|
23241
|
-
try {
|
|
23242
|
-
await options.conversations.syncHermesSessions();
|
|
23243
|
-
} catch (error) {
|
|
23244
|
-
void options.logger.warn("hermes_session_sync_failed", {
|
|
23245
|
-
source: "daemon_scheduler",
|
|
23246
|
-
error: error instanceof Error ? error.message : String(error)
|
|
23247
|
-
});
|
|
23248
|
-
} finally {
|
|
23249
|
-
running = false;
|
|
23250
|
-
}
|
|
23251
|
-
};
|
|
23252
|
-
const timer = setInterval(() => {
|
|
23253
|
-
current = syncSessions();
|
|
23254
|
-
}, options.intervalMs ?? 10 * 60 * 1e3);
|
|
23255
|
-
timer.unref?.();
|
|
23256
|
-
return {
|
|
23257
|
-
async close() {
|
|
23258
|
-
clearInterval(timer);
|
|
23259
|
-
await current.catch(() => void 0);
|
|
23260
|
-
}
|
|
23261
|
-
};
|
|
23262
|
-
}
|
|
23263
|
-
|
|
23264
|
-
// src/daemon/service.ts
|
|
23265
|
-
var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
|
|
23266
|
-
var RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS = 15 * 6e4;
|
|
23267
|
-
async function startLinkService(options = {}) {
|
|
23268
|
-
const paths = options.paths ?? resolveRuntimePaths();
|
|
23269
|
-
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
23270
|
-
const logger = createFileLogger({ paths, minLevel: config.logLevel });
|
|
23271
|
-
await logger.info("service_starting", {
|
|
23272
|
-
port: config.port,
|
|
23273
|
-
mode: identity?.link_id ? "paired" : "local-only"
|
|
23067
|
+
child.stdout?.on("data", (chunk) => {
|
|
23068
|
+
void appendChunk(chunk);
|
|
23274
23069
|
});
|
|
23275
|
-
|
|
23276
|
-
|
|
23277
|
-
await logger.info("database_migrated", {
|
|
23278
|
-
database_file: migration.databaseFile,
|
|
23279
|
-
applied_versions: migration.appliedVersions,
|
|
23280
|
-
current_version: migration.currentVersion
|
|
23281
|
-
});
|
|
23282
|
-
}
|
|
23283
|
-
const conversations = new ConversationService(paths, logger);
|
|
23284
|
-
await conversations.rebuildStatisticsIndex();
|
|
23285
|
-
let hermesSessionSync = Promise.resolve();
|
|
23286
|
-
const triggerHermesSessionSync = () => {
|
|
23287
|
-
hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
|
|
23288
|
-
void logger.warn("hermes_session_sync_failed", {
|
|
23289
|
-
source: "service_startup",
|
|
23290
|
-
error: error instanceof Error ? error.message : String(error)
|
|
23291
|
-
});
|
|
23292
|
-
});
|
|
23293
|
-
};
|
|
23294
|
-
const app = await createApp({
|
|
23295
|
-
paths,
|
|
23296
|
-
logger,
|
|
23297
|
-
conversations,
|
|
23298
|
-
onPairingClaimed: async () => {
|
|
23299
|
-
triggerHermesSessionSync();
|
|
23300
|
-
await options.onPairingClaimed?.();
|
|
23301
|
-
}
|
|
23070
|
+
child.stderr?.on("data", (chunk) => {
|
|
23071
|
+
void appendChunk(chunk);
|
|
23302
23072
|
});
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
|
|
23306
|
-
|
|
23307
|
-
|
|
23308
|
-
|
|
23309
|
-
|
|
23310
|
-
|
|
23073
|
+
runningUpdate2 = new Promise((resolve) => {
|
|
23074
|
+
child.on("error", (error) => {
|
|
23075
|
+
void (async () => {
|
|
23076
|
+
const failed = {
|
|
23077
|
+
...started,
|
|
23078
|
+
state: "failed",
|
|
23079
|
+
finished_at: now().toISOString(),
|
|
23080
|
+
error: error.message
|
|
23081
|
+
};
|
|
23082
|
+
await writer.write(
|
|
23083
|
+
`
|
|
23084
|
+
[failed] link update failed to start: ${error.message}
|
|
23085
|
+
`
|
|
23086
|
+
);
|
|
23087
|
+
await writeUpdateState2(options.paths, failed);
|
|
23088
|
+
await emitUpdateStatus2(options.paths);
|
|
23089
|
+
void options.logger?.error("link_update_spawn_failed", {
|
|
23090
|
+
job_id: jobId,
|
|
23091
|
+
target_version: targetVersion,
|
|
23092
|
+
error: error.message
|
|
23093
|
+
});
|
|
23094
|
+
resolve(await readLinkUpdateStatus(options.paths));
|
|
23095
|
+
})();
|
|
23311
23096
|
});
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
|
|
23318
|
-
|
|
23319
|
-
|
|
23097
|
+
child.on("close", (code, signal) => {
|
|
23098
|
+
void (async () => {
|
|
23099
|
+
const succeeded = code === 0;
|
|
23100
|
+
const state = {
|
|
23101
|
+
...started,
|
|
23102
|
+
state: succeeded ? "restart_required" : "failed",
|
|
23103
|
+
finished_at: now().toISOString(),
|
|
23104
|
+
exit_code: code,
|
|
23105
|
+
signal,
|
|
23106
|
+
error: succeeded ? null : `install script exited with code ${code ?? "unknown"}`
|
|
23107
|
+
};
|
|
23108
|
+
await writer.write(
|
|
23109
|
+
`
|
|
23110
|
+
=== link update finished ${state.finished_at} exit=${code ?? "null"} signal=${signal ?? "null"} ===
|
|
23111
|
+
`
|
|
23112
|
+
);
|
|
23113
|
+
if (succeeded) {
|
|
23114
|
+
await writer.write(
|
|
23115
|
+
`
|
|
23116
|
+
[restart-requested] The install script should restart Hermes Link and verify the running version. If it does not reconnect, run \`${LINK_COMMAND} restart\` on this computer.
|
|
23117
|
+
`
|
|
23118
|
+
);
|
|
23119
|
+
}
|
|
23120
|
+
if (succeeded) {
|
|
23121
|
+
await writer.flush();
|
|
23122
|
+
setTimeout(() => {
|
|
23123
|
+
void (async () => {
|
|
23124
|
+
await writeUpdateState2(options.paths, state);
|
|
23125
|
+
await emitUpdateStatus2(options.paths);
|
|
23126
|
+
})();
|
|
23127
|
+
}, 1e3).unref();
|
|
23128
|
+
} else {
|
|
23129
|
+
await writeUpdateState2(options.paths, state);
|
|
23130
|
+
await emitUpdateStatus2(options.paths);
|
|
23131
|
+
}
|
|
23132
|
+
void options.logger?.info(
|
|
23133
|
+
succeeded ? "link_update_restart_required" : "link_update_failed",
|
|
23134
|
+
{
|
|
23135
|
+
job_id: jobId,
|
|
23136
|
+
target_version: targetVersion,
|
|
23137
|
+
exit_code: code,
|
|
23138
|
+
signal: signal ?? null
|
|
23139
|
+
}
|
|
23140
|
+
);
|
|
23141
|
+
resolve(await readLinkUpdateStatus(options.paths));
|
|
23142
|
+
})();
|
|
23320
23143
|
});
|
|
23144
|
+
}).finally(() => {
|
|
23145
|
+
runningUpdate2 = null;
|
|
23321
23146
|
});
|
|
23322
|
-
|
|
23323
|
-
|
|
23324
|
-
|
|
23325
|
-
|
|
23326
|
-
|
|
23327
|
-
|
|
23328
|
-
paths,
|
|
23329
|
-
conversations,
|
|
23330
|
-
logger
|
|
23331
|
-
});
|
|
23332
|
-
const hermesSessionSyncScheduler = startHermesSessionSyncScheduler({
|
|
23333
|
-
conversations,
|
|
23334
|
-
logger
|
|
23147
|
+
await emitUpdateStatus2(options.paths);
|
|
23148
|
+
void options.logger?.info("link_update_started", {
|
|
23149
|
+
job_id: jobId,
|
|
23150
|
+
pid: child.pid ?? null,
|
|
23151
|
+
target_version: targetVersion,
|
|
23152
|
+
log_path: writer.filePath
|
|
23335
23153
|
});
|
|
23336
|
-
|
|
23337
|
-
|
|
23338
|
-
|
|
23339
|
-
let
|
|
23340
|
-
if (
|
|
23341
|
-
|
|
23342
|
-
|
|
23343
|
-
|
|
23344
|
-
|
|
23345
|
-
|
|
23346
|
-
|
|
23347
|
-
|
|
23348
|
-
localPort: config.port,
|
|
23349
|
-
maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
|
|
23350
|
-
backoffBaseMs: 1e3,
|
|
23351
|
-
backoffMaxMs: 3e4,
|
|
23352
|
-
onStatus: (status) => {
|
|
23353
|
-
void logger.info("relay_status", status);
|
|
23354
|
-
if (status.state === "connected") {
|
|
23355
|
-
const now = Date.now();
|
|
23356
|
-
if (hasSeenRelayConnected && lanIpMonitor && now - lastRelayReconnectPublicRouteRefreshAt >= RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS) {
|
|
23357
|
-
lastRelayReconnectPublicRouteRefreshAt = now;
|
|
23358
|
-
void lanIpMonitor.refreshPublicRoutes();
|
|
23359
|
-
}
|
|
23360
|
-
hasSeenRelayConnected = true;
|
|
23361
|
-
resolveRelayReady?.(true);
|
|
23362
|
-
resolveRelayReady = null;
|
|
23363
|
-
} else if (status.state === "failed") {
|
|
23364
|
-
resolveRelayReady?.(false);
|
|
23365
|
-
resolveRelayReady = null;
|
|
23366
|
-
}
|
|
23367
|
-
}
|
|
23368
|
-
});
|
|
23369
|
-
if (options.waitForRelayReady) {
|
|
23370
|
-
await Promise.race([
|
|
23371
|
-
relayReady,
|
|
23372
|
-
waitForRelayReadyTimeout(options.relayReadyTimeoutMs)
|
|
23373
|
-
]);
|
|
23374
|
-
resolveRelayReady = null;
|
|
23154
|
+
return readLinkUpdateStatus(options.paths);
|
|
23155
|
+
}
|
|
23156
|
+
async function readLinkUpdateStatus(paths) {
|
|
23157
|
+
let state = await readJsonFile(updateStatePath2(paths));
|
|
23158
|
+
if (state?.state === "restart_required" && state.target_version) {
|
|
23159
|
+
if (compareSemver3(LINK_VERSION, state.target_version) >= 0) {
|
|
23160
|
+
state = {
|
|
23161
|
+
...state,
|
|
23162
|
+
state: "succeeded",
|
|
23163
|
+
finished_at: state.finished_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
23164
|
+
};
|
|
23165
|
+
await writeUpdateState2(paths, state);
|
|
23375
23166
|
}
|
|
23376
|
-
} else {
|
|
23377
|
-
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
23378
23167
|
}
|
|
23379
|
-
|
|
23380
|
-
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
|
|
23168
|
+
if (state?.state === "running" && !runningUpdate2 && !isRecentRunningState3(state) && !isProcessAlive3(state.pid)) {
|
|
23169
|
+
const reachedTarget = state.target_version && compareSemver3(LINK_VERSION, state.target_version) >= 0;
|
|
23170
|
+
state = reachedTarget ? {
|
|
23171
|
+
...state,
|
|
23172
|
+
state: "succeeded",
|
|
23173
|
+
finished_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23174
|
+
error: null
|
|
23175
|
+
} : {
|
|
23176
|
+
...state,
|
|
23177
|
+
state: "failed",
|
|
23178
|
+
finished_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23179
|
+
error: state.error ?? "Link update was interrupted before Hermes Link could observe completion."
|
|
23180
|
+
};
|
|
23181
|
+
await writeUpdateState2(paths, state);
|
|
23391
23182
|
}
|
|
23392
23183
|
return {
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
23403
|
-
|
|
23404
|
-
|
|
23405
|
-
|
|
23406
|
-
}
|
|
23407
|
-
}
|
|
23184
|
+
ok: true,
|
|
23185
|
+
state: state?.state ?? "idle",
|
|
23186
|
+
job_id: state?.job_id ?? null,
|
|
23187
|
+
pid: state?.pid ?? null,
|
|
23188
|
+
target_version: state?.target_version ?? null,
|
|
23189
|
+
started_at: state?.started_at ?? null,
|
|
23190
|
+
finished_at: state?.finished_at ?? null,
|
|
23191
|
+
exit_code: state?.exit_code ?? null,
|
|
23192
|
+
signal: state?.signal ?? null,
|
|
23193
|
+
log_path: updateLogPath2(paths),
|
|
23194
|
+
lines: await readUpdateLogLines2(paths),
|
|
23195
|
+
error: state?.error ?? null,
|
|
23196
|
+
manual_command: state?.manual_command ?? null
|
|
23408
23197
|
};
|
|
23409
23198
|
}
|
|
23410
|
-
function
|
|
23411
|
-
|
|
23412
|
-
|
|
23413
|
-
() => resolve(false),
|
|
23414
|
-
timeoutMs ?? DEFAULT_RELAY_READY_TIMEOUT_MS
|
|
23415
|
-
);
|
|
23416
|
-
timer.unref?.();
|
|
23417
|
-
});
|
|
23418
|
-
}
|
|
23419
|
-
function pidFilePath(paths = resolveRuntimePaths()) {
|
|
23420
|
-
return `${paths.runDir}/hermeslink.pid`;
|
|
23199
|
+
function subscribeLinkUpdateStatus(listener) {
|
|
23200
|
+
updateEvents2.on("status", listener);
|
|
23201
|
+
return () => updateEvents2.off("status", listener);
|
|
23421
23202
|
}
|
|
23422
|
-
async function
|
|
23423
|
-
|
|
23424
|
-
|
|
23425
|
-
|
|
23203
|
+
async function writeFailedStartState(options, error, targetVersion = null) {
|
|
23204
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
23205
|
+
const state = {
|
|
23206
|
+
state: "failed",
|
|
23207
|
+
job_id: `link_update_${now().getTime().toString(36)}`,
|
|
23208
|
+
pid: null,
|
|
23209
|
+
target_version: targetVersion,
|
|
23210
|
+
started_at: now().toISOString(),
|
|
23211
|
+
finished_at: now().toISOString(),
|
|
23212
|
+
exit_code: null,
|
|
23213
|
+
signal: null,
|
|
23214
|
+
error,
|
|
23215
|
+
manual_command: targetVersion ? manualInstallCommand(targetVersion) : null
|
|
23216
|
+
};
|
|
23217
|
+
await writeUpdateState2(options.paths, state);
|
|
23218
|
+
await emitUpdateStatus2(options.paths);
|
|
23219
|
+
return readLinkUpdateStatus(options.paths);
|
|
23426
23220
|
}
|
|
23427
|
-
async function
|
|
23428
|
-
await
|
|
23429
|
-
|
|
23430
|
-
|
|
23431
|
-
|
|
23432
|
-
const
|
|
23433
|
-
|
|
23434
|
-
|
|
23435
|
-
|
|
23436
|
-
|
|
23437
|
-
|
|
23438
|
-
|
|
23439
|
-
|
|
23440
|
-
|
|
23441
|
-
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23458
|
-
settle();
|
|
23221
|
+
async function readRemoteLinkPolicy(options) {
|
|
23222
|
+
const context = await readLinkReleaseCheckContext(options.paths).catch(
|
|
23223
|
+
() => null
|
|
23224
|
+
);
|
|
23225
|
+
try {
|
|
23226
|
+
const response = await fetchCurrentLinkReleaseFromServer(
|
|
23227
|
+
options,
|
|
23228
|
+
options.fetchImpl ?? fetch
|
|
23229
|
+
);
|
|
23230
|
+
if (!response.ok) {
|
|
23231
|
+
throw new Error(`HermesPilot Server returned HTTP ${response.status}`);
|
|
23232
|
+
}
|
|
23233
|
+
const snapshot = normalizeServerSnapshot(await response.json());
|
|
23234
|
+
if (!snapshot.remote) {
|
|
23235
|
+
return {
|
|
23236
|
+
remote: null,
|
|
23237
|
+
state: "unavailable",
|
|
23238
|
+
issue: snapshot.issue ?? "HermesPilot Server has no Link release policy"
|
|
23239
|
+
};
|
|
23240
|
+
}
|
|
23241
|
+
return {
|
|
23242
|
+
remote: snapshot.remote,
|
|
23243
|
+
state: "fresh",
|
|
23244
|
+
issue: snapshot.issue
|
|
23245
|
+
};
|
|
23246
|
+
} catch (error) {
|
|
23247
|
+
const issue = error instanceof Error ? error.message : String(error);
|
|
23248
|
+
void options.logger?.warn("link_release_server_check_failed", {
|
|
23249
|
+
server_base_url: context?.serverBaseUrl ?? null,
|
|
23250
|
+
release_check_url: context?.releaseCheckUrl ?? null,
|
|
23251
|
+
error: issue
|
|
23459
23252
|
});
|
|
23460
|
-
|
|
23461
|
-
}
|
|
23253
|
+
return { remote: null, state: "unavailable", issue };
|
|
23254
|
+
}
|
|
23462
23255
|
}
|
|
23463
|
-
|
|
23464
|
-
|
|
23465
|
-
|
|
23466
|
-
|
|
23467
|
-
|
|
23468
|
-
|
|
23469
|
-
|
|
23470
|
-
cleanup();
|
|
23471
|
-
reject(error);
|
|
23256
|
+
function normalizeServerSnapshot(payload) {
|
|
23257
|
+
const snapshot = toRecord17(payload);
|
|
23258
|
+
const policy = toNullableRecord2(snapshot.policy);
|
|
23259
|
+
if (!policy) {
|
|
23260
|
+
return {
|
|
23261
|
+
remote: null,
|
|
23262
|
+
issue: readString18(snapshot, "issue")
|
|
23472
23263
|
};
|
|
23473
|
-
|
|
23474
|
-
|
|
23475
|
-
|
|
23264
|
+
}
|
|
23265
|
+
const release = toNullableRecord2(snapshot.release);
|
|
23266
|
+
const currentVersion = readString18(policy, "current_version") ?? readString18(policy, "currentVersion");
|
|
23267
|
+
const minSafeVersion = readString18(policy, "min_safe_version") ?? readString18(policy, "minSafeVersion");
|
|
23268
|
+
if (!currentVersion) {
|
|
23269
|
+
return {
|
|
23270
|
+
remote: null,
|
|
23271
|
+
issue: readString18(snapshot, "issue")
|
|
23476
23272
|
};
|
|
23477
|
-
|
|
23478
|
-
|
|
23479
|
-
|
|
23480
|
-
|
|
23273
|
+
}
|
|
23274
|
+
return {
|
|
23275
|
+
remote: {
|
|
23276
|
+
current_version: currentVersion,
|
|
23277
|
+
min_safe_version: minSafeVersion,
|
|
23278
|
+
target_version: currentVersion,
|
|
23279
|
+
release_url: release ? readString18(release, "release_url") ?? readString18(release, "releaseUrl") : null,
|
|
23280
|
+
published_at: release ? readString18(release, "published_at") ?? readString18(release, "publishedAt") : null
|
|
23281
|
+
},
|
|
23282
|
+
issue: readString18(snapshot, "issue")
|
|
23283
|
+
};
|
|
23481
23284
|
}
|
|
23482
|
-
|
|
23483
|
-
|
|
23484
|
-
|
|
23485
|
-
|
|
23486
|
-
|
|
23487
|
-
|
|
23488
|
-
|
|
23489
|
-
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
|
|
23493
|
-
|
|
23494
|
-
|
|
23495
|
-
|
|
23496
|
-
}
|
|
23497
|
-
}
|
|
23498
|
-
|
|
23499
|
-
|
|
23500
|
-
const scriptPath = currentCliScriptPath();
|
|
23501
|
-
const child = spawn4(process.execPath, [scriptPath, "daemon-supervisor"], {
|
|
23502
|
-
detached: true,
|
|
23503
|
-
stdio: "ignore",
|
|
23504
|
-
env: process.env
|
|
23505
|
-
});
|
|
23506
|
-
child.unref();
|
|
23507
|
-
for (let index = 0; index < 12; index += 1) {
|
|
23508
|
-
await wait(250);
|
|
23509
|
-
const next = await getDaemonStatus(paths);
|
|
23510
|
-
if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
|
|
23511
|
-
return next;
|
|
23285
|
+
async function fetchCurrentLinkReleaseFromServer(options, fetcher) {
|
|
23286
|
+
const config = await loadConfig(options.paths);
|
|
23287
|
+
const url = new URL(SERVER_LINK_CURRENT_RELEASE_PATH, config.serverBaseUrl);
|
|
23288
|
+
url.searchParams.set("channel", "stable");
|
|
23289
|
+
url.searchParams.set("lang", "en");
|
|
23290
|
+
const controller = new AbortController();
|
|
23291
|
+
const timer = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
|
|
23292
|
+
try {
|
|
23293
|
+
return await fetcher(url, {
|
|
23294
|
+
headers: {
|
|
23295
|
+
accept: "application/json",
|
|
23296
|
+
"user-agent": `HermesPilot-Link/${LINK_VERSION}`
|
|
23297
|
+
},
|
|
23298
|
+
signal: controller.signal
|
|
23299
|
+
});
|
|
23300
|
+
} catch (error) {
|
|
23301
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
23302
|
+
throw new Error("HermesPilot Server Link release check timed out");
|
|
23512
23303
|
}
|
|
23304
|
+
throw error;
|
|
23305
|
+
} finally {
|
|
23306
|
+
clearTimeout(timer);
|
|
23513
23307
|
}
|
|
23514
|
-
return await getDaemonStatus(paths);
|
|
23515
23308
|
}
|
|
23516
|
-
|
|
23517
|
-
|
|
23518
|
-
|
|
23519
|
-
|
|
23520
|
-
|
|
23521
|
-
});
|
|
23522
|
-
const scriptPath = currentCliScriptPath();
|
|
23523
|
-
const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
|
|
23524
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
23525
|
-
env: process.env
|
|
23526
|
-
});
|
|
23527
|
-
const write = (chunk) => {
|
|
23528
|
-
void log.write(chunk);
|
|
23309
|
+
function buildOfficialInstallCommand(targetVersion) {
|
|
23310
|
+
const env = {
|
|
23311
|
+
HERMESLINK_VERSION: targetVersion,
|
|
23312
|
+
HERMESLINK_YES: "1",
|
|
23313
|
+
HERMESLINK_REQUIRE_RESTART_VERIFY: "1"
|
|
23529
23314
|
};
|
|
23530
|
-
|
|
23531
|
-
|
|
23532
|
-
|
|
23533
|
-
|
|
23534
|
-
|
|
23535
|
-
|
|
23536
|
-
|
|
23537
|
-
|
|
23315
|
+
if (process.platform === "win32") {
|
|
23316
|
+
return {
|
|
23317
|
+
command: `Invoke-RestMethod ${OFFICIAL_WINDOWS_INSTALLER_URL} | Invoke-Expression`,
|
|
23318
|
+
displayCommand: `$env:HERMESLINK_VERSION="${targetVersion}"; $env:HERMESLINK_YES="1"; $env:HERMESLINK_REQUIRE_RESTART_VERIFY="1"; Invoke-RestMethod ${OFFICIAL_WINDOWS_INSTALLER_URL} | Invoke-Expression`,
|
|
23319
|
+
env,
|
|
23320
|
+
source: "official-installer"
|
|
23321
|
+
};
|
|
23322
|
+
}
|
|
23323
|
+
return {
|
|
23324
|
+
command: `curl -fsSL ${OFFICIAL_UNIX_INSTALLER_URL} | bash`,
|
|
23325
|
+
displayCommand: `HERMESLINK_VERSION=${targetVersion} HERMESLINK_YES=1 HERMESLINK_REQUIRE_RESTART_VERIFY=1 sh -c 'curl -fsSL ${OFFICIAL_UNIX_INSTALLER_URL} | bash'`,
|
|
23326
|
+
env,
|
|
23327
|
+
source: "official-installer"
|
|
23538
23328
|
};
|
|
23539
|
-
|
|
23540
|
-
|
|
23541
|
-
const
|
|
23542
|
-
|
|
23543
|
-
|
|
23544
|
-
}).catch((error) => {
|
|
23545
|
-
write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor failed: ${error instanceof Error ? error.message : String(error)}
|
|
23546
|
-
`);
|
|
23547
|
-
return { code: 1, signal: null };
|
|
23548
|
-
});
|
|
23549
|
-
process.off("SIGINT", forwardStop);
|
|
23550
|
-
process.off("SIGTERM", forwardStop);
|
|
23551
|
-
write(
|
|
23552
|
-
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${result.code ?? "null"} signal=${result.signal ?? "null"}
|
|
23553
|
-
`
|
|
23554
|
-
);
|
|
23555
|
-
await log.flush();
|
|
23556
|
-
return result.code ?? (result.signal ? 0 : 1);
|
|
23557
|
-
}
|
|
23558
|
-
async function probeLocalLinkService(options) {
|
|
23559
|
-
const unreachable = {
|
|
23560
|
-
reachable: false,
|
|
23561
|
-
reusable: false,
|
|
23562
|
-
linkId: null,
|
|
23563
|
-
version: null
|
|
23329
|
+
}
|
|
23330
|
+
function spawnInstallCommand(input) {
|
|
23331
|
+
const env = {
|
|
23332
|
+
...process.env,
|
|
23333
|
+
...input.env
|
|
23564
23334
|
};
|
|
23565
|
-
|
|
23566
|
-
|
|
23567
|
-
|
|
23568
|
-
|
|
23569
|
-
|
|
23570
|
-
|
|
23571
|
-
|
|
23572
|
-
|
|
23573
|
-
|
|
23574
|
-
|
|
23575
|
-
|
|
23576
|
-
|
|
23577
|
-
const payload = await response.json().catch(() => null);
|
|
23578
|
-
if (!payload || payload.api_version !== 1) {
|
|
23579
|
-
return unreachable;
|
|
23335
|
+
if (process.platform === "win32") {
|
|
23336
|
+
return spawn4(
|
|
23337
|
+
"powershell.exe",
|
|
23338
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", input.command],
|
|
23339
|
+
{
|
|
23340
|
+
env,
|
|
23341
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
23342
|
+
windowsHide: true,
|
|
23343
|
+
detached: false,
|
|
23344
|
+
shell: false
|
|
23345
|
+
}
|
|
23346
|
+
);
|
|
23580
23347
|
}
|
|
23581
|
-
|
|
23348
|
+
return spawn4("/bin/sh", ["-lc", input.command], {
|
|
23349
|
+
env,
|
|
23350
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
23351
|
+
windowsHide: true,
|
|
23352
|
+
detached: false,
|
|
23353
|
+
shell: false
|
|
23354
|
+
});
|
|
23355
|
+
}
|
|
23356
|
+
async function readLinkReleaseCheckContext(paths) {
|
|
23357
|
+
const config = await loadConfig(paths);
|
|
23358
|
+
const url = new URL(SERVER_LINK_CURRENT_RELEASE_PATH, config.serverBaseUrl);
|
|
23359
|
+
url.searchParams.set("channel", "stable");
|
|
23360
|
+
url.searchParams.set("lang", "en");
|
|
23582
23361
|
return {
|
|
23583
|
-
|
|
23584
|
-
|
|
23585
|
-
linkId,
|
|
23586
|
-
version: typeof payload.version === "string" ? payload.version : null
|
|
23362
|
+
serverBaseUrl: config.serverBaseUrl,
|
|
23363
|
+
releaseCheckUrl: url.toString()
|
|
23587
23364
|
};
|
|
23588
23365
|
}
|
|
23589
|
-
|
|
23590
|
-
|
|
23591
|
-
|
|
23592
|
-
return status;
|
|
23593
|
-
}
|
|
23594
|
-
try {
|
|
23595
|
-
process.kill(status.pid, "SIGTERM");
|
|
23596
|
-
} catch {
|
|
23597
|
-
await rm9(pidFilePath(paths), { force: true }).catch(() => void 0);
|
|
23598
|
-
return await getDaemonStatus(paths);
|
|
23366
|
+
function computeLinkUpdateState(localVersion, remote) {
|
|
23367
|
+
if (!remote?.current_version) {
|
|
23368
|
+
return "unknown";
|
|
23599
23369
|
}
|
|
23600
|
-
|
|
23601
|
-
|
|
23602
|
-
if (!isProcessAlive3(status.pid)) {
|
|
23603
|
-
break;
|
|
23604
|
-
}
|
|
23370
|
+
if (remote.min_safe_version && compareSemver3(localVersion, remote.min_safe_version) < 0) {
|
|
23371
|
+
return "unsafe";
|
|
23605
23372
|
}
|
|
23606
|
-
|
|
23607
|
-
|
|
23608
|
-
|
|
23609
|
-
} catch {
|
|
23610
|
-
}
|
|
23611
|
-
for (let index = 0; index < 10; index += 1) {
|
|
23612
|
-
await wait(250);
|
|
23613
|
-
if (!isProcessAlive3(status.pid)) {
|
|
23614
|
-
break;
|
|
23615
|
-
}
|
|
23616
|
-
}
|
|
23373
|
+
const diff = compareSemver3(localVersion, remote.current_version);
|
|
23374
|
+
if (diff < 0) {
|
|
23375
|
+
return "update_available";
|
|
23617
23376
|
}
|
|
23618
|
-
if (
|
|
23619
|
-
|
|
23377
|
+
if (diff > 0) {
|
|
23378
|
+
return "ahead_of_current";
|
|
23620
23379
|
}
|
|
23621
|
-
return
|
|
23380
|
+
return "current";
|
|
23622
23381
|
}
|
|
23623
|
-
async function
|
|
23624
|
-
|
|
23625
|
-
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
};
|
|
23382
|
+
async function emitUpdateStatus2(paths) {
|
|
23383
|
+
updateEvents2.emit("status", await readLinkUpdateStatus(paths));
|
|
23384
|
+
}
|
|
23385
|
+
async function writeUpdateState2(paths, state) {
|
|
23386
|
+
await writeJsonFile(updateStatePath2(paths), state);
|
|
23387
|
+
}
|
|
23388
|
+
async function readUpdateLogLines2(paths) {
|
|
23389
|
+
const raw = await readFile17(updateLogPath2(paths), "utf8").catch(() => "");
|
|
23390
|
+
if (!raw.trim()) {
|
|
23391
|
+
return [];
|
|
23634
23392
|
}
|
|
23635
|
-
return
|
|
23636
|
-
|
|
23637
|
-
|
|
23638
|
-
pidFile,
|
|
23639
|
-
logFile: daemonLogFile(paths)
|
|
23640
|
-
};
|
|
23393
|
+
return raw.split(/\r?\n/u).map((line) => line.trimEnd()).filter(Boolean).slice(-MAX_UPDATE_LOG_LINES2).map(
|
|
23394
|
+
(line) => line.length > MAX_OUTPUT_LINE_LENGTH3 ? `${line.slice(0, MAX_OUTPUT_LINE_LENGTH3)}...` : line
|
|
23395
|
+
);
|
|
23641
23396
|
}
|
|
23642
|
-
function
|
|
23643
|
-
return
|
|
23397
|
+
function updateStatePath2(paths) {
|
|
23398
|
+
return path24.join(paths.runDir, "link-update-state.json");
|
|
23644
23399
|
}
|
|
23645
|
-
function
|
|
23646
|
-
return
|
|
23400
|
+
function updateLogPath2(paths) {
|
|
23401
|
+
return path24.join(paths.logsDir, UPDATE_LOG_FILE2);
|
|
23647
23402
|
}
|
|
23648
|
-
async function
|
|
23649
|
-
const
|
|
23650
|
-
|
|
23651
|
-
|
|
23652
|
-
|
|
23653
|
-
|
|
23654
|
-
|
|
23403
|
+
async function clearUpdateLogFiles2(paths) {
|
|
23404
|
+
const primary = updateLogPath2(paths);
|
|
23405
|
+
await Promise.all([
|
|
23406
|
+
rm8(primary, { force: true }).catch(() => void 0),
|
|
23407
|
+
...Array.from(
|
|
23408
|
+
{ length: UPDATE_LOG_MAX_FILES2 },
|
|
23409
|
+
(_, index) => rm8(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
|
|
23410
|
+
)
|
|
23411
|
+
]);
|
|
23655
23412
|
}
|
|
23656
|
-
function
|
|
23657
|
-
|
|
23658
|
-
process.kill(pid, 0);
|
|
23659
|
-
return true;
|
|
23660
|
-
} catch {
|
|
23661
|
-
return false;
|
|
23662
|
-
}
|
|
23413
|
+
function manualInstallCommand(version) {
|
|
23414
|
+
return `npm install -g ${LINK_NPM_PACKAGE}@${version}`;
|
|
23663
23415
|
}
|
|
23664
|
-
|
|
23665
|
-
|
|
23666
|
-
|
|
23667
|
-
|
|
23416
|
+
function isValidReleaseVersion(version) {
|
|
23417
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(
|
|
23418
|
+
version
|
|
23419
|
+
);
|
|
23420
|
+
}
|
|
23421
|
+
function compareSemver3(left, right) {
|
|
23422
|
+
const leftParts = parseSemver(left);
|
|
23423
|
+
const rightParts = parseSemver(right);
|
|
23424
|
+
for (let index = 0; index < 3; index += 1) {
|
|
23425
|
+
const diff = leftParts[index] - rightParts[index];
|
|
23426
|
+
if (diff !== 0) {
|
|
23427
|
+
return diff;
|
|
23428
|
+
}
|
|
23668
23429
|
}
|
|
23669
|
-
return
|
|
23430
|
+
return 0;
|
|
23670
23431
|
}
|
|
23671
|
-
function
|
|
23672
|
-
|
|
23432
|
+
function parseSemver(value) {
|
|
23433
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/u.exec(value.trim());
|
|
23434
|
+
return [
|
|
23435
|
+
Number.parseInt(match?.[1] ?? "0", 10),
|
|
23436
|
+
Number.parseInt(match?.[2] ?? "0", 10),
|
|
23437
|
+
Number.parseInt(match?.[3] ?? "0", 10)
|
|
23438
|
+
];
|
|
23673
23439
|
}
|
|
23674
|
-
|
|
23675
|
-
|
|
23676
|
-
|
|
23677
|
-
var LINK_NPM_PACKAGE = "@hermespilot/link";
|
|
23678
|
-
var UPDATE_LOG_FILE2 = "link-update.log";
|
|
23679
|
-
var UPDATE_LOG_MAX_FILES2 = 3;
|
|
23680
|
-
var UPDATE_FETCH_TIMEOUT_MS = 5e3;
|
|
23681
|
-
var MAX_UPDATE_LOG_LINES2 = 240;
|
|
23682
|
-
var MAX_OUTPUT_LINE_LENGTH3 = 1200;
|
|
23683
|
-
var AUTO_RESTART_DELAY_MS = 1500;
|
|
23684
|
-
var updateEvents2 = new EventEmitter4();
|
|
23685
|
-
var runningUpdate2 = null;
|
|
23686
|
-
async function readLinkUpdateCheck(options) {
|
|
23687
|
-
const remoteResult = await readRemoteLinkPolicy(options);
|
|
23688
|
-
const remote = remoteResult.remote;
|
|
23689
|
-
const state = computeLinkUpdateState(LINK_VERSION, remote);
|
|
23690
|
-
const targetVersion = remote?.target_version ?? null;
|
|
23691
|
-
return {
|
|
23692
|
-
ok: true,
|
|
23693
|
-
local: {
|
|
23694
|
-
version: LINK_VERSION,
|
|
23695
|
-
raw: LINK_VERSION
|
|
23696
|
-
},
|
|
23697
|
-
remote,
|
|
23698
|
-
state,
|
|
23699
|
-
update_available: state === "update_available" || state === "unsafe" || state === "blocked",
|
|
23700
|
-
unsafe: state === "unsafe",
|
|
23701
|
-
blocked: state === "blocked",
|
|
23702
|
-
check_state: remoteResult.state,
|
|
23703
|
-
issue: remoteResult.issue,
|
|
23704
|
-
manual: {
|
|
23705
|
-
command: targetVersion ? manualInstallCommand(targetVersion) : null,
|
|
23706
|
-
package: LINK_NPM_PACKAGE,
|
|
23707
|
-
version: targetVersion
|
|
23708
|
-
}
|
|
23709
|
-
};
|
|
23440
|
+
function isRecentRunningState3(state, now = Date.now()) {
|
|
23441
|
+
const startedAt = state.started_at ? Date.parse(state.started_at) : Number.NaN;
|
|
23442
|
+
return Number.isFinite(startedAt) && now - startedAt < 1e4;
|
|
23710
23443
|
}
|
|
23711
|
-
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
return current;
|
|
23715
|
-
}
|
|
23716
|
-
const check = await readLinkUpdateCheck(options);
|
|
23717
|
-
const targetVersion = check.remote?.target_version ?? null;
|
|
23718
|
-
if (!targetVersion) {
|
|
23719
|
-
return writeFailedStartState(
|
|
23720
|
-
options,
|
|
23721
|
-
"HermesPilot Server has no Link target version."
|
|
23722
|
-
);
|
|
23444
|
+
function isProcessAlive3(pid) {
|
|
23445
|
+
if (!pid || pid <= 0) {
|
|
23446
|
+
return false;
|
|
23723
23447
|
}
|
|
23724
|
-
|
|
23725
|
-
|
|
23726
|
-
|
|
23727
|
-
|
|
23728
|
-
|
|
23729
|
-
);
|
|
23448
|
+
try {
|
|
23449
|
+
process.kill(pid, 0);
|
|
23450
|
+
return true;
|
|
23451
|
+
} catch {
|
|
23452
|
+
return false;
|
|
23730
23453
|
}
|
|
23731
|
-
|
|
23732
|
-
|
|
23733
|
-
|
|
23734
|
-
|
|
23735
|
-
|
|
23454
|
+
}
|
|
23455
|
+
function toRecord17(value) {
|
|
23456
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
23457
|
+
}
|
|
23458
|
+
function toNullableRecord2(value) {
|
|
23459
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
23460
|
+
}
|
|
23461
|
+
function readString18(payload, key) {
|
|
23462
|
+
const value = payload[key];
|
|
23463
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
23464
|
+
}
|
|
23465
|
+
|
|
23466
|
+
// src/pairing/pairing.ts
|
|
23467
|
+
import path25 from "path";
|
|
23468
|
+
import { rm as rm9 } from "fs/promises";
|
|
23469
|
+
|
|
23470
|
+
// src/relay/bootstrap.ts
|
|
23471
|
+
var RelayNetworkError = class extends Error {
|
|
23472
|
+
constructor(relayBaseUrl, causeMessage) {
|
|
23473
|
+
super(
|
|
23474
|
+
`Cannot reach Hermes Relay at ${relayBaseUrl}. Please check your network connection, VPN, or proxy settings, then try again.`
|
|
23736
23475
|
);
|
|
23476
|
+
this.relayBaseUrl = relayBaseUrl;
|
|
23477
|
+
this.causeMessage = causeMessage;
|
|
23737
23478
|
}
|
|
23738
|
-
|
|
23739
|
-
|
|
23740
|
-
|
|
23741
|
-
|
|
23742
|
-
|
|
23743
|
-
|
|
23744
|
-
|
|
23745
|
-
|
|
23746
|
-
|
|
23747
|
-
|
|
23748
|
-
const manualCommand = manualInstallCommand(targetVersion);
|
|
23749
|
-
const started = {
|
|
23750
|
-
state: "running",
|
|
23751
|
-
job_id: jobId,
|
|
23752
|
-
pid: null,
|
|
23753
|
-
target_version: targetVersion,
|
|
23754
|
-
started_at: startedAt,
|
|
23755
|
-
finished_at: null,
|
|
23756
|
-
exit_code: null,
|
|
23757
|
-
signal: null,
|
|
23758
|
-
error: null,
|
|
23759
|
-
manual_command: manualCommand
|
|
23479
|
+
relayBaseUrl;
|
|
23480
|
+
causeMessage;
|
|
23481
|
+
};
|
|
23482
|
+
async function bootstrapRelayLink(options) {
|
|
23483
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
23484
|
+
const baseUrl = options.relayBaseUrl.replace(/\/+$/u, "");
|
|
23485
|
+
const commonPayload = {
|
|
23486
|
+
install_id: options.identity.install_id,
|
|
23487
|
+
link_id: options.identity.link_id ?? void 0,
|
|
23488
|
+
public_key_pem: options.identity.public_key_pem
|
|
23760
23489
|
};
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
|
|
23490
|
+
const challenge = await postJson(
|
|
23491
|
+
fetcher,
|
|
23492
|
+
`${baseUrl}/api/v1/relay/link/challenge`,
|
|
23493
|
+
options.relayBootstrapToken,
|
|
23494
|
+
commonPayload
|
|
23766
23495
|
);
|
|
23767
|
-
|
|
23768
|
-
|
|
23769
|
-
|
|
23770
|
-
const
|
|
23771
|
-
|
|
23772
|
-
|
|
23496
|
+
if (challenge.ok !== true || typeof challenge.nonce !== "string") {
|
|
23497
|
+
throw new Error("Relay did not return a valid install challenge");
|
|
23498
|
+
}
|
|
23499
|
+
const proof = {
|
|
23500
|
+
nonce: challenge.nonce,
|
|
23501
|
+
signature: signRelayNonce(options.identity, challenge.nonce)
|
|
23502
|
+
};
|
|
23503
|
+
const assigned = await postJson(
|
|
23504
|
+
fetcher,
|
|
23505
|
+
`${baseUrl}/api/v1/relay/link/bootstrap`,
|
|
23506
|
+
options.relayBootstrapToken,
|
|
23773
23507
|
{
|
|
23774
|
-
|
|
23775
|
-
|
|
23776
|
-
detached: false,
|
|
23777
|
-
shell: false
|
|
23508
|
+
...commonPayload,
|
|
23509
|
+
proof
|
|
23778
23510
|
}
|
|
23779
23511
|
);
|
|
23780
|
-
|
|
23781
|
-
|
|
23782
|
-
|
|
23783
|
-
|
|
23784
|
-
|
|
23512
|
+
if (assigned.ok !== true || typeof assigned.link_id !== "string") {
|
|
23513
|
+
throw new Error("Relay did not return a valid link_id");
|
|
23514
|
+
}
|
|
23515
|
+
await saveAssignedLinkId(assigned.link_id, options.paths ?? resolveRuntimePaths());
|
|
23516
|
+
return {
|
|
23517
|
+
linkId: assigned.link_id,
|
|
23518
|
+
reused: assigned.reused === true
|
|
23785
23519
|
};
|
|
23786
|
-
child.stdout?.on("data", (chunk) => {
|
|
23787
|
-
void appendChunk(chunk);
|
|
23788
|
-
});
|
|
23789
|
-
child.stderr?.on("data", (chunk) => {
|
|
23790
|
-
void appendChunk(chunk);
|
|
23791
|
-
});
|
|
23792
|
-
runningUpdate2 = new Promise((resolve) => {
|
|
23793
|
-
child.on("error", (error) => {
|
|
23794
|
-
void (async () => {
|
|
23795
|
-
const failed = {
|
|
23796
|
-
...started,
|
|
23797
|
-
state: "failed",
|
|
23798
|
-
finished_at: now().toISOString(),
|
|
23799
|
-
error: error.message
|
|
23800
|
-
};
|
|
23801
|
-
await writer.write(
|
|
23802
|
-
`
|
|
23803
|
-
[failed] link update failed to start: ${error.message}
|
|
23804
|
-
`
|
|
23805
|
-
);
|
|
23806
|
-
await writeUpdateState2(options.paths, failed);
|
|
23807
|
-
await emitUpdateStatus2(options.paths);
|
|
23808
|
-
void options.logger?.error("link_update_spawn_failed", {
|
|
23809
|
-
job_id: jobId,
|
|
23810
|
-
target_version: targetVersion,
|
|
23811
|
-
error: error.message
|
|
23812
|
-
});
|
|
23813
|
-
resolve(await readLinkUpdateStatus(options.paths));
|
|
23814
|
-
})();
|
|
23815
|
-
});
|
|
23816
|
-
child.on("close", (code, signal) => {
|
|
23817
|
-
void (async () => {
|
|
23818
|
-
const succeeded = code === 0;
|
|
23819
|
-
const state = {
|
|
23820
|
-
...started,
|
|
23821
|
-
state: succeeded ? "restart_required" : "failed",
|
|
23822
|
-
finished_at: now().toISOString(),
|
|
23823
|
-
exit_code: code,
|
|
23824
|
-
signal,
|
|
23825
|
-
error: succeeded ? null : `npm install exited with code ${code ?? "unknown"}`
|
|
23826
|
-
};
|
|
23827
|
-
await writer.write(
|
|
23828
|
-
`
|
|
23829
|
-
=== link update finished ${state.finished_at} exit=${code ?? "null"} signal=${signal ?? "null"} ===
|
|
23830
|
-
`
|
|
23831
|
-
);
|
|
23832
|
-
if (succeeded) {
|
|
23833
|
-
await writer.write(
|
|
23834
|
-
`
|
|
23835
|
-
[restart-scheduled] Hermes Link will restart automatically. If it does not reconnect, run \`${LINK_COMMAND} restart\` on this computer.
|
|
23836
|
-
`
|
|
23837
|
-
);
|
|
23838
|
-
}
|
|
23839
|
-
await writeUpdateState2(options.paths, state);
|
|
23840
|
-
await emitUpdateStatus2(options.paths);
|
|
23841
|
-
if (succeeded) {
|
|
23842
|
-
await writer.flush();
|
|
23843
|
-
scheduleAutomaticRestart(options);
|
|
23844
|
-
}
|
|
23845
|
-
void options.logger?.info(
|
|
23846
|
-
succeeded ? "link_update_restart_required" : "link_update_failed",
|
|
23847
|
-
{
|
|
23848
|
-
job_id: jobId,
|
|
23849
|
-
target_version: targetVersion,
|
|
23850
|
-
exit_code: code,
|
|
23851
|
-
signal: signal ?? null
|
|
23852
|
-
}
|
|
23853
|
-
);
|
|
23854
|
-
resolve(await readLinkUpdateStatus(options.paths));
|
|
23855
|
-
})();
|
|
23856
|
-
});
|
|
23857
|
-
}).finally(() => {
|
|
23858
|
-
runningUpdate2 = null;
|
|
23859
|
-
});
|
|
23860
|
-
await emitUpdateStatus2(options.paths);
|
|
23861
|
-
void options.logger?.info("link_update_started", {
|
|
23862
|
-
job_id: jobId,
|
|
23863
|
-
pid: child.pid ?? null,
|
|
23864
|
-
target_version: targetVersion,
|
|
23865
|
-
log_path: writer.filePath
|
|
23866
|
-
});
|
|
23867
|
-
return readLinkUpdateStatus(options.paths);
|
|
23868
23520
|
}
|
|
23869
|
-
function
|
|
23870
|
-
|
|
23871
|
-
|
|
23872
|
-
|
|
23873
|
-
|
|
23874
|
-
|
|
23875
|
-
|
|
23876
|
-
|
|
23877
|
-
|
|
23878
|
-
|
|
23879
|
-
void options.logger?.info("link_update_restart_scheduled", {
|
|
23880
|
-
delay_ms: AUTO_RESTART_DELAY_MS,
|
|
23881
|
-
command: `${LINK_COMMAND} restart`
|
|
23521
|
+
async function postJson(fetcher, url, token, body) {
|
|
23522
|
+
let response;
|
|
23523
|
+
try {
|
|
23524
|
+
response = await fetcher(url, {
|
|
23525
|
+
method: "POST",
|
|
23526
|
+
headers: {
|
|
23527
|
+
authorization: `Bearer ${token}`,
|
|
23528
|
+
"content-type": "application/json"
|
|
23529
|
+
},
|
|
23530
|
+
body: JSON.stringify(body)
|
|
23882
23531
|
});
|
|
23883
|
-
}
|
|
23532
|
+
} catch (error) {
|
|
23533
|
+
const baseUrl = new URL(url).origin;
|
|
23534
|
+
throw new RelayNetworkError(
|
|
23535
|
+
baseUrl,
|
|
23536
|
+
error instanceof Error ? error.message : String(error)
|
|
23537
|
+
);
|
|
23538
|
+
}
|
|
23539
|
+
const payload = await response.json().catch(() => null);
|
|
23540
|
+
if (!response.ok) {
|
|
23541
|
+
const message = readErrorMessage3(payload) ?? `Relay request failed with HTTP ${response.status}`;
|
|
23542
|
+
throw new Error(message);
|
|
23543
|
+
}
|
|
23544
|
+
if (!payload) {
|
|
23545
|
+
throw new Error("Relay returned an empty response");
|
|
23546
|
+
}
|
|
23547
|
+
return payload;
|
|
23884
23548
|
}
|
|
23885
|
-
|
|
23886
|
-
|
|
23887
|
-
|
|
23888
|
-
if (compareSemver3(LINK_VERSION, state.target_version) >= 0) {
|
|
23889
|
-
state = {
|
|
23890
|
-
...state,
|
|
23891
|
-
state: "succeeded",
|
|
23892
|
-
finished_at: state.finished_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
23893
|
-
};
|
|
23894
|
-
await writeUpdateState2(paths, state);
|
|
23895
|
-
}
|
|
23549
|
+
function readErrorMessage3(payload) {
|
|
23550
|
+
if (typeof payload !== "object" || payload === null) {
|
|
23551
|
+
return null;
|
|
23896
23552
|
}
|
|
23897
|
-
|
|
23898
|
-
|
|
23899
|
-
|
|
23900
|
-
state: "failed",
|
|
23901
|
-
finished_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23902
|
-
error: state.error ?? "Link update was interrupted before Hermes Link could observe completion."
|
|
23903
|
-
};
|
|
23904
|
-
await writeUpdateState2(paths, state);
|
|
23553
|
+
const error = payload.error;
|
|
23554
|
+
if (typeof error !== "object" || error === null) {
|
|
23555
|
+
return null;
|
|
23905
23556
|
}
|
|
23906
|
-
|
|
23907
|
-
|
|
23908
|
-
|
|
23909
|
-
|
|
23910
|
-
|
|
23911
|
-
|
|
23912
|
-
|
|
23913
|
-
|
|
23914
|
-
|
|
23915
|
-
|
|
23916
|
-
|
|
23917
|
-
|
|
23918
|
-
|
|
23919
|
-
|
|
23557
|
+
const message = error.message;
|
|
23558
|
+
return typeof message === "string" ? message : null;
|
|
23559
|
+
}
|
|
23560
|
+
|
|
23561
|
+
// src/runtime/system-info.ts
|
|
23562
|
+
import { execFileSync } from "child_process";
|
|
23563
|
+
import { readFileSync } from "fs";
|
|
23564
|
+
import os4 from "os";
|
|
23565
|
+
function readLinkSystemInfo() {
|
|
23566
|
+
const platform = process.platform;
|
|
23567
|
+
const hostname = readHostname(platform);
|
|
23568
|
+
const osLabel = readOsLabel(platform);
|
|
23569
|
+
const defaultDisplayName = buildDefaultDisplayName({ hostname, osLabel, platform });
|
|
23570
|
+
return {
|
|
23571
|
+
platform,
|
|
23572
|
+
hostname,
|
|
23573
|
+
osLabel,
|
|
23574
|
+
defaultDisplayName
|
|
23920
23575
|
};
|
|
23921
23576
|
}
|
|
23922
|
-
function
|
|
23923
|
-
|
|
23924
|
-
|
|
23577
|
+
function buildDefaultDisplayName(input) {
|
|
23578
|
+
const hostname = normalizeText(input.hostname);
|
|
23579
|
+
const osLabel = normalizeText(input.osLabel);
|
|
23580
|
+
if (hostname) {
|
|
23581
|
+
return truncateText(hostname, 128);
|
|
23582
|
+
}
|
|
23583
|
+
return truncateText(hostname ?? osLabel ?? `Hermes Link ${input.platform}`, 128);
|
|
23925
23584
|
}
|
|
23926
|
-
|
|
23927
|
-
const
|
|
23928
|
-
const
|
|
23929
|
-
|
|
23930
|
-
|
|
23931
|
-
|
|
23932
|
-
|
|
23933
|
-
|
|
23934
|
-
|
|
23935
|
-
|
|
23936
|
-
signal: null,
|
|
23937
|
-
error,
|
|
23938
|
-
manual_command: targetVersion ? manualInstallCommand(targetVersion) : null
|
|
23939
|
-
};
|
|
23940
|
-
await writeUpdateState2(options.paths, state);
|
|
23941
|
-
await emitUpdateStatus2(options.paths);
|
|
23942
|
-
return readLinkUpdateStatus(options.paths);
|
|
23585
|
+
function parseLinuxOsRelease(content) {
|
|
23586
|
+
const values = /* @__PURE__ */ new Map();
|
|
23587
|
+
for (const line of content.split(/\r?\n/u)) {
|
|
23588
|
+
const match = /^([A-Z0-9_]+)=(.*)$/u.exec(line.trim());
|
|
23589
|
+
if (!match) {
|
|
23590
|
+
continue;
|
|
23591
|
+
}
|
|
23592
|
+
values.set(match[1], unquoteOsReleaseValue(match[2]));
|
|
23593
|
+
}
|
|
23594
|
+
return normalizeText(values.get("PRETTY_NAME")) ?? buildLinuxName(values);
|
|
23943
23595
|
}
|
|
23944
|
-
|
|
23945
|
-
|
|
23946
|
-
(
|
|
23947
|
-
|
|
23948
|
-
|
|
23949
|
-
const response = await fetchCurrentLinkReleaseFromServer(
|
|
23950
|
-
options,
|
|
23951
|
-
options.fetchImpl ?? fetch
|
|
23952
|
-
);
|
|
23953
|
-
if (!response.ok) {
|
|
23954
|
-
throw new Error(`HermesPilot Server returned HTTP ${response.status}`);
|
|
23596
|
+
function readHostname(platform) {
|
|
23597
|
+
if (platform === "darwin") {
|
|
23598
|
+
const computerName = readCommandOutput("scutil", ["--get", "ComputerName"]);
|
|
23599
|
+
if (computerName) {
|
|
23600
|
+
return computerName;
|
|
23955
23601
|
}
|
|
23956
|
-
|
|
23957
|
-
|
|
23958
|
-
|
|
23959
|
-
|
|
23960
|
-
|
|
23961
|
-
|
|
23962
|
-
|
|
23602
|
+
}
|
|
23603
|
+
return normalizeText(os4.hostname());
|
|
23604
|
+
}
|
|
23605
|
+
function readOsLabel(platform) {
|
|
23606
|
+
if (platform === "darwin") {
|
|
23607
|
+
const version = readCommandOutput("sw_vers", ["-productVersion"]);
|
|
23608
|
+
return version ? `macOS ${version}` : "macOS";
|
|
23609
|
+
}
|
|
23610
|
+
if (platform === "linux") {
|
|
23611
|
+
return readLinuxOsRelease() ?? `Linux ${os4.release()}`;
|
|
23612
|
+
}
|
|
23613
|
+
if (platform === "win32") {
|
|
23614
|
+
return `Windows ${os4.release()}`;
|
|
23615
|
+
}
|
|
23616
|
+
return `${os4.type()} ${os4.release()}`.trim();
|
|
23617
|
+
}
|
|
23618
|
+
function readLinuxOsRelease() {
|
|
23619
|
+
for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
|
|
23620
|
+
try {
|
|
23621
|
+
return parseLinuxOsRelease(readFileSync(file, "utf8"));
|
|
23622
|
+
} catch {
|
|
23963
23623
|
}
|
|
23964
|
-
|
|
23965
|
-
|
|
23966
|
-
|
|
23967
|
-
|
|
23968
|
-
|
|
23969
|
-
|
|
23970
|
-
|
|
23971
|
-
|
|
23972
|
-
|
|
23973
|
-
release_check_url: context?.releaseCheckUrl ?? null,
|
|
23974
|
-
error: issue
|
|
23624
|
+
}
|
|
23625
|
+
return null;
|
|
23626
|
+
}
|
|
23627
|
+
function readCommandOutput(command, args) {
|
|
23628
|
+
try {
|
|
23629
|
+
const output = execFileSync(command, args, {
|
|
23630
|
+
encoding: "utf8",
|
|
23631
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
23632
|
+
timeout: 1e3
|
|
23975
23633
|
});
|
|
23976
|
-
return
|
|
23634
|
+
return normalizeText(output);
|
|
23635
|
+
} catch {
|
|
23636
|
+
return null;
|
|
23977
23637
|
}
|
|
23978
23638
|
}
|
|
23979
|
-
function
|
|
23980
|
-
const
|
|
23981
|
-
const
|
|
23982
|
-
if (
|
|
23639
|
+
function buildLinuxName(values) {
|
|
23640
|
+
const name = normalizeText(values.get("NAME"));
|
|
23641
|
+
const version = normalizeText(values.get("VERSION_ID")) ?? normalizeText(values.get("VERSION"));
|
|
23642
|
+
if (name && version) {
|
|
23643
|
+
return `${name} ${version}`;
|
|
23644
|
+
}
|
|
23645
|
+
return name ?? version;
|
|
23646
|
+
}
|
|
23647
|
+
function unquoteOsReleaseValue(value) {
|
|
23648
|
+
const trimmed = value.trim();
|
|
23649
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
23650
|
+
return trimmed.slice(1, -1).replace(/\\(["'`$\\])/gu, "$1");
|
|
23651
|
+
}
|
|
23652
|
+
return trimmed;
|
|
23653
|
+
}
|
|
23654
|
+
function normalizeText(value) {
|
|
23655
|
+
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
23656
|
+
return normalized ? normalized : null;
|
|
23657
|
+
}
|
|
23658
|
+
function truncateText(value, maxLength) {
|
|
23659
|
+
return value.length > maxLength ? value.slice(0, maxLength).trimEnd() : value;
|
|
23660
|
+
}
|
|
23661
|
+
|
|
23662
|
+
// src/topology/network.ts
|
|
23663
|
+
import os6 from "os";
|
|
23664
|
+
|
|
23665
|
+
// src/topology/environment.ts
|
|
23666
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
23667
|
+
import os5 from "os";
|
|
23668
|
+
function detectRuntimeEnvironment(env = process.env) {
|
|
23669
|
+
if (isWsl(env)) {
|
|
23983
23670
|
return {
|
|
23984
|
-
|
|
23985
|
-
|
|
23671
|
+
kind: "wsl",
|
|
23672
|
+
lanAutoDiscoveryUsable: false,
|
|
23673
|
+
warning: "Detected WSL. The LAN IP found inside WSL is usually a private VM address and is not reachable from your phone. Use Relay or set `hermeslink config set lan-host <Windows LAN IP>`."
|
|
23986
23674
|
};
|
|
23987
23675
|
}
|
|
23988
|
-
|
|
23989
|
-
const currentVersion = readString18(policy, "current_version") ?? readString18(policy, "currentVersion");
|
|
23990
|
-
const minSafeVersion = readString18(policy, "min_safe_version") ?? readString18(policy, "minSafeVersion");
|
|
23991
|
-
if (!currentVersion) {
|
|
23676
|
+
if (isContainer(env)) {
|
|
23992
23677
|
return {
|
|
23993
|
-
|
|
23994
|
-
|
|
23678
|
+
kind: "container",
|
|
23679
|
+
lanAutoDiscoveryUsable: false,
|
|
23680
|
+
warning: "Detected a container environment. Container LAN IPs are usually not reachable from your phone. Use Relay or set `hermeslink config set lan-host <host LAN IP>`."
|
|
23995
23681
|
};
|
|
23996
23682
|
}
|
|
23997
23683
|
return {
|
|
23998
|
-
|
|
23999
|
-
|
|
24000
|
-
|
|
24001
|
-
target_version: currentVersion,
|
|
24002
|
-
release_url: release ? readString18(release, "release_url") ?? readString18(release, "releaseUrl") : null,
|
|
24003
|
-
published_at: release ? readString18(release, "published_at") ?? readString18(release, "publishedAt") : null
|
|
24004
|
-
},
|
|
24005
|
-
issue: readString18(snapshot, "issue")
|
|
23684
|
+
kind: "native",
|
|
23685
|
+
lanAutoDiscoveryUsable: true,
|
|
23686
|
+
warning: null
|
|
24006
23687
|
};
|
|
24007
23688
|
}
|
|
24008
|
-
|
|
24009
|
-
|
|
24010
|
-
|
|
24011
|
-
|
|
24012
|
-
|
|
24013
|
-
|
|
24014
|
-
|
|
23689
|
+
function isWsl(env) {
|
|
23690
|
+
if (process.platform !== "linux") {
|
|
23691
|
+
return false;
|
|
23692
|
+
}
|
|
23693
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
|
|
23694
|
+
return true;
|
|
23695
|
+
}
|
|
23696
|
+
const release = os5.release().toLowerCase();
|
|
23697
|
+
return release.includes("microsoft") || release.includes("wsl");
|
|
23698
|
+
}
|
|
23699
|
+
function isContainer(env) {
|
|
23700
|
+
if (env.container || env.CONTAINER || env.KUBERNETES_SERVICE_HOST) {
|
|
23701
|
+
return true;
|
|
23702
|
+
}
|
|
23703
|
+
if (existsSync("/.dockerenv")) {
|
|
23704
|
+
return true;
|
|
23705
|
+
}
|
|
24015
23706
|
try {
|
|
24016
|
-
|
|
24017
|
-
|
|
24018
|
-
|
|
24019
|
-
|
|
24020
|
-
|
|
24021
|
-
|
|
24022
|
-
|
|
24023
|
-
|
|
24024
|
-
|
|
24025
|
-
|
|
23707
|
+
const cgroup = readFileSync2("/proc/1/cgroup", "utf8").toLowerCase();
|
|
23708
|
+
return /docker|containerd|kubepods|libpod|podman/u.test(cgroup);
|
|
23709
|
+
} catch {
|
|
23710
|
+
return false;
|
|
23711
|
+
}
|
|
23712
|
+
}
|
|
23713
|
+
|
|
23714
|
+
// src/topology/network.ts
|
|
23715
|
+
var VIRTUAL_INTERFACE_NAME_PATTERN = /(docker|veth|vmnet|vmenet|vbox|virtualbox|vmware|tailscale|zerotier|wireguard|utun|virbr|hyper-v|vethernet|loopback|\blo\b|^lo\d*$|^br-|^bridge\d+$|^zt|^tun|^tap|awdl|llw|anpi|gif|stf|ipsec|ppp)/iu;
|
|
23716
|
+
var MAX_LAN_IPS = 4;
|
|
23717
|
+
var MAX_PUBLIC_IPV4S = 2;
|
|
23718
|
+
var MAX_PUBLIC_IPV6S = 2;
|
|
23719
|
+
async function discoverRouteCandidates(options) {
|
|
23720
|
+
const environment = detectRuntimeEnvironment();
|
|
23721
|
+
const configuredLanHost = normalizeLanHost(options.configuredLanHost);
|
|
23722
|
+
const lanIps = configuredLanHost ? [configuredLanHost] : environment.lanAutoDiscoveryUsable ? discoverLanIps() : [];
|
|
23723
|
+
const publicIps = options.relayBootstrapToken || options.observePublicRoute ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
|
|
23724
|
+
const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
|
|
23725
|
+
const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
|
|
23726
|
+
const preferredUrls = [
|
|
23727
|
+
...lanIps.map((ip) => buildDirectUrl(ip, options.port)),
|
|
23728
|
+
...publicIpv4s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
23729
|
+
...publicIpv6s.map((ip) => buildDirectUrl(ip, options.port)),
|
|
23730
|
+
`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/links/${options.linkId}`
|
|
23731
|
+
];
|
|
23732
|
+
return {
|
|
23733
|
+
lanIps,
|
|
23734
|
+
publicIpv4s,
|
|
23735
|
+
publicIpv6s,
|
|
23736
|
+
preferredUrls,
|
|
23737
|
+
environment
|
|
23738
|
+
};
|
|
23739
|
+
}
|
|
23740
|
+
function discoverLanIps() {
|
|
23741
|
+
return discoverLanIpsFromInterfaces(os6.networkInterfaces());
|
|
23742
|
+
}
|
|
23743
|
+
function discoverLanIpsFromInterfaces(interfaces) {
|
|
23744
|
+
const result = /* @__PURE__ */ new Set();
|
|
23745
|
+
const candidates = [];
|
|
23746
|
+
for (const [name, items] of Object.entries(interfaces)) {
|
|
23747
|
+
if (shouldIgnoreInterface(name)) {
|
|
23748
|
+
continue;
|
|
24026
23749
|
}
|
|
24027
|
-
|
|
24028
|
-
|
|
24029
|
-
|
|
23750
|
+
for (const item of items ?? []) {
|
|
23751
|
+
if (!item.internal && item.address && item.family === "IPv4" && isUsableLanIpv42(item.address, item.netmask)) {
|
|
23752
|
+
candidates.push({ name, address: item.address });
|
|
23753
|
+
}
|
|
23754
|
+
}
|
|
23755
|
+
}
|
|
23756
|
+
for (const candidate of candidates.sort(compareLanCandidate)) {
|
|
23757
|
+
result.add(candidate.address);
|
|
24030
23758
|
}
|
|
23759
|
+
return [...result].slice(0, MAX_LAN_IPS);
|
|
24031
23760
|
}
|
|
24032
|
-
async function
|
|
24033
|
-
const
|
|
24034
|
-
const
|
|
24035
|
-
|
|
24036
|
-
|
|
23761
|
+
async function observePublicRoute(options) {
|
|
23762
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
23763
|
+
const response = await fetcher(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/public-route/observe`, {
|
|
23764
|
+
method: "POST",
|
|
23765
|
+
headers: {
|
|
23766
|
+
"content-type": "application/json",
|
|
23767
|
+
...options.relayBootstrapToken ? { authorization: `Bearer ${options.relayBootstrapToken}` } : {}
|
|
23768
|
+
},
|
|
23769
|
+
body: JSON.stringify({
|
|
23770
|
+
install_id: options.installId,
|
|
23771
|
+
link_id: options.linkId,
|
|
23772
|
+
public_key_pem: options.publicKeyPem
|
|
23773
|
+
})
|
|
23774
|
+
});
|
|
23775
|
+
const payload = await response.json().catch(() => null);
|
|
23776
|
+
const record = typeof payload?.record === "object" && payload.record !== null ? payload.record : null;
|
|
23777
|
+
const observed = typeof payload?.observed === "object" && payload.observed !== null ? payload.observed : null;
|
|
23778
|
+
const values = [
|
|
23779
|
+
readIpRecord(record?.ipv4),
|
|
23780
|
+
readIpRecord(record?.ipv6),
|
|
23781
|
+
typeof observed?.ip === "string" ? observed.ip : null
|
|
23782
|
+
].filter((value) => Boolean(value));
|
|
24037
23783
|
return {
|
|
24038
|
-
|
|
24039
|
-
|
|
23784
|
+
publicIpv4s: unique(values.filter(isUsablePublicIpv4)),
|
|
23785
|
+
publicIpv6s: unique(values.filter(isUsablePublicIpv6))
|
|
24040
23786
|
};
|
|
24041
23787
|
}
|
|
24042
|
-
function
|
|
24043
|
-
if (
|
|
24044
|
-
return
|
|
24045
|
-
}
|
|
24046
|
-
if (remote.min_safe_version && compareSemver3(localVersion, remote.min_safe_version) < 0) {
|
|
24047
|
-
return "unsafe";
|
|
24048
|
-
}
|
|
24049
|
-
const diff = compareSemver3(localVersion, remote.current_version);
|
|
24050
|
-
if (diff < 0) {
|
|
24051
|
-
return "update_available";
|
|
24052
|
-
}
|
|
24053
|
-
if (diff > 0) {
|
|
24054
|
-
return "ahead_of_current";
|
|
24055
|
-
}
|
|
24056
|
-
return "current";
|
|
24057
|
-
}
|
|
24058
|
-
async function emitUpdateStatus2(paths) {
|
|
24059
|
-
updateEvents2.emit("status", await readLinkUpdateStatus(paths));
|
|
24060
|
-
}
|
|
24061
|
-
async function writeUpdateState2(paths, state) {
|
|
24062
|
-
await writeJsonFile(updateStatePath2(paths), state);
|
|
24063
|
-
}
|
|
24064
|
-
async function readUpdateLogLines2(paths) {
|
|
24065
|
-
const raw = await readFile18(updateLogPath2(paths), "utf8").catch(() => "");
|
|
24066
|
-
if (!raw.trim()) {
|
|
24067
|
-
return [];
|
|
23788
|
+
function readIpRecord(value) {
|
|
23789
|
+
if (typeof value !== "object" || value === null) {
|
|
23790
|
+
return null;
|
|
24068
23791
|
}
|
|
24069
|
-
|
|
24070
|
-
|
|
24071
|
-
);
|
|
24072
|
-
}
|
|
24073
|
-
function updateStatePath2(paths) {
|
|
24074
|
-
return path25.join(paths.runDir, "link-update-state.json");
|
|
24075
|
-
}
|
|
24076
|
-
function updateLogPath2(paths) {
|
|
24077
|
-
return path25.join(paths.logsDir, UPDATE_LOG_FILE2);
|
|
23792
|
+
const ip = value.ip;
|
|
23793
|
+
return typeof ip === "string" && ip.trim() ? ip.trim() : null;
|
|
24078
23794
|
}
|
|
24079
|
-
|
|
24080
|
-
|
|
24081
|
-
await Promise.all([
|
|
24082
|
-
rm10(primary, { force: true }).catch(() => void 0),
|
|
24083
|
-
...Array.from(
|
|
24084
|
-
{ length: UPDATE_LOG_MAX_FILES2 },
|
|
24085
|
-
(_, index) => rm10(`${primary}.${index + 1}`, { force: true }).catch(() => void 0)
|
|
24086
|
-
)
|
|
24087
|
-
]);
|
|
23795
|
+
function buildDirectUrl(ip, port) {
|
|
23796
|
+
return `http://${ip.includes(":") ? `[${ip}]` : ip}:${port}`;
|
|
24088
23797
|
}
|
|
24089
|
-
function
|
|
24090
|
-
return
|
|
23798
|
+
function shouldIgnoreInterface(name) {
|
|
23799
|
+
return !name.trim() || VIRTUAL_INTERFACE_NAME_PATTERN.test(name);
|
|
24091
23800
|
}
|
|
24092
|
-
function
|
|
24093
|
-
|
|
23801
|
+
function compareLanCandidate(left, right) {
|
|
23802
|
+
const priority = interfacePriority(left.name) - interfacePriority(right.name);
|
|
23803
|
+
return priority || left.name.localeCompare(right.name) || left.address.localeCompare(right.address);
|
|
24094
23804
|
}
|
|
24095
|
-
function
|
|
24096
|
-
|
|
24097
|
-
|
|
24098
|
-
for (let index = 0; index < 3; index += 1) {
|
|
24099
|
-
const diff = leftParts[index] - rightParts[index];
|
|
24100
|
-
if (diff !== 0) {
|
|
24101
|
-
return diff;
|
|
24102
|
-
}
|
|
23805
|
+
function interfacePriority(name) {
|
|
23806
|
+
if (/^(en|eth|wlan|wi-fi|wifi)/iu.test(name)) {
|
|
23807
|
+
return 0;
|
|
24103
23808
|
}
|
|
24104
|
-
return
|
|
23809
|
+
return 1;
|
|
24105
23810
|
}
|
|
24106
|
-
function
|
|
24107
|
-
|
|
24108
|
-
return [
|
|
24109
|
-
Number.parseInt(match?.[1] ?? "0", 10),
|
|
24110
|
-
Number.parseInt(match?.[2] ?? "0", 10),
|
|
24111
|
-
Number.parseInt(match?.[3] ?? "0", 10)
|
|
24112
|
-
];
|
|
23811
|
+
function isUsableLanIpv42(address, netmask) {
|
|
23812
|
+
return isPrivateIpv4(address) && !isNetworkOrBroadcastIpv4Address(address, netmask);
|
|
24113
23813
|
}
|
|
24114
|
-
function
|
|
24115
|
-
|
|
24116
|
-
return Number.isFinite(startedAt) && now - startedAt < 1e4;
|
|
23814
|
+
function isUsablePublicIpv4(address) {
|
|
23815
|
+
return isValidIpv4(address) && !isSpecialIpv4(address);
|
|
24117
23816
|
}
|
|
24118
|
-
function
|
|
24119
|
-
|
|
23817
|
+
function isUsablePublicIpv6(address) {
|
|
23818
|
+
const normalized = address.toLowerCase();
|
|
23819
|
+
return normalized.includes(":") && !normalized.startsWith("fe80:") && !normalized.startsWith("fc") && !normalized.startsWith("fd") && !normalized.startsWith("ff") && normalized !== "::" && normalized !== "::1";
|
|
23820
|
+
}
|
|
23821
|
+
function isPrivateIpv4(address) {
|
|
23822
|
+
const parts = parseIpv4Segments(address);
|
|
23823
|
+
if (!parts) {
|
|
24120
23824
|
return false;
|
|
24121
23825
|
}
|
|
24122
|
-
|
|
24123
|
-
|
|
23826
|
+
const [first, second] = parts;
|
|
23827
|
+
return first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
23828
|
+
}
|
|
23829
|
+
function isSpecialIpv4(address) {
|
|
23830
|
+
const parts = parseIpv4Segments(address);
|
|
23831
|
+
if (!parts) {
|
|
24124
23832
|
return true;
|
|
24125
|
-
} catch {
|
|
24126
|
-
return false;
|
|
24127
23833
|
}
|
|
23834
|
+
const [first, second, third, fourth] = parts;
|
|
23835
|
+
return first === 0 || first === 10 || first === 127 || first >= 224 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 192 && second === 0 && third === 2 || first === 198 && (second === 18 || second === 19) || first === 198 && second === 51 && third === 100 || first === 203 && second === 0 && third === 113 || first === 255 && second === 255 && third === 255 && fourth === 255;
|
|
24128
23836
|
}
|
|
24129
|
-
function
|
|
24130
|
-
|
|
24131
|
-
|
|
24132
|
-
|
|
24133
|
-
|
|
24134
|
-
}
|
|
24135
|
-
function readString18(payload, key) {
|
|
24136
|
-
const value = payload[key];
|
|
24137
|
-
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
24138
|
-
}
|
|
24139
|
-
|
|
24140
|
-
// src/pairing/pairing.ts
|
|
24141
|
-
import path26 from "path";
|
|
24142
|
-
import { rm as rm11 } from "fs/promises";
|
|
24143
|
-
|
|
24144
|
-
// src/relay/bootstrap.ts
|
|
24145
|
-
var RelayNetworkError = class extends Error {
|
|
24146
|
-
constructor(relayBaseUrl, causeMessage) {
|
|
24147
|
-
super(
|
|
24148
|
-
`Cannot reach Hermes Relay at ${relayBaseUrl}. Please check your network connection, VPN, or proxy settings, then try again.`
|
|
24149
|
-
);
|
|
24150
|
-
this.relayBaseUrl = relayBaseUrl;
|
|
24151
|
-
this.causeMessage = causeMessage;
|
|
23837
|
+
function isNetworkOrBroadcastIpv4Address(address, netmask) {
|
|
23838
|
+
const addressParts = parseIpv4Segments(address);
|
|
23839
|
+
const netmaskParts = netmask ? parseIpv4Segments(netmask) : null;
|
|
23840
|
+
if (!addressParts) {
|
|
23841
|
+
return true;
|
|
24152
23842
|
}
|
|
24153
|
-
|
|
24154
|
-
|
|
24155
|
-
|
|
24156
|
-
async function bootstrapRelayLink(options) {
|
|
24157
|
-
const fetcher = options.fetchImpl ?? fetch;
|
|
24158
|
-
const baseUrl = options.relayBaseUrl.replace(/\/+$/u, "");
|
|
24159
|
-
const commonPayload = {
|
|
24160
|
-
install_id: options.identity.install_id,
|
|
24161
|
-
link_id: options.identity.link_id ?? void 0,
|
|
24162
|
-
public_key_pem: options.identity.public_key_pem
|
|
24163
|
-
};
|
|
24164
|
-
const challenge = await postJson(
|
|
24165
|
-
fetcher,
|
|
24166
|
-
`${baseUrl}/api/v1/relay/link/challenge`,
|
|
24167
|
-
options.relayBootstrapToken,
|
|
24168
|
-
commonPayload
|
|
24169
|
-
);
|
|
24170
|
-
if (challenge.ok !== true || typeof challenge.nonce !== "string") {
|
|
24171
|
-
throw new Error("Relay did not return a valid install challenge");
|
|
23843
|
+
if (!netmaskParts) {
|
|
23844
|
+
const last = addressParts[3];
|
|
23845
|
+
return last === 0 || last === 255;
|
|
24172
23846
|
}
|
|
24173
|
-
const
|
|
24174
|
-
|
|
24175
|
-
|
|
24176
|
-
|
|
24177
|
-
|
|
24178
|
-
fetcher,
|
|
24179
|
-
`${baseUrl}/api/v1/relay/link/bootstrap`,
|
|
24180
|
-
options.relayBootstrapToken,
|
|
24181
|
-
{
|
|
24182
|
-
...commonPayload,
|
|
24183
|
-
proof
|
|
24184
|
-
}
|
|
24185
|
-
);
|
|
24186
|
-
if (assigned.ok !== true || typeof assigned.link_id !== "string") {
|
|
24187
|
-
throw new Error("Relay did not return a valid link_id");
|
|
23847
|
+
const addressInt = ipv4SegmentsToInt(addressParts);
|
|
23848
|
+
const netmaskInt = ipv4SegmentsToInt(netmaskParts);
|
|
23849
|
+
const hostMask = ~netmaskInt >>> 0;
|
|
23850
|
+
if (hostMask === 0) {
|
|
23851
|
+
return false;
|
|
24188
23852
|
}
|
|
24189
|
-
|
|
24190
|
-
|
|
24191
|
-
|
|
24192
|
-
reused: assigned.reused === true
|
|
24193
|
-
};
|
|
23853
|
+
const networkInt = addressInt & netmaskInt;
|
|
23854
|
+
const broadcastInt = (networkInt | hostMask) >>> 0;
|
|
23855
|
+
return addressInt === networkInt || addressInt === broadcastInt;
|
|
24194
23856
|
}
|
|
24195
|
-
|
|
24196
|
-
|
|
24197
|
-
try {
|
|
24198
|
-
response = await fetcher(url, {
|
|
24199
|
-
method: "POST",
|
|
24200
|
-
headers: {
|
|
24201
|
-
authorization: `Bearer ${token}`,
|
|
24202
|
-
"content-type": "application/json"
|
|
24203
|
-
},
|
|
24204
|
-
body: JSON.stringify(body)
|
|
24205
|
-
});
|
|
24206
|
-
} catch (error) {
|
|
24207
|
-
const baseUrl = new URL(url).origin;
|
|
24208
|
-
throw new RelayNetworkError(
|
|
24209
|
-
baseUrl,
|
|
24210
|
-
error instanceof Error ? error.message : String(error)
|
|
24211
|
-
);
|
|
24212
|
-
}
|
|
24213
|
-
const payload = await response.json().catch(() => null);
|
|
24214
|
-
if (!response.ok) {
|
|
24215
|
-
const message = readErrorMessage4(payload) ?? `Relay request failed with HTTP ${response.status}`;
|
|
24216
|
-
throw new Error(message);
|
|
24217
|
-
}
|
|
24218
|
-
if (!payload) {
|
|
24219
|
-
throw new Error("Relay returned an empty response");
|
|
24220
|
-
}
|
|
24221
|
-
return payload;
|
|
23857
|
+
function isValidIpv4(address) {
|
|
23858
|
+
return Boolean(parseIpv4Segments(address));
|
|
24222
23859
|
}
|
|
24223
|
-
function
|
|
24224
|
-
if (
|
|
23860
|
+
function parseIpv4Segments(address) {
|
|
23861
|
+
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(address)) {
|
|
24225
23862
|
return null;
|
|
24226
23863
|
}
|
|
24227
|
-
const
|
|
24228
|
-
if (
|
|
23864
|
+
const parts = address.split(".").map((part) => Number.parseInt(part, 10));
|
|
23865
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
24229
23866
|
return null;
|
|
24230
23867
|
}
|
|
24231
|
-
|
|
24232
|
-
|
|
23868
|
+
return parts;
|
|
23869
|
+
}
|
|
23870
|
+
function ipv4SegmentsToInt(parts) {
|
|
23871
|
+
return (parts[0] << 24 >>> 0 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
|
|
23872
|
+
}
|
|
23873
|
+
function unique(values) {
|
|
23874
|
+
return [...new Set(values)];
|
|
24233
23875
|
}
|
|
24234
23876
|
|
|
24235
23877
|
// src/pairing/pairing.ts
|
|
@@ -24401,7 +24043,7 @@ async function readPairingClaim(sessionId, paths = resolveRuntimePaths()) {
|
|
|
24401
24043
|
};
|
|
24402
24044
|
}
|
|
24403
24045
|
async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
|
|
24404
|
-
await
|
|
24046
|
+
await rm9(pairingClaimPath(sessionId, paths), { force: true }).catch(() => void 0);
|
|
24405
24047
|
}
|
|
24406
24048
|
async function claimPairing(input) {
|
|
24407
24049
|
const paths = input.paths ?? resolveRuntimePaths();
|
|
@@ -24478,10 +24120,10 @@ async function loadRequiredIdentity2(paths) {
|
|
|
24478
24120
|
}
|
|
24479
24121
|
return identity;
|
|
24480
24122
|
}
|
|
24481
|
-
async function postServerJson(serverBaseUrl,
|
|
24123
|
+
async function postServerJson(serverBaseUrl, path26, body, options) {
|
|
24482
24124
|
let response;
|
|
24483
24125
|
try {
|
|
24484
|
-
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
24126
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
|
|
24485
24127
|
method: "POST",
|
|
24486
24128
|
headers: {
|
|
24487
24129
|
accept: "application/json",
|
|
@@ -24529,10 +24171,10 @@ function pairingErrorSnapshot(stage, error) {
|
|
|
24529
24171
|
occurred_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
24530
24172
|
};
|
|
24531
24173
|
}
|
|
24532
|
-
async function patchServerJson(serverBaseUrl,
|
|
24174
|
+
async function patchServerJson(serverBaseUrl, path26, token, body, options) {
|
|
24533
24175
|
let response;
|
|
24534
24176
|
try {
|
|
24535
|
-
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
24177
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path26}`, {
|
|
24536
24178
|
method: "PATCH",
|
|
24537
24179
|
headers: {
|
|
24538
24180
|
accept: "application/json",
|
|
@@ -24554,12 +24196,12 @@ async function patchServerJson(serverBaseUrl, path27, token, body, options) {
|
|
|
24554
24196
|
async function readJsonResponse2(response) {
|
|
24555
24197
|
const payload = await response.json().catch(() => null);
|
|
24556
24198
|
if (!response.ok || !payload) {
|
|
24557
|
-
const message =
|
|
24199
|
+
const message = readErrorMessage4(payload) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
24558
24200
|
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
24559
24201
|
}
|
|
24560
24202
|
return payload;
|
|
24561
24203
|
}
|
|
24562
|
-
function
|
|
24204
|
+
function readErrorMessage4(payload) {
|
|
24563
24205
|
if (typeof payload !== "object" || payload === null) {
|
|
24564
24206
|
return null;
|
|
24565
24207
|
}
|
|
@@ -24580,10 +24222,10 @@ function createPairingNetworkError(input) {
|
|
|
24580
24222
|
);
|
|
24581
24223
|
}
|
|
24582
24224
|
function pairingClaimPath(sessionId, paths) {
|
|
24583
|
-
return
|
|
24225
|
+
return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
24584
24226
|
}
|
|
24585
24227
|
function pairingSessionPath(sessionId, paths) {
|
|
24586
|
-
return
|
|
24228
|
+
return path25.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
|
|
24587
24229
|
}
|
|
24588
24230
|
function qrPreferredUrls(routes) {
|
|
24589
24231
|
return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
|
|
@@ -25757,14 +25399,20 @@ async function createApp(options = {}) {
|
|
|
25757
25399
|
export {
|
|
25758
25400
|
LINK_VERSION,
|
|
25759
25401
|
LINK_COMMAND,
|
|
25402
|
+
migrateLinkDatabase,
|
|
25760
25403
|
LinkHttpError,
|
|
25404
|
+
readJsonFile,
|
|
25405
|
+
writeJsonFile,
|
|
25761
25406
|
resolveHermesProfileDir,
|
|
25762
25407
|
resolveHermesConfigPath,
|
|
25763
25408
|
readHermesApiServerConfig,
|
|
25764
25409
|
ensureHermesApiServerConfig,
|
|
25410
|
+
syncHermesLinkCronDeliveries,
|
|
25765
25411
|
resolveRuntimePaths,
|
|
25766
25412
|
createFileLogger,
|
|
25767
25413
|
getLinkLogFile,
|
|
25414
|
+
getDaemonLogFile,
|
|
25415
|
+
createRotatingTextLogWriter,
|
|
25768
25416
|
ensureHermesApiServerAvailable,
|
|
25769
25417
|
readHermesVersion,
|
|
25770
25418
|
defaultLinkConfig,
|
|
@@ -25774,22 +25422,15 @@ export {
|
|
|
25774
25422
|
normalizeLanHost,
|
|
25775
25423
|
loadIdentity,
|
|
25776
25424
|
ensureIdentity,
|
|
25425
|
+
signIdentityPayload,
|
|
25777
25426
|
getIdentityStatus,
|
|
25778
25427
|
ConversationService,
|
|
25779
25428
|
hasActiveDevices,
|
|
25429
|
+
readLinkSystemInfo,
|
|
25780
25430
|
detectRuntimeEnvironment,
|
|
25431
|
+
discoverRouteCandidates,
|
|
25781
25432
|
preparePairing,
|
|
25782
25433
|
readPairingClaim,
|
|
25783
25434
|
clearPairingClaim,
|
|
25784
|
-
createApp
|
|
25785
|
-
connectRelayControl,
|
|
25786
|
-
reportLinkStatusToServer,
|
|
25787
|
-
startLinkService,
|
|
25788
|
-
startDaemonProcess,
|
|
25789
|
-
runDaemonSupervisor,
|
|
25790
|
-
probeLocalLinkService,
|
|
25791
|
-
stopDaemonProcess,
|
|
25792
|
-
getDaemonStatus,
|
|
25793
|
-
daemonLogFile,
|
|
25794
|
-
currentCliScriptPath
|
|
25435
|
+
createApp
|
|
25795
25436
|
};
|