@hermespilot/link 0.4.2 → 0.4.4
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 +2 -0
- package/dist/{chunk-YSSZPVBP.js → chunk-2CHGHWCY.js} +356 -90
- package/dist/cli/index.js +44 -14
- package/dist/http/app.d.ts +2 -0
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ If Hermes Agent is configured through `~/.hermes/.env`, Link follows the same `A
|
|
|
44
44
|
|
|
45
45
|
CLI output follows the current system language when it is Chinese or English. You can override it for a single command with `HERMESLINK_LANG=zh-CN` or `HERMESLINK_LANG=en`.
|
|
46
46
|
|
|
47
|
+
Set `HERMESLINK_LOG_LEVEL=warn` to suppress `debug` and `info` logs in published builds; `warn` is the default. You can also persist it with `hermeslink config set log-level warn`.
|
|
48
|
+
|
|
47
49
|
## Runtime data
|
|
48
50
|
|
|
49
51
|
Hermes Link keeps its local identity and runtime state under:
|
|
@@ -1102,6 +1102,20 @@ function readProfileAvatarType(row) {
|
|
|
1102
1102
|
import { mkdir as mkdir3, readdir as readdir3, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
1103
1103
|
import path4 from "path";
|
|
1104
1104
|
|
|
1105
|
+
// src/core/errors.ts
|
|
1106
|
+
var LinkHttpError = class extends Error {
|
|
1107
|
+
constructor(status, code, message) {
|
|
1108
|
+
super(message);
|
|
1109
|
+
this.status = status;
|
|
1110
|
+
this.code = code;
|
|
1111
|
+
}
|
|
1112
|
+
status;
|
|
1113
|
+
code;
|
|
1114
|
+
};
|
|
1115
|
+
function isLinkHttpError(error) {
|
|
1116
|
+
return error instanceof LinkHttpError;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1105
1119
|
// src/storage/atomic-json.ts
|
|
1106
1120
|
import { readFile } from "fs/promises";
|
|
1107
1121
|
|
|
@@ -3824,6 +3838,7 @@ async function decorateHermesLinkCronJob(paths, profileName, job) {
|
|
|
3824
3838
|
async function syncHermesLinkCronDeliveries(paths, runtime, logger) {
|
|
3825
3839
|
const registry = await readRegistry(paths);
|
|
3826
3840
|
let touched = false;
|
|
3841
|
+
const staleBindings = [];
|
|
3827
3842
|
for (const binding of registry.bindings) {
|
|
3828
3843
|
const delivered = new Set(binding.deliveredOutputPaths ?? []);
|
|
3829
3844
|
const outputs = await listCronOutputFiles(
|
|
@@ -3848,6 +3863,13 @@ async function syncHermesLinkCronDeliveries(paths, runtime, logger) {
|
|
|
3848
3863
|
delivered.add(output.path);
|
|
3849
3864
|
touched = true;
|
|
3850
3865
|
} catch (error) {
|
|
3866
|
+
if (isConversationMissingError(error)) {
|
|
3867
|
+
staleBindings.push({
|
|
3868
|
+
profileName: binding.profileName,
|
|
3869
|
+
jobId: binding.jobId
|
|
3870
|
+
});
|
|
3871
|
+
break;
|
|
3872
|
+
}
|
|
3851
3873
|
void logger.warn("cron_link_delivery_failed", {
|
|
3852
3874
|
profile: binding.profileName,
|
|
3853
3875
|
job_id: binding.jobId,
|
|
@@ -3858,6 +3880,14 @@ async function syncHermesLinkCronDeliveries(paths, runtime, logger) {
|
|
|
3858
3880
|
}
|
|
3859
3881
|
binding.deliveredOutputPaths = [...delivered];
|
|
3860
3882
|
}
|
|
3883
|
+
if (staleBindings.length > 0) {
|
|
3884
|
+
registry.bindings = registry.bindings.filter(
|
|
3885
|
+
(binding) => !staleBindings.some(
|
|
3886
|
+
(staleBinding) => staleBinding.profileName === binding.profileName && staleBinding.jobId === binding.jobId
|
|
3887
|
+
)
|
|
3888
|
+
);
|
|
3889
|
+
touched = true;
|
|
3890
|
+
}
|
|
3861
3891
|
if (touched) {
|
|
3862
3892
|
await writeRegistry(paths, registry);
|
|
3863
3893
|
}
|
|
@@ -3945,6 +3975,9 @@ function readString3(record, ...keys) {
|
|
|
3945
3975
|
function isNodeError4(error, code) {
|
|
3946
3976
|
return error instanceof Error && error.code === code;
|
|
3947
3977
|
}
|
|
3978
|
+
function isConversationMissingError(error) {
|
|
3979
|
+
return isLinkHttpError(error) && error.status === 404 && error.code === "conversation_not_found";
|
|
3980
|
+
}
|
|
3948
3981
|
|
|
3949
3982
|
// src/hermes/gateway.ts
|
|
3950
3983
|
import { execFile as execFile2, spawn } from "child_process";
|
|
@@ -3953,20 +3986,6 @@ import path7 from "path";
|
|
|
3953
3986
|
import { setTimeout as delay } from "timers/promises";
|
|
3954
3987
|
import { promisify as promisify2 } from "util";
|
|
3955
3988
|
|
|
3956
|
-
// src/core/errors.ts
|
|
3957
|
-
var LinkHttpError = class extends Error {
|
|
3958
|
-
constructor(status, code, message) {
|
|
3959
|
-
super(message);
|
|
3960
|
-
this.status = status;
|
|
3961
|
-
this.code = code;
|
|
3962
|
-
}
|
|
3963
|
-
status;
|
|
3964
|
-
code;
|
|
3965
|
-
};
|
|
3966
|
-
function isLinkHttpError(error) {
|
|
3967
|
-
return error instanceof LinkHttpError;
|
|
3968
|
-
}
|
|
3969
|
-
|
|
3970
3989
|
// src/runtime/logger.ts
|
|
3971
3990
|
import { appendFile, mkdir as mkdir4, open as open2, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3 } from "fs/promises";
|
|
3972
3991
|
import os3 from "os";
|
|
@@ -3977,7 +3996,7 @@ import os2 from "os";
|
|
|
3977
3996
|
import path5 from "path";
|
|
3978
3997
|
|
|
3979
3998
|
// src/constants.ts
|
|
3980
|
-
var LINK_VERSION = "0.4.
|
|
3999
|
+
var LINK_VERSION = "0.4.4";
|
|
3981
4000
|
var LINK_COMMAND = "hermeslink";
|
|
3982
4001
|
var LINK_DEFAULT_PORT = 52379;
|
|
3983
4002
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -4012,11 +4031,18 @@ var MAX_READ_LIMIT = 1e3;
|
|
|
4012
4031
|
var DEFAULT_MAX_BYTES_PER_FILE = 512 * 1024;
|
|
4013
4032
|
var GATEWAY_LOG_FILE = "hermes-gateway.log";
|
|
4014
4033
|
var DAEMON_LOG_FILE = "daemon.log";
|
|
4034
|
+
var LOG_LEVEL_PRIORITY = {
|
|
4035
|
+
debug: 10,
|
|
4036
|
+
info: 20,
|
|
4037
|
+
warn: 30,
|
|
4038
|
+
error: 40
|
|
4039
|
+
};
|
|
4015
4040
|
var FileLogger = class {
|
|
4016
4041
|
filePath;
|
|
4017
4042
|
paths;
|
|
4018
4043
|
maxFileBytes;
|
|
4019
4044
|
maxFiles;
|
|
4045
|
+
minLevel;
|
|
4020
4046
|
now;
|
|
4021
4047
|
queue = Promise.resolve();
|
|
4022
4048
|
constructor(options = {}) {
|
|
@@ -4024,6 +4050,7 @@ var FileLogger = class {
|
|
|
4024
4050
|
this.filePath = getLinkLogFile(this.paths, options.fileName);
|
|
4025
4051
|
this.maxFileBytes = Math.max(256, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
|
|
4026
4052
|
this.maxFiles = Math.max(0, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
|
|
4053
|
+
this.minLevel = options.minLevel ?? "warn";
|
|
4027
4054
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4028
4055
|
}
|
|
4029
4056
|
debug(message, fields) {
|
|
@@ -4039,6 +4066,9 @@ var FileLogger = class {
|
|
|
4039
4066
|
return this.write("error", message, fields);
|
|
4040
4067
|
}
|
|
4041
4068
|
write(level, message, fields) {
|
|
4069
|
+
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.minLevel]) {
|
|
4070
|
+
return Promise.resolve();
|
|
4071
|
+
}
|
|
4042
4072
|
const entry = {
|
|
4043
4073
|
ts: this.now().toISOString(),
|
|
4044
4074
|
level,
|
|
@@ -4464,8 +4494,11 @@ var MIN_API_SERVER_VERSION = "0.4.0";
|
|
|
4464
4494
|
var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/u;
|
|
4465
4495
|
var DASHBOARD_STATUS_URL = "http://127.0.0.1:9119/api/status";
|
|
4466
4496
|
var DASHBOARD_STATUS_TIMEOUT_MS = 1500;
|
|
4497
|
+
var DEFAULT_VERSION_CACHE_TTL_MS = 6e4;
|
|
4467
4498
|
var MAX_VERSION_LOG_OUTPUT_LENGTH = 1200;
|
|
4468
4499
|
var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
|
|
4500
|
+
var hermesVersionCache = /* @__PURE__ */ new Map();
|
|
4501
|
+
var hermesVersionInFlight = /* @__PURE__ */ new Map();
|
|
4469
4502
|
async function ensureHermesApiServerAvailable(options = {}) {
|
|
4470
4503
|
const profileName = normalizeProfileName(options.profileName);
|
|
4471
4504
|
await assertProfileExists(profileName);
|
|
@@ -4582,41 +4615,38 @@ async function reloadHermesGateway(options = {}) {
|
|
|
4582
4615
|
return ensureHermesApiServerAvailable({ ...options, forceRestart: true });
|
|
4583
4616
|
}
|
|
4584
4617
|
async function readHermesVersion(options = {}) {
|
|
4618
|
+
const hermesBin = resolveHermesBin();
|
|
4619
|
+
const cacheTtlMs = Math.max(
|
|
4620
|
+
0,
|
|
4621
|
+
Math.floor(options.cacheTtlMs ?? DEFAULT_VERSION_CACHE_TTL_MS)
|
|
4622
|
+
);
|
|
4623
|
+
if (!options.forceRefresh && cacheTtlMs > 0) {
|
|
4624
|
+
const cached = hermesVersionCache.get(hermesBin);
|
|
4625
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
4626
|
+
return cached.value;
|
|
4627
|
+
}
|
|
4628
|
+
hermesVersionCache.delete(hermesBin);
|
|
4629
|
+
}
|
|
4630
|
+
const inFlight = hermesVersionInFlight.get(hermesBin);
|
|
4631
|
+
if (inFlight) {
|
|
4632
|
+
return await inFlight;
|
|
4633
|
+
}
|
|
4634
|
+
const probe = probeHermesVersion(hermesBin, options);
|
|
4635
|
+
hermesVersionInFlight.set(hermesBin, probe);
|
|
4585
4636
|
try {
|
|
4586
|
-
const
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
const dashboardStatusUrl = options.dashboardStatusUrl ?? DASHBOARD_STATUS_URL;
|
|
4592
|
-
void options.logger?.warn("hermes_version_dashboard_fallback_requested", {
|
|
4593
|
-
dashboard_status_url: dashboardStatusUrl,
|
|
4594
|
-
reason: cliError instanceof Error ? cliError.message : String(cliError)
|
|
4595
|
-
});
|
|
4596
|
-
try {
|
|
4597
|
-
const fallback = await readHermesDashboardVersion({
|
|
4598
|
-
fetchImpl: options.fetchImpl,
|
|
4599
|
-
statusUrl: dashboardStatusUrl,
|
|
4600
|
-
timeoutMs: options.dashboardTimeoutMs
|
|
4601
|
-
});
|
|
4602
|
-
void options.logger?.info("hermes_version_dashboard_fallback_succeeded", {
|
|
4603
|
-
dashboard_status_url: dashboardStatusUrl,
|
|
4604
|
-
hermes_version: fallback.version
|
|
4605
|
-
});
|
|
4606
|
-
return fallback;
|
|
4607
|
-
} catch (dashboardError) {
|
|
4608
|
-
void options.logger?.warn("hermes_version_dashboard_fallback_failed", {
|
|
4609
|
-
dashboard_status_url: dashboardStatusUrl,
|
|
4610
|
-
error: dashboardError instanceof Error ? dashboardError.message : String(dashboardError)
|
|
4637
|
+
const version = await probe;
|
|
4638
|
+
if (cacheTtlMs > 0) {
|
|
4639
|
+
hermesVersionCache.set(hermesBin, {
|
|
4640
|
+
value: version,
|
|
4641
|
+
expiresAt: Date.now() + cacheTtlMs
|
|
4611
4642
|
});
|
|
4612
|
-
throw new Error(
|
|
4613
|
-
`Hermes version detection failed. CLI: ${cliError instanceof Error ? cliError.message : String(cliError)}; dashboard fallback: ${dashboardError instanceof Error ? dashboardError.message : String(dashboardError)}`
|
|
4614
|
-
);
|
|
4615
4643
|
}
|
|
4644
|
+
return version;
|
|
4645
|
+
} finally {
|
|
4646
|
+
hermesVersionInFlight.delete(hermesBin);
|
|
4616
4647
|
}
|
|
4617
4648
|
}
|
|
4618
|
-
async function execHermesVersion(logger) {
|
|
4619
|
-
const hermesBin = resolveHermesBin();
|
|
4649
|
+
async function execHermesVersion(hermesBin, logger) {
|
|
4620
4650
|
const failures = [];
|
|
4621
4651
|
for (const args of [["version"], ["--version"]]) {
|
|
4622
4652
|
try {
|
|
@@ -4626,11 +4656,18 @@ async function execHermesVersion(logger) {
|
|
|
4626
4656
|
});
|
|
4627
4657
|
} catch (error) {
|
|
4628
4658
|
const failure = describeVersionCommandFailure(hermesBin, args, error);
|
|
4629
|
-
failures.push(failure
|
|
4630
|
-
void logger?.
|
|
4659
|
+
failures.push(failure);
|
|
4660
|
+
void logger?.debug("hermes_version_cli_command_attempt_failed", failure.fields);
|
|
4631
4661
|
}
|
|
4632
4662
|
}
|
|
4633
|
-
|
|
4663
|
+
const summary = failures.map((failure) => failure.summary).join("; ");
|
|
4664
|
+
void logger?.warn("hermes_version_cli_command_failed", {
|
|
4665
|
+
hermes_bin: hermesBin,
|
|
4666
|
+
failure_count: failures.length,
|
|
4667
|
+
error: summary,
|
|
4668
|
+
attempts: failures.map((failure) => failure.fields)
|
|
4669
|
+
});
|
|
4670
|
+
throw new Error(summary);
|
|
4634
4671
|
}
|
|
4635
4672
|
function assertHermesRunsApiSupported(version, status) {
|
|
4636
4673
|
if (status !== 404) {
|
|
@@ -5051,27 +5088,85 @@ async function readHermesDashboardVersion(options = {}) {
|
|
|
5051
5088
|
clearTimeout(timer);
|
|
5052
5089
|
}
|
|
5053
5090
|
}
|
|
5091
|
+
async function probeHermesVersion(hermesBin, options) {
|
|
5092
|
+
try {
|
|
5093
|
+
const { stdout } = await execHermesVersion(hermesBin, options.logger);
|
|
5094
|
+
const raw = stdout.trim();
|
|
5095
|
+
const version = parseHermesVersion(raw);
|
|
5096
|
+
return buildHermesVersionInfo(raw, version);
|
|
5097
|
+
} catch (cliError) {
|
|
5098
|
+
const dashboardStatusUrl = options.dashboardStatusUrl ?? DASHBOARD_STATUS_URL;
|
|
5099
|
+
void options.logger?.warn("hermes_version_dashboard_fallback_requested", {
|
|
5100
|
+
dashboard_status_url: dashboardStatusUrl,
|
|
5101
|
+
reason: cliError instanceof Error ? cliError.message : String(cliError)
|
|
5102
|
+
});
|
|
5103
|
+
try {
|
|
5104
|
+
const fallback = await readHermesDashboardVersion({
|
|
5105
|
+
fetchImpl: options.fetchImpl,
|
|
5106
|
+
statusUrl: dashboardStatusUrl,
|
|
5107
|
+
timeoutMs: options.dashboardTimeoutMs
|
|
5108
|
+
});
|
|
5109
|
+
void options.logger?.info("hermes_version_dashboard_fallback_succeeded", {
|
|
5110
|
+
dashboard_status_url: dashboardStatusUrl,
|
|
5111
|
+
hermes_version: fallback.version
|
|
5112
|
+
});
|
|
5113
|
+
return fallback;
|
|
5114
|
+
} catch (dashboardError) {
|
|
5115
|
+
void options.logger?.warn("hermes_version_dashboard_fallback_failed", {
|
|
5116
|
+
dashboard_status_url: dashboardStatusUrl,
|
|
5117
|
+
error: dashboardError instanceof Error ? dashboardError.message : String(dashboardError)
|
|
5118
|
+
});
|
|
5119
|
+
throw new Error(
|
|
5120
|
+
`Hermes version detection failed. CLI: ${cliError instanceof Error ? cliError.message : String(cliError)}; dashboard fallback: ${dashboardError instanceof Error ? dashboardError.message : String(dashboardError)}`
|
|
5121
|
+
);
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5054
5125
|
function describeVersionCommandFailure(hermesBin, args, error) {
|
|
5055
5126
|
const message = error instanceof Error ? error.message : String(error);
|
|
5056
|
-
const
|
|
5127
|
+
const details = readExecErrorDetails(error, message);
|
|
5057
5128
|
return {
|
|
5058
5129
|
summary: `${hermesBin} ${args.join(" ")} failed: ${message}`,
|
|
5059
5130
|
fields: {
|
|
5060
5131
|
hermes_bin: hermesBin,
|
|
5061
5132
|
command: args.join(" "),
|
|
5133
|
+
cwd: process.cwd(),
|
|
5062
5134
|
error: message,
|
|
5063
|
-
...
|
|
5135
|
+
...details
|
|
5064
5136
|
}
|
|
5065
5137
|
};
|
|
5066
5138
|
}
|
|
5067
|
-
function
|
|
5139
|
+
function readExecErrorDetails(error, message) {
|
|
5068
5140
|
if (typeof error !== "object" || error === null) {
|
|
5069
|
-
return "";
|
|
5141
|
+
return message.includes("timed out") ? { timed_out: true } : {};
|
|
5070
5142
|
}
|
|
5143
|
+
const details = {};
|
|
5071
5144
|
const stdout = "stdout" in error && error.stdout != null ? String(error.stdout) : "";
|
|
5072
5145
|
const stderr = "stderr" in error && error.stderr != null ? String(error.stderr) : "";
|
|
5073
|
-
|
|
5074
|
-
|
|
5146
|
+
if ("code" in error) {
|
|
5147
|
+
const code = error.code;
|
|
5148
|
+
if (typeof code === "number") {
|
|
5149
|
+
details.exit_code = code;
|
|
5150
|
+
} else if (typeof code === "string" && code.trim()) {
|
|
5151
|
+
details.error_code = code;
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
if ("signal" in error && typeof error.signal === "string") {
|
|
5155
|
+
details.signal = error.signal;
|
|
5156
|
+
}
|
|
5157
|
+
if ("killed" in error && typeof error.killed === "boolean") {
|
|
5158
|
+
details.killed = error.killed;
|
|
5159
|
+
}
|
|
5160
|
+
if (message.includes("timed out")) {
|
|
5161
|
+
details.timed_out = true;
|
|
5162
|
+
}
|
|
5163
|
+
if (stdout.trim()) {
|
|
5164
|
+
details.stdout = truncateVersionLogOutput(stdout.trim());
|
|
5165
|
+
}
|
|
5166
|
+
if (stderr.trim()) {
|
|
5167
|
+
details.stderr = truncateVersionLogOutput(stderr.trim());
|
|
5168
|
+
}
|
|
5169
|
+
return details;
|
|
5075
5170
|
}
|
|
5076
5171
|
function truncateVersionLogOutput(value) {
|
|
5077
5172
|
return value.length > MAX_VERSION_LOG_OUTPUT_LENGTH ? `${value.slice(0, MAX_VERSION_LOG_OUTPUT_LENGTH)}...` : value;
|
|
@@ -7244,22 +7339,31 @@ var defaultLinkConfig = {
|
|
|
7244
7339
|
relayBaseUrl: "https://hermes-relay.clawpilot.me",
|
|
7245
7340
|
appConnectTokenIssuer: "https://hermes-server.clawpilot.me",
|
|
7246
7341
|
appConnectTokenAudience: "hermes-link",
|
|
7247
|
-
language: "auto"
|
|
7342
|
+
language: "auto",
|
|
7343
|
+
logLevel: "warn"
|
|
7248
7344
|
};
|
|
7249
7345
|
async function loadConfig(paths = resolveRuntimePaths()) {
|
|
7250
7346
|
const existing = await readJsonFile(paths.configFile);
|
|
7251
7347
|
const language = normalizeConfiguredLanguage(existing?.language);
|
|
7252
7348
|
const lanHost = normalizeLanHost(existing?.lanHost);
|
|
7349
|
+
const logLevel = normalizeLogLevel(
|
|
7350
|
+
existing?.logLevel ?? process.env.HERMESLINK_LOG_LEVEL
|
|
7351
|
+
);
|
|
7253
7352
|
return {
|
|
7254
7353
|
...defaultLinkConfig,
|
|
7255
7354
|
...existing ?? {},
|
|
7256
7355
|
language,
|
|
7257
|
-
lanHost
|
|
7356
|
+
lanHost,
|
|
7357
|
+
logLevel
|
|
7258
7358
|
};
|
|
7259
7359
|
}
|
|
7260
7360
|
async function saveConfig(patch, paths = resolveRuntimePaths()) {
|
|
7261
7361
|
const current = await loadConfig(paths);
|
|
7262
|
-
const next = {
|
|
7362
|
+
const next = {
|
|
7363
|
+
...current,
|
|
7364
|
+
...patch,
|
|
7365
|
+
logLevel: patch.logLevel === void 0 ? current.logLevel : normalizeLogLevel(patch.logLevel)
|
|
7366
|
+
};
|
|
7263
7367
|
await writeJsonFile(paths.configFile, next);
|
|
7264
7368
|
return next;
|
|
7265
7369
|
}
|
|
@@ -7269,6 +7373,18 @@ function normalizeConfiguredLanguage(language) {
|
|
|
7269
7373
|
}
|
|
7270
7374
|
return defaultLinkConfig.language;
|
|
7271
7375
|
}
|
|
7376
|
+
function normalizeLogLevel(level) {
|
|
7377
|
+
if (level === "debug" || level === "info" || level === "warn" || level === "error") {
|
|
7378
|
+
return level;
|
|
7379
|
+
}
|
|
7380
|
+
return defaultLinkConfig.logLevel;
|
|
7381
|
+
}
|
|
7382
|
+
function parseLogLevel(value) {
|
|
7383
|
+
if (value === "debug" || value === "info" || value === "warn" || value === "error") {
|
|
7384
|
+
return value;
|
|
7385
|
+
}
|
|
7386
|
+
return null;
|
|
7387
|
+
}
|
|
7272
7388
|
function normalizeLanHost(value) {
|
|
7273
7389
|
if (value === null || value === void 0) {
|
|
7274
7390
|
return null;
|
|
@@ -8037,13 +8153,23 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
8037
8153
|
return this.appendCommandResultLocked(input);
|
|
8038
8154
|
}
|
|
8039
8155
|
startRunWorkerAndDrain(conversationId, runId, input) {
|
|
8040
|
-
void this.deps.runLifecycle.startRunWorker(conversationId, runId, input).catch(
|
|
8041
|
-
(error)
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8156
|
+
void this.deps.runLifecycle.startRunWorker(conversationId, runId, input).catch(async (error) => {
|
|
8157
|
+
if (isConversationNotFoundError(error)) {
|
|
8158
|
+
return;
|
|
8159
|
+
}
|
|
8160
|
+
try {
|
|
8161
|
+
await this.deps.runLifecycle.failRun(
|
|
8162
|
+
conversationId,
|
|
8163
|
+
runId,
|
|
8164
|
+
error instanceof Error ? error.message : String(error)
|
|
8165
|
+
);
|
|
8166
|
+
} catch (failError) {
|
|
8167
|
+
if (isConversationNotFoundError(failError)) {
|
|
8168
|
+
return;
|
|
8169
|
+
}
|
|
8170
|
+
throw failError;
|
|
8171
|
+
}
|
|
8172
|
+
}).finally(() => {
|
|
8047
8173
|
void this.startNextQueuedRun(conversationId);
|
|
8048
8174
|
});
|
|
8049
8175
|
}
|
|
@@ -8575,6 +8701,9 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
8575
8701
|
${attachmentLines.join("\n")}`;
|
|
8576
8702
|
}
|
|
8577
8703
|
};
|
|
8704
|
+
function isConversationNotFoundError(error) {
|
|
8705
|
+
return error instanceof LinkHttpError && error.code === "conversation_not_found";
|
|
8706
|
+
}
|
|
8578
8707
|
|
|
8579
8708
|
// src/conversations/agent-events.ts
|
|
8580
8709
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -11483,16 +11612,32 @@ async function callHermesApi(path26, init, options) {
|
|
|
11483
11612
|
try {
|
|
11484
11613
|
response = await request();
|
|
11485
11614
|
} catch (error) {
|
|
11486
|
-
logHermesApiError(
|
|
11615
|
+
logHermesApiError(
|
|
11616
|
+
options.logger,
|
|
11617
|
+
method,
|
|
11618
|
+
path26,
|
|
11619
|
+
options.profileName,
|
|
11620
|
+
startedAt,
|
|
11621
|
+
error
|
|
11622
|
+
);
|
|
11487
11623
|
throw error;
|
|
11488
11624
|
}
|
|
11489
11625
|
if (response.status !== 401) {
|
|
11490
|
-
logHermesApiResponse(
|
|
11626
|
+
logHermesApiResponse(
|
|
11627
|
+
options.logger,
|
|
11628
|
+
method,
|
|
11629
|
+
path26,
|
|
11630
|
+
options.profileName,
|
|
11631
|
+
startedAt,
|
|
11632
|
+
response
|
|
11633
|
+
);
|
|
11491
11634
|
return response;
|
|
11492
11635
|
}
|
|
11493
11636
|
void options.logger?.warn("hermes_api_request_retrying_after_401", {
|
|
11494
11637
|
method,
|
|
11495
11638
|
path: path26,
|
|
11639
|
+
profile: options.profileName ?? "default",
|
|
11640
|
+
port: config.port ?? null,
|
|
11496
11641
|
duration_ms: Date.now() - startedAt
|
|
11497
11642
|
});
|
|
11498
11643
|
const refreshedAvailability = await ensureHermesApiServerAvailable({
|
|
@@ -11505,10 +11650,24 @@ async function callHermesApi(path26, init, options) {
|
|
|
11505
11650
|
try {
|
|
11506
11651
|
response = await request();
|
|
11507
11652
|
} catch (error) {
|
|
11508
|
-
logHermesApiError(
|
|
11653
|
+
logHermesApiError(
|
|
11654
|
+
options.logger,
|
|
11655
|
+
method,
|
|
11656
|
+
path26,
|
|
11657
|
+
options.profileName,
|
|
11658
|
+
startedAt,
|
|
11659
|
+
error
|
|
11660
|
+
);
|
|
11509
11661
|
throw error;
|
|
11510
11662
|
}
|
|
11511
|
-
logHermesApiResponse(
|
|
11663
|
+
logHermesApiResponse(
|
|
11664
|
+
options.logger,
|
|
11665
|
+
method,
|
|
11666
|
+
path26,
|
|
11667
|
+
options.profileName,
|
|
11668
|
+
startedAt,
|
|
11669
|
+
response
|
|
11670
|
+
);
|
|
11512
11671
|
return response;
|
|
11513
11672
|
}
|
|
11514
11673
|
async function fetchHermesApi(fetcher, config, path26, init, options) {
|
|
@@ -11526,8 +11685,11 @@ async function fetchHermesApi(fetcher, config, path26, init, options) {
|
|
|
11526
11685
|
throw error;
|
|
11527
11686
|
}
|
|
11528
11687
|
void options.logger?.warn("hermes_api_server_connect_failed", {
|
|
11688
|
+
method: String(init.method ?? "GET").toUpperCase(),
|
|
11529
11689
|
path: path26,
|
|
11690
|
+
profile: options.profileName ?? "default",
|
|
11530
11691
|
port: config.port ?? null,
|
|
11692
|
+
url: `http://127.0.0.1:${config.port}${path26}`,
|
|
11531
11693
|
error: error instanceof Error ? error.message : String(error)
|
|
11532
11694
|
});
|
|
11533
11695
|
throw new LinkHttpError(
|
|
@@ -11537,10 +11699,11 @@ async function fetchHermesApi(fetcher, config, path26, init, options) {
|
|
|
11537
11699
|
);
|
|
11538
11700
|
});
|
|
11539
11701
|
}
|
|
11540
|
-
function logHermesApiResponse(logger, method, path26, startedAt, response) {
|
|
11702
|
+
function logHermesApiResponse(logger, method, path26, profileName, startedAt, response) {
|
|
11541
11703
|
const fields = {
|
|
11542
11704
|
method,
|
|
11543
11705
|
path: path26,
|
|
11706
|
+
profile: profileName ?? "default",
|
|
11544
11707
|
status: response.status,
|
|
11545
11708
|
duration_ms: Date.now() - startedAt
|
|
11546
11709
|
};
|
|
@@ -11560,10 +11723,11 @@ async function logHermesApiFailureResponse(logger, fields, response) {
|
|
|
11560
11723
|
...upstreamError ? { upstream_error: upstreamError } : {}
|
|
11561
11724
|
});
|
|
11562
11725
|
}
|
|
11563
|
-
function logHermesApiError(logger, method, path26, startedAt, error) {
|
|
11726
|
+
function logHermesApiError(logger, method, path26, profileName, startedAt, error) {
|
|
11564
11727
|
void logger?.warn("hermes_api_request_failed", {
|
|
11565
11728
|
method,
|
|
11566
11729
|
path: path26,
|
|
11730
|
+
profile: profileName ?? "default",
|
|
11567
11731
|
duration_ms: Date.now() - startedAt,
|
|
11568
11732
|
...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
|
|
11569
11733
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -15141,6 +15305,24 @@ function readHeader(ctx, name) {
|
|
|
15141
15305
|
const value = ctx.get(name).trim();
|
|
15142
15306
|
return value ? value : null;
|
|
15143
15307
|
}
|
|
15308
|
+
function readUploadFilenameHeader(ctx) {
|
|
15309
|
+
const encoded = readHeader(ctx, "x-filename-base64") ?? readHeader(ctx, "x-filename-b64");
|
|
15310
|
+
if (encoded) {
|
|
15311
|
+
const decoded = decodeBase64Utf8Header(encoded);
|
|
15312
|
+
if (decoded) {
|
|
15313
|
+
return decoded;
|
|
15314
|
+
}
|
|
15315
|
+
}
|
|
15316
|
+
return readHeader(ctx, "x-filename");
|
|
15317
|
+
}
|
|
15318
|
+
function decodeBase64Utf8Header(value) {
|
|
15319
|
+
const trimmed = value.trim();
|
|
15320
|
+
if (!trimmed || !/^[A-Za-z0-9+/]+={0,2}$/u.test(trimmed)) {
|
|
15321
|
+
return null;
|
|
15322
|
+
}
|
|
15323
|
+
const decoded = Buffer.from(trimmed, "base64").toString("utf8").trim();
|
|
15324
|
+
return decoded || null;
|
|
15325
|
+
}
|
|
15144
15326
|
var CONVERSATION_HISTORY_REPLAY_FIELDS = [
|
|
15145
15327
|
"tool_call_id",
|
|
15146
15328
|
"tool_calls",
|
|
@@ -15633,7 +15815,7 @@ function registerConversationRoutes(router, options) {
|
|
|
15633
15815
|
}
|
|
15634
15816
|
const blob = await conversations.writeBlob(ctx.params.conversationId, {
|
|
15635
15817
|
bytes,
|
|
15636
|
-
filename:
|
|
15818
|
+
filename: readUploadFilenameHeader(ctx) ?? void 0,
|
|
15637
15819
|
mime: ctx.get("content-type") || void 0
|
|
15638
15820
|
});
|
|
15639
15821
|
ctx.status = 201;
|
|
@@ -15748,6 +15930,7 @@ function createHttpErrorMiddleware(logger) {
|
|
|
15748
15930
|
{
|
|
15749
15931
|
method: ctx.method,
|
|
15750
15932
|
path: ctx.path,
|
|
15933
|
+
query: ctx.querystring || null,
|
|
15751
15934
|
status,
|
|
15752
15935
|
code,
|
|
15753
15936
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -20254,6 +20437,9 @@ function subscribeHermesUpdateStatus(listener) {
|
|
|
20254
20437
|
return () => updateEvents.off("status", listener);
|
|
20255
20438
|
}
|
|
20256
20439
|
async function readRemoteRelease(options, now) {
|
|
20440
|
+
const context = await readHermesReleaseCheckContext(options.paths).catch(
|
|
20441
|
+
() => null
|
|
20442
|
+
);
|
|
20257
20443
|
const cached = await readReleaseCache(options.paths);
|
|
20258
20444
|
const cacheFresh = cached ? now().getTime() - Date.parse(cached.fetched_at) < RELEASE_CACHE_TTL_MS : false;
|
|
20259
20445
|
if (!options.refreshRemote || cacheFresh) {
|
|
@@ -20267,6 +20453,8 @@ async function readRemoteRelease(options, now) {
|
|
|
20267
20453
|
const snapshot = normalizeServerReleaseSnapshot(await response.json());
|
|
20268
20454
|
if (snapshot.issue) {
|
|
20269
20455
|
void options.logger?.warn("hermes_release_server_cache_issue", {
|
|
20456
|
+
server_base_url: context?.serverBaseUrl ?? null,
|
|
20457
|
+
release_check_url: context?.releaseCheckUrl ?? null,
|
|
20270
20458
|
error: snapshot.issue
|
|
20271
20459
|
});
|
|
20272
20460
|
}
|
|
@@ -20296,6 +20484,10 @@ async function readRemoteRelease(options, now) {
|
|
|
20296
20484
|
} catch (error) {
|
|
20297
20485
|
const issue = error instanceof Error ? error.message : String(error);
|
|
20298
20486
|
void options.logger?.warn("hermes_release_server_check_failed", {
|
|
20487
|
+
server_base_url: context?.serverBaseUrl ?? null,
|
|
20488
|
+
release_check_url: context?.releaseCheckUrl ?? null,
|
|
20489
|
+
cached: cached !== null,
|
|
20490
|
+
cache_fresh: cacheFresh,
|
|
20299
20491
|
error: issue
|
|
20300
20492
|
});
|
|
20301
20493
|
return cached ? { remote: fromCache(cached), state: "cached", issue } : { remote: null, state: "unavailable", issue };
|
|
@@ -20419,6 +20611,14 @@ async function fetchLatestReleaseFromServer(options, fetcher) {
|
|
|
20419
20611
|
clearTimeout(timer);
|
|
20420
20612
|
}
|
|
20421
20613
|
}
|
|
20614
|
+
async function readHermesReleaseCheckContext(paths) {
|
|
20615
|
+
const config = await loadConfig(paths);
|
|
20616
|
+
const url = new URL(SERVER_HERMES_RELEASES_LATEST_PATH, config.serverBaseUrl);
|
|
20617
|
+
return {
|
|
20618
|
+
serverBaseUrl: config.serverBaseUrl,
|
|
20619
|
+
releaseCheckUrl: url.toString()
|
|
20620
|
+
};
|
|
20621
|
+
}
|
|
20422
20622
|
function isProcessAlive2(pid) {
|
|
20423
20623
|
if (!pid || pid <= 0) {
|
|
20424
20624
|
return false;
|
|
@@ -20619,12 +20819,18 @@ async function handleFrame(socket, raw, localPort, abortControllers) {
|
|
|
20619
20819
|
const body = Buffer.from(await response.arrayBuffer()).toString("base64");
|
|
20620
20820
|
socket.send(JSON.stringify({ type: "http.response", id: frame.id, status: response.status, headers, bodyBase64: body }));
|
|
20621
20821
|
} catch (error) {
|
|
20822
|
+
if (abortController.signal.aborted || isAbortError2(error)) {
|
|
20823
|
+
return;
|
|
20824
|
+
}
|
|
20622
20825
|
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
20623
20826
|
socket.send(JSON.stringify({ type: "http.error", id: frame.id, status: 502, message }));
|
|
20624
20827
|
} finally {
|
|
20625
20828
|
abortControllers.delete(frame.id);
|
|
20626
20829
|
}
|
|
20627
20830
|
}
|
|
20831
|
+
function isAbortError2(error) {
|
|
20832
|
+
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
20833
|
+
}
|
|
20628
20834
|
|
|
20629
20835
|
// src/runtime/system-info.ts
|
|
20630
20836
|
import { execFileSync } from "child_process";
|
|
@@ -20645,8 +20851,8 @@ function readLinkSystemInfo() {
|
|
|
20645
20851
|
function buildDefaultDisplayName(input) {
|
|
20646
20852
|
const hostname = normalizeText(input.hostname);
|
|
20647
20853
|
const osLabel = normalizeText(input.osLabel);
|
|
20648
|
-
if (hostname
|
|
20649
|
-
return truncateText(
|
|
20854
|
+
if (hostname) {
|
|
20855
|
+
return truncateText(hostname, 128);
|
|
20650
20856
|
}
|
|
20651
20857
|
return truncateText(hostname ?? osLabel ?? `Hermes Link ${input.platform}`, 128);
|
|
20652
20858
|
}
|
|
@@ -20944,6 +21150,10 @@ function unique(values) {
|
|
|
20944
21150
|
|
|
20945
21151
|
// src/link/network-report-state.ts
|
|
20946
21152
|
var DEFAULT_AUTO_DAILY_LIMIT = 20;
|
|
21153
|
+
async function readNetworkReportState(paths) {
|
|
21154
|
+
const state = await readLinkState(paths);
|
|
21155
|
+
return normalizeNetworkReportState(state.networkReport);
|
|
21156
|
+
}
|
|
20947
21157
|
async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
|
|
20948
21158
|
const snapshot = normalizeNetworkSnapshot(snapshotInput);
|
|
20949
21159
|
await updateNetworkReportState(paths, (current) => ({
|
|
@@ -20993,6 +21203,20 @@ async function reserveAutomaticNetworkReport(paths, snapshotInput, options = {})
|
|
|
20993
21203
|
});
|
|
20994
21204
|
return reservation;
|
|
20995
21205
|
}
|
|
21206
|
+
async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
|
|
21207
|
+
const state = await readNetworkReportState(paths);
|
|
21208
|
+
return {
|
|
21209
|
+
...snapshotInput,
|
|
21210
|
+
publicIpv4s: uniqueStrings([
|
|
21211
|
+
...snapshotInput.publicIpv4s,
|
|
21212
|
+
...state.lastReportedPublicIpv4s
|
|
21213
|
+
]).slice(0, 2),
|
|
21214
|
+
publicIpv6s: uniqueStrings([
|
|
21215
|
+
...snapshotInput.publicIpv6s,
|
|
21216
|
+
...state.lastReportedPublicIpv6s
|
|
21217
|
+
]).slice(0, 2)
|
|
21218
|
+
};
|
|
21219
|
+
}
|
|
20996
21220
|
async function updateNetworkReportState(paths, update) {
|
|
20997
21221
|
const state = await readLinkState(paths);
|
|
20998
21222
|
const next = {
|
|
@@ -21081,6 +21305,9 @@ function sameStringList(left, right) {
|
|
|
21081
21305
|
}
|
|
21082
21306
|
return left.every((value, index) => value === right[index]);
|
|
21083
21307
|
}
|
|
21308
|
+
function uniqueStrings(values) {
|
|
21309
|
+
return [...new Set(values)];
|
|
21310
|
+
}
|
|
21084
21311
|
function formatUtcDay(date) {
|
|
21085
21312
|
return date.toISOString().slice(0, 10);
|
|
21086
21313
|
}
|
|
@@ -21092,7 +21319,7 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
21092
21319
|
if (!identity?.link_id) {
|
|
21093
21320
|
return null;
|
|
21094
21321
|
}
|
|
21095
|
-
const
|
|
21322
|
+
const discoveredRoutes = options.routes ?? await discoverRouteCandidates({
|
|
21096
21323
|
port: config.port,
|
|
21097
21324
|
relayBaseUrl: config.relayBaseUrl,
|
|
21098
21325
|
linkId: identity.link_id,
|
|
@@ -21102,6 +21329,7 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
21102
21329
|
configuredLanHost: config.lanHost,
|
|
21103
21330
|
fetchImpl: options.fetchImpl
|
|
21104
21331
|
});
|
|
21332
|
+
const routes = await mergeLastReportedPublicRoutes(paths, discoveredRoutes);
|
|
21105
21333
|
const systemInfo = readLinkSystemInfo();
|
|
21106
21334
|
const payload = {
|
|
21107
21335
|
type: "hermes_link_status_report",
|
|
@@ -21193,12 +21421,16 @@ function startLanIpMonitor(options) {
|
|
|
21193
21421
|
running = false;
|
|
21194
21422
|
}
|
|
21195
21423
|
};
|
|
21196
|
-
current = check({ forceReport: true, publishToRelay: true });
|
|
21424
|
+
current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
|
|
21197
21425
|
const timer = setInterval(() => {
|
|
21198
|
-
current = check();
|
|
21426
|
+
current = check({ observePublicRoute: false });
|
|
21199
21427
|
}, options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
21200
21428
|
timer.unref?.();
|
|
21201
21429
|
return {
|
|
21430
|
+
async refreshPublicRoutes() {
|
|
21431
|
+
current = check({ forceReport: true, publishToRelay: true, observePublicRoute: true });
|
|
21432
|
+
await current;
|
|
21433
|
+
},
|
|
21202
21434
|
async close() {
|
|
21203
21435
|
closed = true;
|
|
21204
21436
|
clearInterval(timer);
|
|
@@ -21214,16 +21446,17 @@ async function checkLanIpChange(options, context = {}) {
|
|
|
21214
21446
|
if (!identity?.link_id) {
|
|
21215
21447
|
return;
|
|
21216
21448
|
}
|
|
21217
|
-
const
|
|
21449
|
+
const discoveredRoutes = await discoverRouteCandidates({
|
|
21218
21450
|
port: config.port,
|
|
21219
21451
|
relayBaseUrl: config.relayBaseUrl,
|
|
21220
21452
|
linkId: identity.link_id,
|
|
21221
21453
|
installId: identity.install_id,
|
|
21222
21454
|
publicKeyPem: identity.public_key_pem,
|
|
21223
|
-
observePublicRoute: true,
|
|
21455
|
+
observePublicRoute: context.observePublicRoute === true,
|
|
21224
21456
|
configuredLanHost: config.lanHost,
|
|
21225
21457
|
fetchImpl: options.fetchImpl
|
|
21226
21458
|
});
|
|
21459
|
+
const routes = await mergeLastReportedPublicRoutes(options.paths, discoveredRoutes);
|
|
21227
21460
|
if (context.publishToRelay) {
|
|
21228
21461
|
options.onNetworkRoutes?.(routes);
|
|
21229
21462
|
}
|
|
@@ -21239,11 +21472,7 @@ async function checkLanIpChange(options, context = {}) {
|
|
|
21239
21472
|
public_ipv6s: routes.publicIpv6s,
|
|
21240
21473
|
reason: reservation.reason
|
|
21241
21474
|
};
|
|
21242
|
-
|
|
21243
|
-
void options.logger.warn("lan_ip_report_skipped", logFields);
|
|
21244
|
-
} else {
|
|
21245
|
-
void options.logger.debug("lan_ip_report_skipped", logFields);
|
|
21246
|
-
}
|
|
21475
|
+
void options.logger.debug("lan_ip_report_skipped", logFields);
|
|
21247
21476
|
return;
|
|
21248
21477
|
}
|
|
21249
21478
|
try {
|
|
@@ -21286,6 +21515,7 @@ function startCronDeliveryScheduler(options) {
|
|
|
21286
21515
|
);
|
|
21287
21516
|
} catch (error) {
|
|
21288
21517
|
void options.logger.warn("cron_link_delivery_sync_failed", {
|
|
21518
|
+
source: "daemon_scheduler",
|
|
21289
21519
|
error: error instanceof Error ? error.message : String(error)
|
|
21290
21520
|
});
|
|
21291
21521
|
} finally {
|
|
@@ -21315,6 +21545,7 @@ function startHermesSessionSyncScheduler(options) {
|
|
|
21315
21545
|
await options.conversations.syncHermesSessions();
|
|
21316
21546
|
} catch (error) {
|
|
21317
21547
|
void options.logger.warn("hermes_session_sync_failed", {
|
|
21548
|
+
source: "daemon_scheduler",
|
|
21318
21549
|
error: error instanceof Error ? error.message : String(error)
|
|
21319
21550
|
});
|
|
21320
21551
|
} finally {
|
|
@@ -21335,10 +21566,11 @@ function startHermesSessionSyncScheduler(options) {
|
|
|
21335
21566
|
|
|
21336
21567
|
// src/daemon/service.ts
|
|
21337
21568
|
var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
|
|
21569
|
+
var RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS = 15 * 6e4;
|
|
21338
21570
|
async function startLinkService(options = {}) {
|
|
21339
21571
|
const paths = options.paths ?? resolveRuntimePaths();
|
|
21340
|
-
const logger = createFileLogger({ paths });
|
|
21341
21572
|
const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
|
|
21573
|
+
const logger = createFileLogger({ paths, minLevel: config.logLevel });
|
|
21342
21574
|
await logger.info("service_starting", {
|
|
21343
21575
|
port: config.port,
|
|
21344
21576
|
mode: identity?.link_id ? "paired" : "local-only"
|
|
@@ -21357,6 +21589,7 @@ async function startLinkService(options = {}) {
|
|
|
21357
21589
|
const triggerHermesSessionSync = () => {
|
|
21358
21590
|
hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
|
|
21359
21591
|
void logger.warn("hermes_session_sync_failed", {
|
|
21592
|
+
source: "service_startup",
|
|
21360
21593
|
error: error instanceof Error ? error.message : String(error)
|
|
21361
21594
|
});
|
|
21362
21595
|
});
|
|
@@ -21376,13 +21609,18 @@ async function startLinkService(options = {}) {
|
|
|
21376
21609
|
} catch (error) {
|
|
21377
21610
|
await logger.error("service_start_failed", {
|
|
21378
21611
|
port: config.port,
|
|
21612
|
+
link_id: identity?.link_id ?? null,
|
|
21379
21613
|
error: error instanceof Error ? error.message : String(error)
|
|
21380
21614
|
});
|
|
21381
21615
|
await logger.flush();
|
|
21382
21616
|
throw error;
|
|
21383
21617
|
}
|
|
21384
21618
|
server.on("error", (error) => {
|
|
21385
|
-
void logger.error("service_error", {
|
|
21619
|
+
void logger.error("service_error", {
|
|
21620
|
+
port: config.port,
|
|
21621
|
+
link_id: identity?.link_id ?? null,
|
|
21622
|
+
error: error.message
|
|
21623
|
+
});
|
|
21386
21624
|
});
|
|
21387
21625
|
void logger.info("service_started", {
|
|
21388
21626
|
port: config.port,
|
|
@@ -21399,6 +21637,9 @@ async function startLinkService(options = {}) {
|
|
|
21399
21637
|
logger
|
|
21400
21638
|
});
|
|
21401
21639
|
let relay = null;
|
|
21640
|
+
let lanIpMonitor = null;
|
|
21641
|
+
let hasSeenRelayConnected = false;
|
|
21642
|
+
let lastRelayReconnectPublicRouteRefreshAt = 0;
|
|
21402
21643
|
if (identity?.link_id) {
|
|
21403
21644
|
let resolveRelayReady = null;
|
|
21404
21645
|
const relayReady = new Promise((resolve) => {
|
|
@@ -21414,6 +21655,12 @@ async function startLinkService(options = {}) {
|
|
|
21414
21655
|
onStatus: (status) => {
|
|
21415
21656
|
void logger.info("relay_status", status);
|
|
21416
21657
|
if (status.state === "connected") {
|
|
21658
|
+
const now = Date.now();
|
|
21659
|
+
if (hasSeenRelayConnected && lanIpMonitor && now - lastRelayReconnectPublicRouteRefreshAt >= RELAY_RECONNECT_PUBLIC_ROUTE_REFRESH_MIN_INTERVAL_MS) {
|
|
21660
|
+
lastRelayReconnectPublicRouteRefreshAt = now;
|
|
21661
|
+
void lanIpMonitor.refreshPublicRoutes();
|
|
21662
|
+
}
|
|
21663
|
+
hasSeenRelayConnected = true;
|
|
21417
21664
|
resolveRelayReady?.(true);
|
|
21418
21665
|
resolveRelayReady = null;
|
|
21419
21666
|
} else if (status.state === "failed") {
|
|
@@ -21432,7 +21679,7 @@ async function startLinkService(options = {}) {
|
|
|
21432
21679
|
} else {
|
|
21433
21680
|
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
21434
21681
|
}
|
|
21435
|
-
|
|
21682
|
+
lanIpMonitor = startLanIpMonitor({
|
|
21436
21683
|
paths,
|
|
21437
21684
|
logger,
|
|
21438
21685
|
intervalMs: options.lanIpMonitorIntervalMs,
|
|
@@ -21452,7 +21699,7 @@ async function startLinkService(options = {}) {
|
|
|
21452
21699
|
await Promise.all([
|
|
21453
21700
|
scheduler.close(),
|
|
21454
21701
|
hermesSessionSyncScheduler.close(),
|
|
21455
|
-
lanIpMonitor
|
|
21702
|
+
lanIpMonitor?.close(),
|
|
21456
21703
|
hermesSessionSync.catch(() => void 0)
|
|
21457
21704
|
]);
|
|
21458
21705
|
await logger.info("service_stopped");
|
|
@@ -21998,6 +22245,9 @@ async function writeFailedStartState(options, error, targetVersion = null) {
|
|
|
21998
22245
|
return readLinkUpdateStatus(options.paths);
|
|
21999
22246
|
}
|
|
22000
22247
|
async function readRemoteLinkPolicy(options) {
|
|
22248
|
+
const context = await readLinkReleaseCheckContext(options.paths).catch(
|
|
22249
|
+
() => null
|
|
22250
|
+
);
|
|
22001
22251
|
try {
|
|
22002
22252
|
const response = await fetchCurrentLinkReleaseFromServer(
|
|
22003
22253
|
options,
|
|
@@ -22022,6 +22272,8 @@ async function readRemoteLinkPolicy(options) {
|
|
|
22022
22272
|
} catch (error) {
|
|
22023
22273
|
const issue = error instanceof Error ? error.message : String(error);
|
|
22024
22274
|
void options.logger?.warn("link_release_server_check_failed", {
|
|
22275
|
+
server_base_url: context?.serverBaseUrl ?? null,
|
|
22276
|
+
release_check_url: context?.releaseCheckUrl ?? null,
|
|
22025
22277
|
error: issue
|
|
22026
22278
|
});
|
|
22027
22279
|
return { remote: null, state: "unavailable", issue };
|
|
@@ -22080,6 +22332,16 @@ async function fetchCurrentLinkReleaseFromServer(options, fetcher) {
|
|
|
22080
22332
|
clearTimeout(timer);
|
|
22081
22333
|
}
|
|
22082
22334
|
}
|
|
22335
|
+
async function readLinkReleaseCheckContext(paths) {
|
|
22336
|
+
const config = await loadConfig(paths);
|
|
22337
|
+
const url = new URL(SERVER_LINK_CURRENT_RELEASE_PATH, config.serverBaseUrl);
|
|
22338
|
+
url.searchParams.set("channel", "stable");
|
|
22339
|
+
url.searchParams.set("lang", "en");
|
|
22340
|
+
return {
|
|
22341
|
+
serverBaseUrl: config.serverBaseUrl,
|
|
22342
|
+
releaseCheckUrl: url.toString()
|
|
22343
|
+
};
|
|
22344
|
+
}
|
|
22083
22345
|
function computeLinkUpdateState(localVersion, remote) {
|
|
22084
22346
|
if (!remote?.current_version) {
|
|
22085
22347
|
return "unknown";
|
|
@@ -22879,7 +23141,7 @@ function registerSystemRoutes(router, options) {
|
|
|
22879
23141
|
})),
|
|
22880
23142
|
readDeviceSummary(paths),
|
|
22881
23143
|
listHermesProfiles(paths).catch(() => []),
|
|
22882
|
-
readHermesUpdateCheck({ paths, logger
|
|
23144
|
+
readHermesUpdateCheck({ paths, logger }).catch(
|
|
22883
23145
|
(error) => ({
|
|
22884
23146
|
ok: true,
|
|
22885
23147
|
local: {
|
|
@@ -23744,7 +24006,8 @@ function assertLoopbackRequest(request) {
|
|
|
23744
24006
|
// src/http/app.ts
|
|
23745
24007
|
async function createApp(options = {}) {
|
|
23746
24008
|
const paths = options.paths ?? resolveRuntimePaths();
|
|
23747
|
-
const
|
|
24009
|
+
const config = await loadConfig(paths).catch(() => null);
|
|
24010
|
+
const logger = options.logger ?? createFileLogger({ paths, minLevel: config?.logLevel ?? "warn" });
|
|
23748
24011
|
const conversations = options.conversations ?? new ConversationService(paths, logger);
|
|
23749
24012
|
let cronDeliverySyncRunning = false;
|
|
23750
24013
|
const syncCronDeliveries = async () => {
|
|
@@ -23756,6 +24019,7 @@ async function createApp(options = {}) {
|
|
|
23756
24019
|
await syncHermesLinkCronDeliveries(paths, conversations, logger);
|
|
23757
24020
|
} catch (error) {
|
|
23758
24021
|
void logger.warn("cron_link_delivery_sync_failed", {
|
|
24022
|
+
source: "http_app_bootstrap",
|
|
23759
24023
|
error: error instanceof Error ? error.message : String(error)
|
|
23760
24024
|
});
|
|
23761
24025
|
} finally {
|
|
@@ -23794,18 +24058,20 @@ async function createApp(options = {}) {
|
|
|
23794
24058
|
export {
|
|
23795
24059
|
LINK_VERSION,
|
|
23796
24060
|
LINK_COMMAND,
|
|
24061
|
+
LinkHttpError,
|
|
23797
24062
|
resolveHermesProfileDir,
|
|
23798
24063
|
resolveHermesConfigPath,
|
|
23799
24064
|
readHermesApiServerConfig,
|
|
23800
24065
|
ensureHermesApiServerConfig,
|
|
23801
|
-
LinkHttpError,
|
|
23802
24066
|
resolveRuntimePaths,
|
|
23803
24067
|
createFileLogger,
|
|
23804
24068
|
getLinkLogFile,
|
|
23805
24069
|
ensureHermesApiServerAvailable,
|
|
23806
24070
|
readHermesVersion,
|
|
24071
|
+
defaultLinkConfig,
|
|
23807
24072
|
loadConfig,
|
|
23808
24073
|
saveConfig,
|
|
24074
|
+
parseLogLevel,
|
|
23809
24075
|
normalizeLanHost,
|
|
23810
24076
|
ConversationService,
|
|
23811
24077
|
loadIdentity,
|
package/dist/cli/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createFileLogger,
|
|
10
10
|
currentCliScriptPath,
|
|
11
11
|
daemonLogFile,
|
|
12
|
+
defaultLinkConfig,
|
|
12
13
|
detectRuntimeEnvironment,
|
|
13
14
|
ensureHermesApiServerAvailable,
|
|
14
15
|
ensureHermesApiServerConfig,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
loadConfig,
|
|
21
22
|
loadIdentity,
|
|
22
23
|
normalizeLanHost,
|
|
24
|
+
parseLogLevel,
|
|
23
25
|
preparePairing,
|
|
24
26
|
probeLocalLinkService,
|
|
25
27
|
readHermesApiServerConfig,
|
|
@@ -34,7 +36,7 @@ import {
|
|
|
34
36
|
startDaemonProcess,
|
|
35
37
|
startLinkService,
|
|
36
38
|
stopDaemonProcess
|
|
37
|
-
} from "../chunk-
|
|
39
|
+
} from "../chunk-2CHGHWCY.js";
|
|
38
40
|
|
|
39
41
|
// src/cli/index.ts
|
|
40
42
|
import { Command } from "commander";
|
|
@@ -259,6 +261,9 @@ var messages = {
|
|
|
259
261
|
"config.lanHostInvalid": "lan-host must be a private LAN IPv4 address, such as 192.168.1.23.",
|
|
260
262
|
"config.lanHostSet": "Configured LAN host: {value}",
|
|
261
263
|
"config.lanHostUnset": "Configured LAN host cleared.",
|
|
264
|
+
"config.logLevelInvalid": "log-level must be one of: debug, info, warn, error.",
|
|
265
|
+
"config.logLevelSet": "Configured log level: {value}",
|
|
266
|
+
"config.logLevelUnset": "Configured log level reset to the default: {value}.",
|
|
262
267
|
"config.reported": "Updated HermesPilot Server with the latest LAN address.",
|
|
263
268
|
"config.reportSkippedUnpaired": "Hermes Link is not paired yet. The LAN address will be reported after pairing.",
|
|
264
269
|
"daemon.description": "Run Hermes Link in the foreground",
|
|
@@ -354,6 +359,9 @@ var messages = {
|
|
|
354
359
|
"config.lanHostInvalid": "lan-host \u5FC5\u987B\u662F\u5C40\u57DF\u7F51 IPv4 \u5730\u5740\uFF0C\u4F8B\u5982 192.168.1.23\u3002",
|
|
355
360
|
"config.lanHostSet": "\u5DF2\u914D\u7F6E\u5C40\u57DF\u7F51\u4E3B\u673A\uFF1A{value}",
|
|
356
361
|
"config.lanHostUnset": "\u5DF2\u6E05\u9664\u5C40\u57DF\u7F51\u4E3B\u673A\u914D\u7F6E\u3002",
|
|
362
|
+
"config.logLevelInvalid": "log-level \u53EA\u80FD\u662F\u4EE5\u4E0B\u503C\u4E4B\u4E00\uFF1Adebug\u3001info\u3001warn\u3001error\u3002",
|
|
363
|
+
"config.logLevelSet": "\u5DF2\u914D\u7F6E\u65E5\u5FD7\u7EA7\u522B\uFF1A{value}",
|
|
364
|
+
"config.logLevelUnset": "\u5DF2\u5C06\u65E5\u5FD7\u7EA7\u522B\u6062\u590D\u4E3A\u9ED8\u8BA4\u503C\uFF1A{value}\u3002",
|
|
357
365
|
"config.reported": "\u5DF2\u628A\u6700\u65B0\u5C40\u57DF\u7F51\u5730\u5740\u66F4\u65B0\u5230 HermesPilot Server\u3002",
|
|
358
366
|
"config.reportSkippedUnpaired": "Hermes Link \u8FD8\u6CA1\u6709\u914D\u5BF9\uFF0C\u5C40\u57DF\u7F51\u5730\u5740\u4F1A\u5728\u914D\u5BF9\u540E\u4E0A\u62A5\u3002",
|
|
359
367
|
"daemon.description": "\u4EE5\u524D\u53F0\u65B9\u5F0F\u8FD0\u884C Hermes Link",
|
|
@@ -738,16 +746,28 @@ configCommand.command("set").argument("<key>").argument("<value>").description(h
|
|
|
738
746
|
const language = resolveLanguage(current.language);
|
|
739
747
|
const t = translate.bind(null, language);
|
|
740
748
|
const normalizedKey = key.trim().toLowerCase();
|
|
741
|
-
if (normalizedKey
|
|
742
|
-
|
|
749
|
+
if (normalizedKey === "lan-host") {
|
|
750
|
+
const lanHost = normalizeLanHost(value);
|
|
751
|
+
if (!lanHost) {
|
|
752
|
+
throw new Error(t("config.lanHostInvalid"));
|
|
753
|
+
}
|
|
754
|
+
const next = await saveConfig({ lanHost }, paths);
|
|
755
|
+
console.log(t("config.lanHostSet", { value: next.lanHost ?? lanHost }));
|
|
756
|
+
await reportConfigNetworkUpdate(paths, t);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (normalizedKey === "log-level") {
|
|
760
|
+
const logLevel = parseLogLevel(value.trim().toLowerCase());
|
|
761
|
+
if (!logLevel) {
|
|
762
|
+
throw new Error(t("config.logLevelInvalid"));
|
|
763
|
+
}
|
|
764
|
+
const next = await saveConfig({ logLevel }, paths);
|
|
765
|
+
console.log(t("config.logLevelSet", { value: next.logLevel }));
|
|
766
|
+
return;
|
|
743
767
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
throw new Error(t("config.lanHostInvalid"));
|
|
768
|
+
{
|
|
769
|
+
throw new Error(t("config.unknownKey", { key }));
|
|
747
770
|
}
|
|
748
|
-
const next = await saveConfig({ lanHost }, paths);
|
|
749
|
-
console.log(t("config.lanHostSet", { value: next.lanHost ?? lanHost }));
|
|
750
|
-
await reportConfigNetworkUpdate(paths, t);
|
|
751
771
|
});
|
|
752
772
|
configCommand.command("unset").argument("<key>").description(helpText("config.unset.description")).action(async (key) => {
|
|
753
773
|
const paths = resolveRuntimePaths();
|
|
@@ -755,12 +775,22 @@ configCommand.command("unset").argument("<key>").description(helpText("config.un
|
|
|
755
775
|
const language = resolveLanguage(current.language);
|
|
756
776
|
const t = translate.bind(null, language);
|
|
757
777
|
const normalizedKey = key.trim().toLowerCase();
|
|
758
|
-
if (normalizedKey
|
|
778
|
+
if (normalizedKey === "lan-host") {
|
|
779
|
+
await saveConfig({ lanHost: null }, paths);
|
|
780
|
+
console.log(t("config.lanHostUnset"));
|
|
781
|
+
await reportConfigNetworkUpdate(paths, t);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (normalizedKey === "log-level") {
|
|
785
|
+
await saveConfig({ logLevel: defaultLinkConfig.logLevel }, paths);
|
|
786
|
+
console.log(
|
|
787
|
+
t("config.logLevelUnset", { value: defaultLinkConfig.logLevel })
|
|
788
|
+
);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
{
|
|
759
792
|
throw new Error(t("config.unknownKey", { key }));
|
|
760
793
|
}
|
|
761
|
-
await saveConfig({ lanHost: null }, paths);
|
|
762
|
-
console.log(t("config.lanHostUnset"));
|
|
763
|
-
await reportConfigNetworkUpdate(paths, t);
|
|
764
794
|
});
|
|
765
795
|
program.command("start").description(helpText("start.description")).action(async () => {
|
|
766
796
|
const [config, status] = await Promise.all([loadConfig(), getDaemonStatus()]);
|
|
@@ -1033,7 +1063,7 @@ async function deliverStagedFilesFromCli(stagingDir, paths, config) {
|
|
|
1033
1063
|
throw error;
|
|
1034
1064
|
}
|
|
1035
1065
|
}
|
|
1036
|
-
const logger = createFileLogger({ paths });
|
|
1066
|
+
const logger = createFileLogger({ paths, minLevel: config.logLevel });
|
|
1037
1067
|
try {
|
|
1038
1068
|
const conversations = new ConversationService(paths, logger);
|
|
1039
1069
|
return await conversations.deliverStagedFiles(stagingDir);
|
package/dist/http/app.d.ts
CHANGED
|
@@ -310,6 +310,7 @@ interface FileLoggerOptions {
|
|
|
310
310
|
fileName?: string;
|
|
311
311
|
maxFileBytes?: number;
|
|
312
312
|
maxFiles?: number;
|
|
313
|
+
minLevel?: LogLevel;
|
|
313
314
|
now?: () => Date;
|
|
314
315
|
}
|
|
315
316
|
declare class FileLogger {
|
|
@@ -317,6 +318,7 @@ declare class FileLogger {
|
|
|
317
318
|
private readonly paths;
|
|
318
319
|
private readonly maxFileBytes;
|
|
319
320
|
private readonly maxFiles;
|
|
321
|
+
private readonly minLevel;
|
|
320
322
|
private readonly now;
|
|
321
323
|
private queue;
|
|
322
324
|
constructor(options?: FileLoggerOptions);
|
package/dist/http/app.js
CHANGED