@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 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.2";
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 { stdout } = await execHermesVersion(options.logger);
4587
- const raw = stdout.trim();
4588
- const version = parseHermesVersion(raw);
4589
- return buildHermesVersionInfo(raw, version);
4590
- } catch (cliError) {
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.summary);
4630
- void logger?.warn("hermes_version_cli_command_failed", failure.fields);
4659
+ failures.push(failure);
4660
+ void logger?.debug("hermes_version_cli_command_attempt_failed", failure.fields);
4631
4661
  }
4632
4662
  }
4633
- throw new Error(failures.join("; "));
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 output = truncateVersionLogOutput(readExecErrorOutput2(error));
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
- ...output ? { output } : {}
5135
+ ...details
5064
5136
  }
5065
5137
  };
5066
5138
  }
5067
- function readExecErrorOutput2(error) {
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
- return `${stdout}
5074
- ${stderr}`.trim();
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 = { ...current, ...patch };
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) => this.deps.runLifecycle.failRun(
8042
- conversationId,
8043
- runId,
8044
- error instanceof Error ? error.message : String(error)
8045
- )
8046
- ).finally(() => {
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(options.logger, method, path26, startedAt, error);
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(options.logger, method, path26, startedAt, response);
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(options.logger, method, path26, startedAt, error);
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(options.logger, method, path26, startedAt, response);
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: readHeader(ctx, "x-filename") ?? void 0,
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 && osLabel && hostname.toLowerCase() !== osLabel.toLowerCase()) {
20649
- return truncateText(`${hostname} - ${osLabel}`, 128);
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 routes = options.routes ?? await discoverRouteCandidates({
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 routes = await discoverRouteCandidates({
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
- if (reservation.reason === "daily_limit_reached") {
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", { error: error.message });
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
- const lanIpMonitor = startLanIpMonitor({
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.close(),
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, refreshRemote: true }).catch(
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 logger = options.logger ?? createFileLogger({ paths });
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-YSSZPVBP.js";
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 !== "lan-host") {
742
- throw new Error(t("config.unknownKey", { key }));
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
- const lanHost = normalizeLanHost(value);
745
- if (!lanHost) {
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 !== "lan-host") {
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);
@@ -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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-YSSZPVBP.js";
3
+ } from "../chunk-2CHGHWCY.js";
4
4
  export {
5
5
  createApp
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hermespilot/link",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",