@hermespilot/link 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.3";
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)
@@ -15748,6 +15912,7 @@ function createHttpErrorMiddleware(logger) {
15748
15912
  {
15749
15913
  method: ctx.method,
15750
15914
  path: ctx.path,
15915
+ query: ctx.querystring || null,
15751
15916
  status,
15752
15917
  code,
15753
15918
  error: error instanceof Error ? error.message : String(error)
@@ -20254,6 +20419,9 @@ function subscribeHermesUpdateStatus(listener) {
20254
20419
  return () => updateEvents.off("status", listener);
20255
20420
  }
20256
20421
  async function readRemoteRelease(options, now) {
20422
+ const context = await readHermesReleaseCheckContext(options.paths).catch(
20423
+ () => null
20424
+ );
20257
20425
  const cached = await readReleaseCache(options.paths);
20258
20426
  const cacheFresh = cached ? now().getTime() - Date.parse(cached.fetched_at) < RELEASE_CACHE_TTL_MS : false;
20259
20427
  if (!options.refreshRemote || cacheFresh) {
@@ -20267,6 +20435,8 @@ async function readRemoteRelease(options, now) {
20267
20435
  const snapshot = normalizeServerReleaseSnapshot(await response.json());
20268
20436
  if (snapshot.issue) {
20269
20437
  void options.logger?.warn("hermes_release_server_cache_issue", {
20438
+ server_base_url: context?.serverBaseUrl ?? null,
20439
+ release_check_url: context?.releaseCheckUrl ?? null,
20270
20440
  error: snapshot.issue
20271
20441
  });
20272
20442
  }
@@ -20296,6 +20466,10 @@ async function readRemoteRelease(options, now) {
20296
20466
  } catch (error) {
20297
20467
  const issue = error instanceof Error ? error.message : String(error);
20298
20468
  void options.logger?.warn("hermes_release_server_check_failed", {
20469
+ server_base_url: context?.serverBaseUrl ?? null,
20470
+ release_check_url: context?.releaseCheckUrl ?? null,
20471
+ cached: cached !== null,
20472
+ cache_fresh: cacheFresh,
20299
20473
  error: issue
20300
20474
  });
20301
20475
  return cached ? { remote: fromCache(cached), state: "cached", issue } : { remote: null, state: "unavailable", issue };
@@ -20419,6 +20593,14 @@ async function fetchLatestReleaseFromServer(options, fetcher) {
20419
20593
  clearTimeout(timer);
20420
20594
  }
20421
20595
  }
20596
+ async function readHermesReleaseCheckContext(paths) {
20597
+ const config = await loadConfig(paths);
20598
+ const url = new URL(SERVER_HERMES_RELEASES_LATEST_PATH, config.serverBaseUrl);
20599
+ return {
20600
+ serverBaseUrl: config.serverBaseUrl,
20601
+ releaseCheckUrl: url.toString()
20602
+ };
20603
+ }
20422
20604
  function isProcessAlive2(pid) {
20423
20605
  if (!pid || pid <= 0) {
20424
20606
  return false;
@@ -21239,11 +21421,7 @@ async function checkLanIpChange(options, context = {}) {
21239
21421
  public_ipv6s: routes.publicIpv6s,
21240
21422
  reason: reservation.reason
21241
21423
  };
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
- }
21424
+ void options.logger.debug("lan_ip_report_skipped", logFields);
21247
21425
  return;
21248
21426
  }
21249
21427
  try {
@@ -21286,6 +21464,7 @@ function startCronDeliveryScheduler(options) {
21286
21464
  );
21287
21465
  } catch (error) {
21288
21466
  void options.logger.warn("cron_link_delivery_sync_failed", {
21467
+ source: "daemon_scheduler",
21289
21468
  error: error instanceof Error ? error.message : String(error)
21290
21469
  });
21291
21470
  } finally {
@@ -21315,6 +21494,7 @@ function startHermesSessionSyncScheduler(options) {
21315
21494
  await options.conversations.syncHermesSessions();
21316
21495
  } catch (error) {
21317
21496
  void options.logger.warn("hermes_session_sync_failed", {
21497
+ source: "daemon_scheduler",
21318
21498
  error: error instanceof Error ? error.message : String(error)
21319
21499
  });
21320
21500
  } finally {
@@ -21337,8 +21517,8 @@ function startHermesSessionSyncScheduler(options) {
21337
21517
  var DEFAULT_RELAY_READY_TIMEOUT_MS = 2e3;
21338
21518
  async function startLinkService(options = {}) {
21339
21519
  const paths = options.paths ?? resolveRuntimePaths();
21340
- const logger = createFileLogger({ paths });
21341
21520
  const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
21521
+ const logger = createFileLogger({ paths, minLevel: config.logLevel });
21342
21522
  await logger.info("service_starting", {
21343
21523
  port: config.port,
21344
21524
  mode: identity?.link_id ? "paired" : "local-only"
@@ -21357,6 +21537,7 @@ async function startLinkService(options = {}) {
21357
21537
  const triggerHermesSessionSync = () => {
21358
21538
  hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
21359
21539
  void logger.warn("hermes_session_sync_failed", {
21540
+ source: "service_startup",
21360
21541
  error: error instanceof Error ? error.message : String(error)
21361
21542
  });
21362
21543
  });
@@ -21376,13 +21557,18 @@ async function startLinkService(options = {}) {
21376
21557
  } catch (error) {
21377
21558
  await logger.error("service_start_failed", {
21378
21559
  port: config.port,
21560
+ link_id: identity?.link_id ?? null,
21379
21561
  error: error instanceof Error ? error.message : String(error)
21380
21562
  });
21381
21563
  await logger.flush();
21382
21564
  throw error;
21383
21565
  }
21384
21566
  server.on("error", (error) => {
21385
- void logger.error("service_error", { error: error.message });
21567
+ void logger.error("service_error", {
21568
+ port: config.port,
21569
+ link_id: identity?.link_id ?? null,
21570
+ error: error.message
21571
+ });
21386
21572
  });
21387
21573
  void logger.info("service_started", {
21388
21574
  port: config.port,
@@ -21998,6 +22184,9 @@ async function writeFailedStartState(options, error, targetVersion = null) {
21998
22184
  return readLinkUpdateStatus(options.paths);
21999
22185
  }
22000
22186
  async function readRemoteLinkPolicy(options) {
22187
+ const context = await readLinkReleaseCheckContext(options.paths).catch(
22188
+ () => null
22189
+ );
22001
22190
  try {
22002
22191
  const response = await fetchCurrentLinkReleaseFromServer(
22003
22192
  options,
@@ -22022,6 +22211,8 @@ async function readRemoteLinkPolicy(options) {
22022
22211
  } catch (error) {
22023
22212
  const issue = error instanceof Error ? error.message : String(error);
22024
22213
  void options.logger?.warn("link_release_server_check_failed", {
22214
+ server_base_url: context?.serverBaseUrl ?? null,
22215
+ release_check_url: context?.releaseCheckUrl ?? null,
22025
22216
  error: issue
22026
22217
  });
22027
22218
  return { remote: null, state: "unavailable", issue };
@@ -22080,6 +22271,16 @@ async function fetchCurrentLinkReleaseFromServer(options, fetcher) {
22080
22271
  clearTimeout(timer);
22081
22272
  }
22082
22273
  }
22274
+ async function readLinkReleaseCheckContext(paths) {
22275
+ const config = await loadConfig(paths);
22276
+ const url = new URL(SERVER_LINK_CURRENT_RELEASE_PATH, config.serverBaseUrl);
22277
+ url.searchParams.set("channel", "stable");
22278
+ url.searchParams.set("lang", "en");
22279
+ return {
22280
+ serverBaseUrl: config.serverBaseUrl,
22281
+ releaseCheckUrl: url.toString()
22282
+ };
22283
+ }
22083
22284
  function computeLinkUpdateState(localVersion, remote) {
22084
22285
  if (!remote?.current_version) {
22085
22286
  return "unknown";
@@ -22879,7 +23080,7 @@ function registerSystemRoutes(router, options) {
22879
23080
  })),
22880
23081
  readDeviceSummary(paths),
22881
23082
  listHermesProfiles(paths).catch(() => []),
22882
- readHermesUpdateCheck({ paths, logger, refreshRemote: true }).catch(
23083
+ readHermesUpdateCheck({ paths, logger }).catch(
22883
23084
  (error) => ({
22884
23085
  ok: true,
22885
23086
  local: {
@@ -23744,7 +23945,8 @@ function assertLoopbackRequest(request) {
23744
23945
  // src/http/app.ts
23745
23946
  async function createApp(options = {}) {
23746
23947
  const paths = options.paths ?? resolveRuntimePaths();
23747
- const logger = options.logger ?? createFileLogger({ paths });
23948
+ const config = await loadConfig(paths).catch(() => null);
23949
+ const logger = options.logger ?? createFileLogger({ paths, minLevel: config?.logLevel ?? "warn" });
23748
23950
  const conversations = options.conversations ?? new ConversationService(paths, logger);
23749
23951
  let cronDeliverySyncRunning = false;
23750
23952
  const syncCronDeliveries = async () => {
@@ -23756,6 +23958,7 @@ async function createApp(options = {}) {
23756
23958
  await syncHermesLinkCronDeliveries(paths, conversations, logger);
23757
23959
  } catch (error) {
23758
23960
  void logger.warn("cron_link_delivery_sync_failed", {
23961
+ source: "http_app_bootstrap",
23759
23962
  error: error instanceof Error ? error.message : String(error)
23760
23963
  });
23761
23964
  } finally {
@@ -23794,18 +23997,20 @@ async function createApp(options = {}) {
23794
23997
  export {
23795
23998
  LINK_VERSION,
23796
23999
  LINK_COMMAND,
24000
+ LinkHttpError,
23797
24001
  resolveHermesProfileDir,
23798
24002
  resolveHermesConfigPath,
23799
24003
  readHermesApiServerConfig,
23800
24004
  ensureHermesApiServerConfig,
23801
- LinkHttpError,
23802
24005
  resolveRuntimePaths,
23803
24006
  createFileLogger,
23804
24007
  getLinkLogFile,
23805
24008
  ensureHermesApiServerAvailable,
23806
24009
  readHermesVersion,
24010
+ defaultLinkConfig,
23807
24011
  loadConfig,
23808
24012
  saveConfig,
24013
+ parseLogLevel,
23809
24014
  normalizeLanHost,
23810
24015
  ConversationService,
23811
24016
  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-MFRSQUSE.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-MFRSQUSE.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.3",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",