@hermespilot/link 0.6.1 → 0.6.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.
@@ -1183,6 +1183,8 @@ import {
1183
1183
  stat
1184
1184
  } from "fs/promises";
1185
1185
  import path2 from "path";
1186
+ var TRANSIENT_RENAME_ERROR_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
1187
+ var RENAME_RETRY_DELAYS_MS = [25, 50, 100, 200, 400];
1186
1188
  async function atomicWriteFilePreservingMetadata(filePath, value, options = {}) {
1187
1189
  const resolvedPath = path2.resolve(filePath);
1188
1190
  const directory = path2.dirname(resolvedPath);
@@ -1214,7 +1216,7 @@ async function atomicWriteFilePreservingMetadata(filePath, value, options = {})
1214
1216
  await handle.close();
1215
1217
  }
1216
1218
  await applyMetadata(tempPath, metadata);
1217
- await rename(tempPath, resolvedPath);
1219
+ await renameWithTransientRetry(tempPath, resolvedPath);
1218
1220
  } catch (error) {
1219
1221
  await rm(tempPath, { force: true });
1220
1222
  throw error;
@@ -1321,6 +1323,30 @@ async function applyOwner(filePath, metadata) {
1321
1323
  }
1322
1324
  }
1323
1325
  }
1326
+ async function renameWithTransientRetry(from, to) {
1327
+ let lastError;
1328
+ for (let attempt = 0; attempt <= RENAME_RETRY_DELAYS_MS.length; attempt += 1) {
1329
+ try {
1330
+ await rename(from, to);
1331
+ return;
1332
+ } catch (error) {
1333
+ lastError = error;
1334
+ if (attempt >= RENAME_RETRY_DELAYS_MS.length || !isTransientRenameError(error)) {
1335
+ throw error;
1336
+ }
1337
+ await delay(RENAME_RETRY_DELAYS_MS[attempt]);
1338
+ }
1339
+ }
1340
+ throw lastError;
1341
+ }
1342
+ function isTransientRenameError(error) {
1343
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && TRANSIENT_RENAME_ERROR_CODES.has(error.code);
1344
+ }
1345
+ function delay(ms) {
1346
+ return new Promise((resolve) => {
1347
+ setTimeout(resolve, ms);
1348
+ });
1349
+ }
1324
1350
  function metadataFromStats(statsValue) {
1325
1351
  return {
1326
1352
  uid: statsValue.uid,
@@ -4852,7 +4878,7 @@ import { execFile as execFile2, spawn } from "child_process";
4852
4878
  import { constants as fsConstants } from "fs";
4853
4879
  import { access, readFile as readFile5, realpath, stat as stat4 } from "fs/promises";
4854
4880
  import path7 from "path";
4855
- import { setTimeout as delay } from "timers/promises";
4881
+ import { setTimeout as delay2 } from "timers/promises";
4856
4882
  import { promisify as promisify2 } from "util";
4857
4883
 
4858
4884
  // src/runtime/logger.ts
@@ -4865,7 +4891,7 @@ import os2 from "os";
4865
4891
  import path5 from "path";
4866
4892
 
4867
4893
  // src/constants.ts
4868
- var LINK_VERSION = "0.6.1";
4894
+ var LINK_VERSION = "0.6.3";
4869
4895
  var LINK_COMMAND = "hermeslink";
4870
4896
  var LINK_DEFAULT_PORT = 52379;
4871
4897
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -5396,6 +5422,17 @@ var HERMES_GATEWAY_ENV_BLOCKLIST_PREFIXES = ["API_SERVER_"];
5396
5422
  var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
5397
5423
  var hermesVersionCache = /* @__PURE__ */ new Map();
5398
5424
  var hermesVersionInFlight = /* @__PURE__ */ new Map();
5425
+ var HermesApiServerUnavailableError = class extends LinkHttpError {
5426
+ constructor(details) {
5427
+ super(
5428
+ 503,
5429
+ "hermes_api_server_unavailable",
5430
+ buildUnavailableErrorMessage(details)
5431
+ );
5432
+ this.details = details;
5433
+ }
5434
+ details;
5435
+ };
5399
5436
  async function ensureHermesApiServerAvailable(options = {}) {
5400
5437
  const profileName = normalizeProfileName(options.profileName);
5401
5438
  await assertProfileExists(profileName);
@@ -5426,11 +5463,12 @@ async function ensureHermesApiServerAvailable(options = {}) {
5426
5463
  reason: "auto_start_disabled",
5427
5464
  issue: health.issue ?? "unknown"
5428
5465
  });
5429
- throw new LinkHttpError(
5430
- 503,
5431
- "hermes_api_server_unavailable",
5432
- `${unavailableMessage()}${health.issue ? ` ${health.issue}` : ""}`
5433
- );
5466
+ throw new HermesApiServerUnavailableError({
5467
+ profileName,
5468
+ port: configResult.apiServer.port ?? null,
5469
+ health,
5470
+ attemptedStart: false
5471
+ });
5434
5472
  }
5435
5473
  void options.logger?.info("gateway_auto_start_requested", {
5436
5474
  profile: profileName,
@@ -5504,11 +5542,15 @@ async function ensureHermesApiServerAvailable(options = {}) {
5504
5542
  issue: health.issue ?? "unknown",
5505
5543
  ...logHint ? { log_hint: logHint } : {}
5506
5544
  });
5507
- throw new LinkHttpError(
5508
- 503,
5509
- "hermes_api_server_unavailable",
5510
- `${unavailableMessage()} Link tried to start it with \`${formatHermesGatewayRunCommand(profileName)}\`, but it did not become ready.${health.issue ? ` ${health.issue}` : ""}${logHint ? ` \u6700\u8FD1 Gateway \u65E5\u5FD7\uFF1A${logHint}` : ""} Gateway log: ${start.logPath}`
5511
- );
5545
+ throw new HermesApiServerUnavailableError({
5546
+ profileName,
5547
+ port: configResult.apiServer.port ?? null,
5548
+ health,
5549
+ attemptedStart: true,
5550
+ startCommand: formatHermesGatewayRunCommand(profileName),
5551
+ gatewayLog: start.logPath,
5552
+ logHint: logHint || void 0
5553
+ });
5512
5554
  }
5513
5555
  async function reloadHermesGateway(options = {}) {
5514
5556
  const profileName = normalizeProfileName(options.profileName);
@@ -5993,7 +6035,7 @@ async function waitForHermesApiHealth(config, fetcher, timeoutMs) {
5993
6035
  const deadline = Date.now() + timeoutMs;
5994
6036
  let health = await readHermesApiServerHealth(config, fetcher);
5995
6037
  while (!health.healthy && !health.terminal && Date.now() < deadline) {
5996
- await delay(400);
6038
+ await delay2(400);
5997
6039
  health = await readHermesApiServerHealth(config, fetcher);
5998
6040
  }
5999
6041
  return health;
@@ -6012,31 +6054,37 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
6012
6054
  const payload = await detailed.json().catch(() => null);
6013
6055
  const record = toRecord2(payload);
6014
6056
  if (!isHermesApiServerHealthPayload(record, { allowStatusOnly: false })) {
6057
+ const issue3 = createHealthIssue("not_hermes_response", {
6058
+ probe: "GET /health/detailed"
6059
+ });
6015
6060
  return {
6016
6061
  healthy: false,
6017
6062
  terminal: true,
6018
- issue: "\u5F53\u524D\u7AEF\u53E3\u8FD4\u56DE\u4E86\u975E Hermes API Server \u7684\u5065\u5EB7\u68C0\u67E5\u54CD\u5E94\uFF0C\u53EF\u80FD\u88AB\u5176\u4ED6\u672C\u673A\u670D\u52A1\u5360\u7528\u3002",
6063
+ ...healthIssueFields(issue3),
6019
6064
  detailed: record
6020
6065
  };
6021
6066
  }
6022
- const issue = describeDetailedHealthIssue(record);
6067
+ const issue2 = describeDetailedHealthIssue(record);
6023
6068
  const terminal = isTerminalDetailedHealth(record);
6024
- const authIssue = issue ? null : await probeHermesApiServerAuth(resolvedConfig, fetcher);
6069
+ const authIssue = issue2 ? null : await probeHermesApiServerAuth(resolvedConfig, fetcher);
6025
6070
  if (authIssue) {
6026
6071
  return authIssue;
6027
6072
  }
6028
6073
  return {
6029
- healthy: !issue,
6074
+ healthy: !issue2,
6030
6075
  terminal,
6031
- ...issue ? { issue } : {},
6076
+ ...issue2 ? healthIssueFields(issue2) : {},
6032
6077
  detailed: record
6033
6078
  };
6034
6079
  }
6035
6080
  if (detailed?.status === 401) {
6081
+ const issue2 = createHealthIssue("auth_invalid", {
6082
+ probe: "GET /health/detailed"
6083
+ });
6036
6084
  return {
6037
6085
  healthy: false,
6038
6086
  authInvalid: true,
6039
- issue: "Hermes API Server \u8FD4\u56DE 401\uFF0C\u5F53\u524D\u7AEF\u53E3\u53EF\u80FD\u5C5E\u4E8E\u53E6\u4E00\u4E2A Profile\uFF0C\u6216 API Server key \u5DF2\u53D8\u66F4\u3002"
6087
+ ...healthIssueFields(issue2)
6040
6088
  };
6041
6089
  }
6042
6090
  const basic = await fetchWithTimeout(
@@ -6053,10 +6101,13 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
6053
6101
  if (!isHermesApiServerHealthPayload(record, {
6054
6102
  allowStatusOnly: true
6055
6103
  })) {
6104
+ const issue2 = createHealthIssue("not_hermes_response", {
6105
+ probe: "GET /health"
6106
+ });
6056
6107
  return {
6057
6108
  healthy: false,
6058
6109
  terminal: true,
6059
- issue: "\u5F53\u524D\u7AEF\u53E3\u8FD4\u56DE\u4E86\u975E Hermes API Server \u7684\u5065\u5EB7\u68C0\u67E5\u54CD\u5E94\uFF0C\u53EF\u80FD\u88AB\u5176\u4ED6\u672C\u673A\u670D\u52A1\u5360\u7528\u3002",
6110
+ ...healthIssueFields(issue2),
6060
6111
  detailed: record
6061
6112
  };
6062
6113
  }
@@ -6067,15 +6118,22 @@ async function readHermesApiServerHealth(config, fetcher = fetch) {
6067
6118
  return { healthy: true };
6068
6119
  }
6069
6120
  if (basic?.status === 401) {
6121
+ const issue2 = createHealthIssue("auth_invalid", {
6122
+ probe: "GET /health"
6123
+ });
6070
6124
  return {
6071
6125
  healthy: false,
6072
6126
  authInvalid: true,
6073
- issue: "Hermes API Server \u8FD4\u56DE 401\uFF0C\u5F53\u524D\u7AEF\u53E3\u53EF\u80FD\u5C5E\u4E8E\u53E6\u4E00\u4E2A Profile\uFF0C\u6216 API Server key \u5DF2\u53D8\u66F4\u3002"
6127
+ ...healthIssueFields(issue2)
6074
6128
  };
6075
6129
  }
6130
+ const issue = createHealthIssue("port_unreachable", {
6131
+ port: resolvedConfig.port,
6132
+ probe: "GET /health/detailed, GET /health"
6133
+ });
6076
6134
  return {
6077
6135
  healthy: false,
6078
- issue: describePortHealthFailure(resolvedConfig.port)
6136
+ ...healthIssueFields(issue)
6079
6137
  };
6080
6138
  }
6081
6139
  async function probeHermesApiServerAuth(config, fetcher) {
@@ -6088,22 +6146,32 @@ async function probeHermesApiServerAuth(config, fetcher) {
6088
6146
  fetcher
6089
6147
  );
6090
6148
  if (!response) {
6149
+ const issue = createHealthIssue("models_probe_timeout", {
6150
+ probe: "GET /v1/models"
6151
+ });
6091
6152
  return {
6092
6153
  healthy: false,
6093
- issue: "Hermes API Server \u9274\u6743\u63A2\u6D4B\u65E0\u54CD\u5E94\uFF1A/v1/models \u6CA1\u6709\u5728\u8D85\u65F6\u65F6\u95F4\u5185\u8FD4\u56DE\u3002"
6154
+ ...healthIssueFields(issue)
6094
6155
  };
6095
6156
  }
6096
6157
  if (response?.status === 401) {
6158
+ const issue = createHealthIssue("auth_invalid", {
6159
+ probe: "GET /v1/models"
6160
+ });
6097
6161
  return {
6098
6162
  healthy: false,
6099
6163
  authInvalid: true,
6100
- issue: "Hermes API Server \u8FD4\u56DE 401\uFF0C\u5F53\u524D\u7AEF\u53E3\u53EF\u80FD\u5C5E\u4E8E\u53E6\u4E00\u4E2A Profile\uFF0C\u6216 API Server key \u5DF2\u53D8\u66F4\u3002"
6164
+ ...healthIssueFields(issue)
6101
6165
  };
6102
6166
  }
6103
6167
  if (response && !response.ok) {
6168
+ const issue = createHealthIssue("models_probe_http", {
6169
+ status: response.status,
6170
+ probe: "GET /v1/models"
6171
+ });
6104
6172
  return {
6105
6173
  healthy: false,
6106
- issue: `Hermes API Server \u9274\u6743\u63A2\u6D4B\u5931\u8D25\uFF1A/v1/models \u8FD4\u56DE HTTP ${response.status}\u3002`
6174
+ ...healthIssueFields(issue)
6107
6175
  };
6108
6176
  }
6109
6177
  return null;
@@ -6129,14 +6197,92 @@ function shouldAutoStart(value) {
6129
6197
  const raw = process.env.HERMESLINK_GATEWAY_AUTOSTART?.trim().toLowerCase();
6130
6198
  return raw !== "0" && raw !== "false" && raw !== "off";
6131
6199
  }
6132
- function unavailableMessage() {
6133
- return "Hermes API Server \u5F53\u524D\u4E0D\u53EF\u7528\u3002\u8BF7\u786E\u8BA4 Hermes Agent \u5DF2\u5B89\u88C5\uFF0C\u5E76\u53EF\u901A\u8FC7 `hermes gateway run` \u542F\u52A8\uFF1BHermesPilot Link \u9700\u8981 Hermes Agent \u7684\u672C\u673A API Server\u3002";
6134
- }
6135
6200
  function isHermesApiServerHealthPayload(payload, options) {
6136
6201
  return payload.platform === "hermes-agent" || options.allowStatusOnly && (payload.status === "ok" || payload.ok === true);
6137
6202
  }
6138
- function describePortHealthFailure(port) {
6139
- return port ? `Hermes API Server \u7AEF\u53E3 ${port} \u6CA1\u6709\u54CD\u5E94\uFF1B\u5982\u679C\u8FD9\u662F\u521A\u521B\u5EFA\u7684 Profile\uFF0C\u53EF\u80FD\u662F Gateway \u5C1A\u672A\u542F\u52A8\u3001\u7AEF\u53E3\u88AB\u5176\u4ED6\u8FDB\u7A0B\u5360\u7528\uFF0C\u6216 Profile \u7684 API Server port \u914D\u7F6E\u4E0D\u6B63\u786E\u3002` : "Hermes API Server \u7AEF\u53E3\u6CA1\u6709\u54CD\u5E94\u3002";
6203
+ function buildUnavailableErrorMessage(details) {
6204
+ const reason = details.health.issue ?? "unknown reason";
6205
+ return `Hermes API Server is unavailable: ${reason}`;
6206
+ }
6207
+ function createHealthIssue(code, values = {}) {
6208
+ switch (code) {
6209
+ case "not_hermes_response":
6210
+ return {
6211
+ code,
6212
+ ...values.probe !== void 0 ? { probe: values.probe } : {},
6213
+ message: "The configured port returned a non-Hermes API Server health response. Another local service may be using this port."
6214
+ };
6215
+ case "auth_invalid":
6216
+ return {
6217
+ code,
6218
+ ...values.probe !== void 0 ? { probe: values.probe } : {},
6219
+ message: "Hermes API Server returned 401. The configured API key may be wrong, or this port may belong to another Profile."
6220
+ };
6221
+ case "models_probe_timeout":
6222
+ return {
6223
+ code,
6224
+ ...values.probe !== void 0 ? { probe: values.probe } : {},
6225
+ message: "Hermes API Server did not respond to /v1/models before the timeout."
6226
+ };
6227
+ case "models_probe_http":
6228
+ return {
6229
+ code,
6230
+ ...values.status !== void 0 ? { status: values.status } : {},
6231
+ ...values.probe !== void 0 ? { probe: values.probe } : {},
6232
+ message: `Hermes API Server /v1/models returned HTTP ${values.status ?? "unknown"}.`
6233
+ };
6234
+ case "port_unreachable":
6235
+ return {
6236
+ code,
6237
+ ...values.port !== void 0 ? { port: values.port } : {},
6238
+ ...values.probe !== void 0 ? { probe: values.probe } : {},
6239
+ message: values.port ? `Hermes API Server port ${values.port} did not respond.` : "Hermes API Server port did not respond."
6240
+ };
6241
+ case "gateway_state":
6242
+ return {
6243
+ code,
6244
+ ...values.state !== void 0 ? { state: values.state } : {},
6245
+ ...values.probe !== void 0 ? { probe: values.probe } : {},
6246
+ message: `Hermes Gateway state is ${values.state ?? "unknown"}.`
6247
+ };
6248
+ case "api_server_state":
6249
+ return {
6250
+ code,
6251
+ ...values.state !== void 0 ? { state: values.state } : {},
6252
+ ...values.detail !== void 0 ? { detail: values.detail } : {},
6253
+ ...values.probe !== void 0 ? { probe: values.probe } : {},
6254
+ message: `Hermes API Server state is ${values.state ?? "unknown"}${values.detail ? `: ${values.detail}` : ""}.`
6255
+ };
6256
+ case "exit_reason":
6257
+ return {
6258
+ code,
6259
+ ...values.detail !== void 0 ? { detail: values.detail } : {},
6260
+ ...values.probe !== void 0 ? { probe: values.probe } : {},
6261
+ message: `Hermes Gateway exit reason: ${values.detail ?? "unknown"}.`
6262
+ };
6263
+ }
6264
+ }
6265
+ function healthIssueFields(issue) {
6266
+ const fields = {
6267
+ issue: issue.message,
6268
+ issueCode: issue.code
6269
+ };
6270
+ if (issue.port !== void 0) {
6271
+ fields.issuePort = issue.port;
6272
+ }
6273
+ if (issue.status !== void 0) {
6274
+ fields.issueStatus = issue.status;
6275
+ }
6276
+ if (issue.state !== void 0) {
6277
+ fields.issueState = issue.state;
6278
+ }
6279
+ if (issue.detail !== void 0) {
6280
+ fields.issueDetail = issue.detail;
6281
+ }
6282
+ if (issue.probe !== void 0) {
6283
+ fields.issueProbe = issue.probe;
6284
+ }
6285
+ return fields;
6140
6286
  }
6141
6287
  function describeDetailedHealthIssue(payload) {
6142
6288
  const gatewayState = readString4(payload, "gateway_state");
@@ -6145,22 +6291,26 @@ function describeDetailedHealthIssue(payload) {
6145
6291
  const apiServer = toRecord2(platforms.api_server);
6146
6292
  const apiServerState = readString4(apiServer, "state");
6147
6293
  const apiServerError = readString4(apiServer, "error_message");
6148
- const platformIssue = firstFatalPlatformIssue(platforms);
6149
- const parts = [];
6150
6294
  if (gatewayState && gatewayState !== "running") {
6151
- parts.push(`Gateway \u72B6\u6001\u662F ${gatewayState}`);
6295
+ return createHealthIssue("gateway_state", {
6296
+ state: gatewayState,
6297
+ probe: "GET /health/detailed"
6298
+ });
6152
6299
  }
6153
6300
  if (apiServerState && !isHealthyPlatformState(apiServerState)) {
6154
- parts.push(
6155
- `API Server \u72B6\u6001\u662F ${apiServerState}${apiServerError ? `\uFF1A${apiServerError}` : ""}`
6156
- );
6301
+ return createHealthIssue("api_server_state", {
6302
+ state: apiServerState,
6303
+ ...apiServerError ? { detail: apiServerError } : {},
6304
+ probe: "GET /health/detailed"
6305
+ });
6157
6306
  }
6158
- if (exitReason) {
6159
- parts.push(`\u9000\u51FA\u539F\u56E0\uFF1A${exitReason}`);
6160
- } else if (platformIssue) {
6161
- parts.push(platformIssue);
6307
+ if (exitReason && gatewayState !== "running") {
6308
+ return createHealthIssue("exit_reason", {
6309
+ detail: exitReason,
6310
+ probe: "GET /health/detailed"
6311
+ });
6162
6312
  }
6163
- return parts.length > 0 ? `Hermes Gateway \u672A\u5C31\u7EEA\uFF1A${dedupe(parts).join("\uFF1B")}\u3002` : null;
6313
+ return null;
6164
6314
  }
6165
6315
  function isTerminalDetailedHealth(payload) {
6166
6316
  const gatewayState = readString4(payload, "gateway_state");
@@ -6168,18 +6318,6 @@ function isTerminalDetailedHealth(payload) {
6168
6318
  const apiServerState = readString4(toRecord2(platforms.api_server), "state");
6169
6319
  return gatewayState === "startup_failed" || apiServerState === "fatal" || apiServerState === "failed";
6170
6320
  }
6171
- function firstFatalPlatformIssue(platforms) {
6172
- for (const [name, value] of Object.entries(platforms)) {
6173
- const platform = toRecord2(value);
6174
- const state = readString4(platform, "state");
6175
- if (state !== "fatal" && state !== "failed") {
6176
- continue;
6177
- }
6178
- const message = readString4(platform, "error_message");
6179
- return `${name} \u72B6\u6001\u662F ${state}${message ? `\uFF1A${message}` : ""}`;
6180
- }
6181
- return null;
6182
- }
6183
6321
  function isHealthyPlatformState(value) {
6184
6322
  return ["connected", "running", "ready", "ok", "healthy"].includes(
6185
6323
  value.toLowerCase()
@@ -9168,12 +9306,12 @@ var ConversationMetadataCoordinator = class {
9168
9306
  return { ...next, last_event_seq: event.seq, updated_at: event.created_at };
9169
9307
  }
9170
9308
  scheduleGeneratedTitleRefresh(conversationId) {
9171
- for (const delay3 of GENERATED_TITLE_RETRY_DELAYS_MS) {
9309
+ for (const delay4 of GENERATED_TITLE_RETRY_DELAYS_MS) {
9172
9310
  setTimeout(() => {
9173
9311
  void this.generateTitleFromFirstRound(conversationId).catch(
9174
9312
  () => void 0
9175
9313
  );
9176
- }, delay3);
9314
+ }, delay4);
9177
9315
  }
9178
9316
  }
9179
9317
  async renameConversation(conversationId, title, input) {
@@ -19600,7 +19738,7 @@ function isExpectedClientDisconnectError2(error, options = {}) {
19600
19738
  import { execFile as execFile4 } from "child_process";
19601
19739
  import { readdir as readdir9, readFile as readFile13, rename as rename3, stat as stat13 } from "fs/promises";
19602
19740
  import path20 from "path";
19603
- import { setTimeout as delay2 } from "timers/promises";
19741
+ import { setTimeout as delay3 } from "timers/promises";
19604
19742
  import { promisify as promisify4 } from "util";
19605
19743
  import YAML2 from "yaml";
19606
19744
  var DEFAULT_PROFILE = "default";
@@ -19861,7 +19999,7 @@ async function waitForProcessesToExit(pids, timeoutMs) {
19861
19999
  const deadline = Date.now() + timeoutMs;
19862
20000
  let remaining = pids.filter(isProcessRunning);
19863
20001
  while (remaining.length > 0 && Date.now() < deadline) {
19864
- await delay2(100);
20002
+ await delay3(100);
19865
20003
  remaining = remaining.filter(isProcessRunning);
19866
20004
  }
19867
20005
  return remaining;
@@ -19880,7 +20018,7 @@ async function waitForProfilePathToRemainAbsent(profilePath) {
19880
20018
  if (await pathExists(profilePath)) {
19881
20019
  return false;
19882
20020
  }
19883
- await delay2(PROFILE_DELETE_VERIFY_INTERVAL_MS);
20021
+ await delay3(PROFILE_DELETE_VERIFY_INTERVAL_MS);
19884
20022
  }
19885
20023
  return !await pathExists(profilePath);
19886
20024
  }
@@ -25352,12 +25490,12 @@ function connectRelayControl(options) {
25352
25490
  onUpdate: options.onStreamBatchPolicy
25353
25491
  };
25354
25492
  const startConnect = () => {
25355
- void waitForPersistedCooldown().then((delay3) => {
25493
+ void waitForPersistedCooldown().then((delay4) => {
25356
25494
  if (closedByUser) {
25357
25495
  return;
25358
25496
  }
25359
- if (delay3 > 0) {
25360
- scheduleTimer(delay3, "cooldown", `Relay reconnect cooldown active for ${delay3}ms`);
25497
+ if (delay4 > 0) {
25498
+ scheduleTimer(delay4, "cooldown", `Relay reconnect cooldown active for ${delay4}ms`);
25361
25499
  return;
25362
25500
  }
25363
25501
  connect();
@@ -25466,8 +25604,8 @@ function connectRelayControl(options) {
25466
25604
  }
25467
25605
  if (recorded.cooldownUntilMs !== null) {
25468
25606
  reconnectAttempts = 0;
25469
- const delay4 = Math.max(0, recorded.cooldownUntilMs - Date.now());
25470
- scheduleTimer(delay4, "cooldown", `Relay reconnect storm guard active for ${delay4}ms`);
25607
+ const delay5 = Math.max(0, recorded.cooldownUntilMs - Date.now());
25608
+ scheduleTimer(delay5, "cooldown", `Relay reconnect storm guard active for ${delay5}ms`);
25471
25609
  return;
25472
25610
  }
25473
25611
  reconnectAttempts += 1;
@@ -25475,15 +25613,15 @@ function connectRelayControl(options) {
25475
25613
  baseMs: backoffBaseMs,
25476
25614
  maxMs: backoffMaxMs
25477
25615
  });
25478
- const delay3 = Math.max(backoffMs, relayRetryAfterMs ?? 0);
25479
- scheduleTimer(delay3, "retrying", `Retrying in ${delay3}ms`);
25616
+ const delay4 = Math.max(backoffMs, relayRetryAfterMs ?? 0);
25617
+ scheduleTimer(delay4, "retrying", `Retrying in ${delay4}ms`);
25480
25618
  }
25481
25619
  async function waitForPersistedCooldown() {
25482
25620
  return await readRelayCooldownDelayMs(paths).catch(() => 0);
25483
25621
  }
25484
- function scheduleTimer(delay3, state, message) {
25622
+ function scheduleTimer(delay4, state, message) {
25485
25623
  options.onStatus?.({ state, attempt: reconnectAttempts, message });
25486
- retryTimer = setTimeout(connect, delay3);
25624
+ retryTimer = setTimeout(connect, delay4);
25487
25625
  retryTimer.unref?.();
25488
25626
  }
25489
25627
  return {
@@ -27723,13 +27861,14 @@ async function buildOfficialInstallCommand(options, targetVersion) {
27723
27861
  HERMESLINK_YES: "1",
27724
27862
  HERMESLINK_NO_PROFILE_EDIT: "1",
27725
27863
  HERMESLINK_NO_PATH_PROMPT: "1",
27864
+ HERMESLINK_SKIP_DOCTOR: "1",
27726
27865
  HERMESLINK_SKIP_RESTART: "1"
27727
27866
  };
27728
27867
  if (process.platform === "win32") {
27729
27868
  const windowsCommand = `& { $ErrorActionPreference = "Stop"; Invoke-RestMethod ${quotePowerShellString(installer.windowsUrl)} | Invoke-Expression }`;
27730
27869
  return {
27731
27870
  command: windowsCommand,
27732
- displayCommand: `$env:HERMESLINK_VERSION="${targetVersion}"; $env:HERMESLINK_YES="1"; $env:HERMESLINK_NO_PATH_PROMPT="1"; $env:HERMESLINK_SKIP_RESTART="1"; ${windowsCommand}; hermeslink restart`,
27871
+ displayCommand: `$env:HERMESLINK_VERSION="${targetVersion}"; $env:HERMESLINK_YES="1"; $env:HERMESLINK_NO_PATH_PROMPT="1"; $env:HERMESLINK_SKIP_DOCTOR="1"; $env:HERMESLINK_SKIP_RESTART="1"; ${windowsCommand}; hermeslink restart`,
27733
27872
  env,
27734
27873
  source: "official-installer",
27735
27874
  installerUrl: installer.windowsUrl
@@ -27738,7 +27877,7 @@ async function buildOfficialInstallCommand(options, targetVersion) {
27738
27877
  const unixCommand = buildUnixInstallCommand(installer.unixUrl);
27739
27878
  return {
27740
27879
  command: unixCommand,
27741
- displayCommand: `HERMESLINK_VERSION=${targetVersion} HERMESLINK_YES=1 HERMESLINK_NO_PROFILE_EDIT=1 HERMESLINK_NO_PATH_PROMPT=1 HERMESLINK_SKIP_RESTART=1 sh -c ${quoteShellToken(unixCommand)} && hermeslink restart`,
27880
+ displayCommand: `HERMESLINK_VERSION=${targetVersion} HERMESLINK_YES=1 HERMESLINK_NO_PROFILE_EDIT=1 HERMESLINK_NO_PATH_PROMPT=1 HERMESLINK_SKIP_DOCTOR=1 HERMESLINK_SKIP_RESTART=1 sh -c ${quoteShellToken(unixCommand)} && hermeslink restart`,
27742
27881
  env,
27743
27882
  source: "official-installer",
27744
27883
  installerUrl: installer.unixUrl
@@ -27920,8 +28059,9 @@ function buildWindowsDetachedUpdaterScript(input) {
27920
28059
  " $env:HERMESLINK_VERSION = $TargetVersion",
27921
28060
  ' $env:HERMESLINK_YES = "1"',
27922
28061
  ' $env:HERMESLINK_NO_PATH_PROMPT = "1"',
28062
+ ' $env:HERMESLINK_SKIP_DOCTOR = "1"',
27923
28063
  ' $env:HERMESLINK_SKIP_RESTART = "1"',
27924
- ' Invoke-Step "Installing Hermes Link $TargetVersion" { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $InstallerPath -Version $TargetVersion -NoPathPrompt -SkipRestart } | Out-Null',
28064
+ ' Invoke-Step "Installing Hermes Link $TargetVersion" { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $InstallerPath -Version $TargetVersion -NoPathPrompt -SkipDoctor -SkipRestart } | Out-Null',
27925
28065
  ' Invoke-Step "Starting Hermes Link after update" { & $NodePath $CliScriptPath start } | Out-Null',
27926
28066
  ' Add-UpdateLog ""',
27927
28067
  ' Add-UpdateLog ("=== link update finished " + (Get-Date).ToUniversalTime().ToString("o") + " exit=0 signal=null ===")',
@@ -29745,7 +29885,6 @@ export {
29745
29885
  LinkHttpError,
29746
29886
  resolveHermesProfileDir,
29747
29887
  resolveHermesConfigPath,
29748
- readHermesApiServerConfig,
29749
29888
  ensureHermesApiServerConfig,
29750
29889
  resolveRuntimePaths,
29751
29890
  createFileLogger,
@@ -29755,6 +29894,7 @@ export {
29755
29894
  getGatewayLogFiles,
29756
29895
  readRecentGatewayLogEntries,
29757
29896
  flushLogFiles,
29897
+ HermesApiServerUnavailableError,
29758
29898
  ensureHermesApiServerAvailable,
29759
29899
  readHermesVersion,
29760
29900
  defaultLinkConfig,
package/dist/cli/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConversationService,
4
+ HermesApiServerUnavailableError,
4
5
  LINK_COMMAND,
5
6
  LINK_VERSION,
6
7
  LinkHttpError,
@@ -28,7 +29,6 @@ import {
28
29
  parseLogLevel,
29
30
  preparePairing,
30
31
  probeLocalLinkService,
31
- readHermesApiServerConfig,
32
32
  readHermesVersion,
33
33
  readPairingClaim,
34
34
  readRecentGatewayLogEntries,
@@ -44,7 +44,7 @@ import {
44
44
  startDaemonProcess,
45
45
  startLinkService,
46
46
  stopDaemonProcess
47
- } from "../chunk-RBMFF32Z.js";
47
+ } from "../chunk-PCJGVIKH.js";
48
48
 
49
49
  // src/cli/index.ts
50
50
  import { Command } from "commander";
@@ -452,6 +452,38 @@ var messages = {
452
452
  "doctor.apiReady": "Hermes API Server: ready",
453
453
  "doctor.apiStarted": "Hermes API Server: started and ready",
454
454
  "doctor.apiUnavailable": "Hermes API Server: unavailable. {message}",
455
+ "doctor.apiUnavailable.summary": "Hermes API Server: unavailable",
456
+ "doctor.apiUnavailable.profile": "Profile: {profile}; port: {port}",
457
+ "doctor.apiUnavailable.diagnosisDivider": "----- API Server diagnosis -----",
458
+ "doctor.apiUnavailable.check": "Check: {check}",
459
+ "doctor.apiUnavailable.result": "Result: {result}",
460
+ "doctor.apiUnavailable.startAttempt": "Link tried to start it with: {command}",
461
+ "doctor.apiUnavailable.startSkipped": "Auto-start was skipped; Link only checked the existing server.",
462
+ "doctor.apiUnavailable.recentLog": "Recent Gateway log: {message}",
463
+ "doctor.apiUnavailable.gatewayLog": "Gateway log: {path}",
464
+ "doctor.apiUnavailable.fix.port": "Suggestion: check whether another process is using port {port}, or run `hermes gateway run --replace` to view the Hermes startup error directly.",
465
+ "doctor.apiUnavailable.fix.auth": "Suggestion: check API_SERVER_KEY in the current Profile .env/config.yaml, or make sure this port belongs to the same Hermes Profile.",
466
+ "doctor.apiUnavailable.fix.notHermes": "Suggestion: change the Hermes API Server port or stop the non-Hermes service using this port.",
467
+ "doctor.apiUnavailable.fix.models": "Suggestion: the health endpoint responds, but the model API does not. Check Hermes provider/model configuration and Gateway logs.",
468
+ "doctor.apiUnavailable.fix.gateway": "Suggestion: start Hermes Gateway with `hermes gateway run --replace` and check the printed Hermes error.",
469
+ "doctor.apiCheck.notHermes": "Read the Hermes health response from {probe}.",
470
+ "doctor.apiCheck.authInvalid": "Call {probe} with the configured API key.",
471
+ "doctor.apiCheck.modelsTimeout": "Call {probe} to verify the API key and model API.",
472
+ "doctor.apiCheck.modelsHttp": "Call {probe} to verify the API key and model API.",
473
+ "doctor.apiCheck.port": "Connect to the Hermes health endpoints: {probe}.",
474
+ "doctor.apiCheck.gatewayState": "Read gateway_state from /health/detailed.",
475
+ "doctor.apiCheck.apiServerState": "Read platforms.api_server.state from /health/detailed.",
476
+ "doctor.apiCheck.exitReason": "Read exit_reason from /health/detailed.",
477
+ "doctor.apiCheck.unknown": "Run the Hermes API Server health checks.",
478
+ "doctor.apiIssue.notHermes": "The configured port returned a non-Hermes health response. Another local service may be using this port.",
479
+ "doctor.apiIssue.authInvalid": "The API key was rejected (HTTP 401). The key in Profile .env/config.yaml may be stale, or this port may belong to another Profile.",
480
+ "doctor.apiIssue.modelsTimeout": "/v1/models did not respond before the timeout.",
481
+ "doctor.apiIssue.modelsHttp": "/v1/models returned HTTP {status}.",
482
+ "doctor.apiIssue.port": "Port {port} did not respond.",
483
+ "doctor.apiIssue.gatewayState": "Gateway state is {state}.",
484
+ "doctor.apiIssue.apiServerState": "API Server state is {state}{detail}.",
485
+ "doctor.apiIssue.exitReason": "Gateway exit reason: {detail}.",
486
+ "doctor.apiIssue.unknown": "{message}",
455
487
  "error.relayPublicKeyMismatch": "Relay rejected the pairing request because the Server-issued bootstrap token does not match this Link public key. Make sure Server and Relay are deployed with the same bootstrap key configuration, then run `hermeslink pair` again.",
456
488
  "error.relayChallengeInvalid": "Relay did not return a valid install challenge.",
457
489
  "error.relayLinkInvalid": "Relay did not return a valid link_id.",
@@ -616,6 +648,38 @@ var messages = {
616
648
  "doctor.apiReady": "Hermes API Server\uFF1A\u5DF2\u5C31\u7EEA",
617
649
  "doctor.apiStarted": "Hermes API Server\uFF1A\u5DF2\u81EA\u52A8\u542F\u52A8\u5E76\u5C31\u7EEA",
618
650
  "doctor.apiUnavailable": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528\u3002{message}",
651
+ "doctor.apiUnavailable.summary": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528",
652
+ "doctor.apiUnavailable.profile": "Profile\uFF1A{profile}\uFF1B\u7AEF\u53E3\uFF1A{port}",
653
+ "doctor.apiUnavailable.diagnosisDivider": "----- API Server \u8BCA\u65AD -----",
654
+ "doctor.apiUnavailable.check": "\u68C0\u6D4B\u73AF\u8282\uFF1A{check}",
655
+ "doctor.apiUnavailable.result": "\u68C0\u6D4B\u7ED3\u679C\uFF1A{result}",
656
+ "doctor.apiUnavailable.startAttempt": "Link \u5DF2\u5C1D\u8BD5\u542F\u52A8\uFF1A{command}",
657
+ "doctor.apiUnavailable.startSkipped": "\u5DF2\u8DF3\u8FC7\u81EA\u52A8\u542F\u52A8\uFF1BLink \u53EA\u68C0\u67E5\u4E86\u5F53\u524D\u5DF2\u6709\u7684 API Server\u3002",
658
+ "doctor.apiUnavailable.recentLog": "\u6700\u8FD1 Gateway \u65E5\u5FD7\uFF1A{message}",
659
+ "doctor.apiUnavailable.gatewayLog": "Gateway \u65E5\u5FD7\uFF1A{path}",
660
+ "doctor.apiUnavailable.fix.port": "\u5EFA\u8BAE\uFF1A\u786E\u8BA4\u7AEF\u53E3 {port} \u662F\u5426\u88AB\u5176\u4ED6\u8FDB\u7A0B\u5360\u7528\uFF0C\u6216\u76F4\u63A5\u8FD0\u884C `hermes gateway run --replace` \u67E5\u770B Hermes \u539F\u59CB\u542F\u52A8\u9519\u8BEF\u3002",
661
+ "doctor.apiUnavailable.fix.auth": "\u5EFA\u8BAE\uFF1A\u68C0\u67E5\u5F53\u524D Profile \u7684 .env/config.yaml \u4E2D API_SERVER_KEY \u662F\u5426\u4E00\u81F4\uFF0C\u6216\u786E\u8BA4\u8FD9\u4E2A\u7AEF\u53E3\u5C5E\u4E8E\u540C\u4E00\u4E2A Hermes Profile\u3002",
662
+ "doctor.apiUnavailable.fix.notHermes": "\u5EFA\u8BAE\uFF1A\u8C03\u6574 Hermes API Server \u7AEF\u53E3\uFF0C\u6216\u505C\u6B62\u5360\u7528\u8BE5\u7AEF\u53E3\u7684\u975E Hermes \u670D\u52A1\u3002",
663
+ "doctor.apiUnavailable.fix.models": "\u5EFA\u8BAE\uFF1A\u5065\u5EB7\u63A5\u53E3\u5DF2\u6709\u54CD\u5E94\uFF0C\u4F46\u6A21\u578B\u63A5\u53E3\u4E0D\u53EF\u7528\uFF1B\u8BF7\u68C0\u67E5 Hermes \u7684\u6A21\u578B/provider \u914D\u7F6E\u548C Gateway \u65E5\u5FD7\u3002",
664
+ "doctor.apiUnavailable.fix.gateway": "\u5EFA\u8BAE\uFF1A\u76F4\u63A5\u8FD0\u884C `hermes gateway run --replace`\uFF0C\u67E5\u770B Hermes Gateway \u8F93\u51FA\u7684\u539F\u59CB\u9519\u8BEF\u3002",
665
+ "doctor.apiCheck.notHermes": "\u8BFB\u53D6 Hermes \u5065\u5EB7\u68C0\u67E5\u54CD\u5E94\uFF08{probe}\uFF09\u3002",
666
+ "doctor.apiCheck.authInvalid": "\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\u7684 API key \u8BF7\u6C42 {probe}\u3002",
667
+ "doctor.apiCheck.modelsTimeout": "\u8BF7\u6C42 {probe}\uFF0C\u9A8C\u8BC1 API key \u548C\u6A21\u578B\u63A5\u53E3\u662F\u5426\u53EF\u7528\u3002",
668
+ "doctor.apiCheck.modelsHttp": "\u8BF7\u6C42 {probe}\uFF0C\u9A8C\u8BC1 API key \u548C\u6A21\u578B\u63A5\u53E3\u662F\u5426\u53EF\u7528\u3002",
669
+ "doctor.apiCheck.port": "\u8FDE\u63A5 Hermes \u5065\u5EB7\u68C0\u67E5\u63A5\u53E3\uFF1A{probe}\u3002",
670
+ "doctor.apiCheck.gatewayState": "\u8BFB\u53D6 /health/detailed \u91CC\u7684 gateway_state\u3002",
671
+ "doctor.apiCheck.apiServerState": "\u8BFB\u53D6 /health/detailed \u91CC\u7684 platforms.api_server.state\u3002",
672
+ "doctor.apiCheck.exitReason": "\u8BFB\u53D6 /health/detailed \u91CC\u7684 exit_reason\u3002",
673
+ "doctor.apiCheck.unknown": "\u6267\u884C Hermes API Server \u5065\u5EB7\u68C0\u67E5\u3002",
674
+ "doctor.apiIssue.notHermes": "\u5F53\u524D\u914D\u7F6E\u7AEF\u53E3\u8FD4\u56DE\u7684\u4E0D\u662F Hermes API Server \u5065\u5EB7\u54CD\u5E94\uFF0C\u53EF\u80FD\u88AB\u5176\u4ED6\u672C\u673A\u670D\u52A1\u5360\u7528\u3002",
675
+ "doctor.apiIssue.authInvalid": "API key \u88AB\u62D2\u7EDD\uFF08HTTP 401\uFF09\u3002\u5F53\u524D Profile .env/config.yaml \u91CC\u7684 key \u53EF\u80FD\u5DF2\u8FC7\u671F\uFF0C\u6216\u8FD9\u4E2A\u7AEF\u53E3\u5C5E\u4E8E\u53E6\u4E00\u4E2A Profile\u3002",
676
+ "doctor.apiIssue.modelsTimeout": "/v1/models \u5728\u8D85\u65F6\u65F6\u95F4\u5185\u6CA1\u6709\u54CD\u5E94\u3002",
677
+ "doctor.apiIssue.modelsHttp": "/v1/models \u8FD4\u56DE HTTP {status}\u3002",
678
+ "doctor.apiIssue.port": "\u7AEF\u53E3 {port} \u6CA1\u6709\u54CD\u5E94\u3002",
679
+ "doctor.apiIssue.gatewayState": "Gateway \u72B6\u6001\u662F {state}\u3002",
680
+ "doctor.apiIssue.apiServerState": "API Server \u72B6\u6001\u662F {state}{detail}\u3002",
681
+ "doctor.apiIssue.exitReason": "Gateway \u9000\u51FA\u539F\u56E0\uFF1A{detail}\u3002",
682
+ "doctor.apiIssue.unknown": "{message}",
619
683
  "error.relayPublicKeyMismatch": "Relay \u62D2\u7EDD\u4E86\u914D\u5BF9\u8BF7\u6C42\uFF1AServer \u7B7E\u53D1\u7684 bootstrap token \u4E0E\u672C\u673A Link \u516C\u94A5\u4E0D\u5339\u914D\u3002\u8BF7\u786E\u8BA4 Server \u548C Relay \u4F7F\u7528\u540C\u4E00\u5957 bootstrap key \u914D\u7F6E\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C `hermeslink pair`\u3002",
620
684
  "error.relayChallengeInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u5B89\u88C5\u6311\u6218\u3002",
621
685
  "error.relayLinkInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684 link_id\u3002",
@@ -717,6 +781,136 @@ function parseLanguage(value) {
717
781
  return null;
718
782
  }
719
783
 
784
+ // src/hermes/api-server-diagnostics.ts
785
+ function formatHermesApiServerUnavailable(error, language) {
786
+ if (!(error instanceof HermesApiServerUnavailableError)) {
787
+ return translate(language, "doctor.apiUnavailable", {
788
+ message: localizeErrorMessage(error, language)
789
+ });
790
+ }
791
+ const details = error.details;
792
+ const port = details.port === null ? "unknown" : String(details.port);
793
+ const lines = [
794
+ translate(language, "doctor.apiUnavailable.summary"),
795
+ translate(language, "doctor.apiUnavailable.profile", {
796
+ profile: details.profileName,
797
+ port
798
+ }),
799
+ translate(language, "doctor.apiUnavailable.diagnosisDivider"),
800
+ translate(language, "doctor.apiUnavailable.check", {
801
+ check: formatHealthCheck(details.health, language)
802
+ }),
803
+ translate(language, "doctor.apiUnavailable.result", {
804
+ result: formatHealthIssue(details.health, language)
805
+ })
806
+ ];
807
+ if (details.attemptedStart && details.startCommand) {
808
+ lines.push(
809
+ translate(language, "doctor.apiUnavailable.startAttempt", {
810
+ command: details.startCommand
811
+ })
812
+ );
813
+ } else if (!details.attemptedStart) {
814
+ lines.push(translate(language, "doctor.apiUnavailable.startSkipped"));
815
+ }
816
+ const fix = formatFix(details.health, language, port);
817
+ if (fix) {
818
+ lines.push(fix);
819
+ }
820
+ if (details.logHint) {
821
+ lines.push(
822
+ translate(language, "doctor.apiUnavailable.recentLog", {
823
+ message: details.logHint
824
+ })
825
+ );
826
+ }
827
+ if (details.gatewayLog) {
828
+ lines.push(
829
+ translate(language, "doctor.apiUnavailable.gatewayLog", {
830
+ path: details.gatewayLog
831
+ })
832
+ );
833
+ }
834
+ return lines.join("\n");
835
+ }
836
+ function formatHealthCheck(health, language) {
837
+ const probe = health.issueProbe ?? "unknown";
838
+ switch (health.issueCode) {
839
+ case "not_hermes_response":
840
+ return translate(language, "doctor.apiCheck.notHermes", { probe });
841
+ case "auth_invalid":
842
+ return translate(language, "doctor.apiCheck.authInvalid", { probe });
843
+ case "models_probe_timeout":
844
+ return translate(language, "doctor.apiCheck.modelsTimeout", { probe });
845
+ case "models_probe_http":
846
+ return translate(language, "doctor.apiCheck.modelsHttp", { probe });
847
+ case "port_unreachable":
848
+ return translate(language, "doctor.apiCheck.port", { probe });
849
+ case "gateway_state":
850
+ return translate(language, "doctor.apiCheck.gatewayState");
851
+ case "api_server_state":
852
+ return translate(language, "doctor.apiCheck.apiServerState");
853
+ case "exit_reason":
854
+ return translate(language, "doctor.apiCheck.exitReason");
855
+ default:
856
+ return translate(language, "doctor.apiCheck.unknown");
857
+ }
858
+ }
859
+ function formatHealthIssue(health, language) {
860
+ switch (health.issueCode) {
861
+ case "not_hermes_response":
862
+ return translate(language, "doctor.apiIssue.notHermes");
863
+ case "auth_invalid":
864
+ return translate(language, "doctor.apiIssue.authInvalid");
865
+ case "models_probe_timeout":
866
+ return translate(language, "doctor.apiIssue.modelsTimeout");
867
+ case "models_probe_http":
868
+ return translate(language, "doctor.apiIssue.modelsHttp", {
869
+ status: health.issueStatus ?? "unknown"
870
+ });
871
+ case "port_unreachable":
872
+ return translate(language, "doctor.apiIssue.port", {
873
+ port: health.issuePort ?? "unknown"
874
+ });
875
+ case "gateway_state":
876
+ return translate(language, "doctor.apiIssue.gatewayState", {
877
+ state: health.issueState ?? "unknown"
878
+ });
879
+ case "api_server_state":
880
+ return translate(language, "doctor.apiIssue.apiServerState", {
881
+ state: health.issueState ?? "unknown",
882
+ detail: health.issueDetail ? `${language === "zh-CN" ? "\uFF1A" : ": "}${health.issueDetail}` : ""
883
+ });
884
+ case "exit_reason":
885
+ return translate(language, "doctor.apiIssue.exitReason", {
886
+ detail: health.issueDetail ?? "unknown"
887
+ });
888
+ default:
889
+ return translate(language, "doctor.apiIssue.unknown", {
890
+ message: health.issue ?? "unknown"
891
+ });
892
+ }
893
+ }
894
+ function formatFix(health, language, port) {
895
+ switch (health.issueCode) {
896
+ case "port_unreachable":
897
+ return translate(language, "doctor.apiUnavailable.fix.port", { port });
898
+ case "auth_invalid":
899
+ return translate(language, "doctor.apiUnavailable.fix.auth");
900
+ case "not_hermes_response":
901
+ return translate(language, "doctor.apiUnavailable.fix.notHermes");
902
+ case "models_probe_timeout":
903
+ case "models_probe_http":
904
+ return translate(language, "doctor.apiUnavailable.fix.models");
905
+ case "gateway_state":
906
+ case "api_server_state":
907
+ case "exit_reason":
908
+ return translate(language, "doctor.apiUnavailable.fix.gateway");
909
+ default:
910
+ return null;
911
+ }
912
+ }
913
+
720
914
  // src/pairing/preflight.ts
721
915
  import { access, stat } from "fs/promises";
722
916
  import path2 from "path";
@@ -755,7 +949,7 @@ async function assertPairingPreflightReady(options = {}) {
755
949
  });
756
950
  }
757
951
  if (failures.length > 0) {
758
- throwPairingPreflightError(failures);
952
+ throwPairingPreflightError(failures, options.language);
759
953
  }
760
954
  options.onProgress?.("hermes_cli");
761
955
  let hermesVersion;
@@ -763,29 +957,20 @@ async function assertPairingPreflightReady(options = {}) {
763
957
  const readVersion = options.readHermesVersion ?? readHermesVersion;
764
958
  hermesVersion = await readVersion();
765
959
  } catch {
766
- throwPairingPreflightError([
767
- {
768
- code: "hermes_cli_unavailable",
769
- zh: "\u6682\u65F6\u627E\u4E0D\u5230\u53EF\u7528\u7684 Hermes CLI \u547D\u4EE4\u3002",
770
- en: "Hermes CLI is not available right now.",
771
- actionZh: "\u8BF7\u786E\u8BA4 Hermes Link \u548C Hermes Agent \u5B89\u88C5\u5728\u540C\u4E00\u4E2A\u7CFB\u7EDF\u73AF\u5883\u4E2D\uFF0C\u5E76\u4E14\u7EC8\u7AEF\u80FD\u76F4\u63A5\u8FD0\u884C `hermes`\u3002\u5982\u679C\u4F60\u4F7F\u7528\u4E86\u81EA\u5B9A\u4E49\u5B89\u88C5\u8DEF\u5F84\uFF0C\u4E5F\u53EF\u4EE5\u8BBE\u7F6E HERMES_BIN\u3002",
772
- actionEn: "Make sure Hermes Link and Hermes Agent are installed in the same system environment, and that the terminal can run `hermes` directly. If Hermes is installed in a custom location, set HERMES_BIN."
773
- }
774
- ]);
960
+ throwPairingPreflightError(
961
+ [
962
+ {
963
+ code: "hermes_cli_unavailable",
964
+ zh: "\u6682\u65F6\u627E\u4E0D\u5230\u53EF\u7528\u7684 Hermes CLI \u547D\u4EE4\u3002",
965
+ en: "Hermes CLI is not available right now.",
966
+ actionZh: "\u8BF7\u786E\u8BA4 Hermes Link \u548C Hermes Agent \u5B89\u88C5\u5728\u540C\u4E00\u4E2A\u7CFB\u7EDF\u73AF\u5883\u4E2D\uFF0C\u5E76\u4E14\u7EC8\u7AEF\u80FD\u76F4\u63A5\u8FD0\u884C `hermes`\u3002\u5982\u679C\u4F60\u4F7F\u7528\u4E86\u81EA\u5B9A\u4E49\u5B89\u88C5\u8DEF\u5F84\uFF0C\u4E5F\u53EF\u4EE5\u8BBE\u7F6E HERMES_BIN\u3002",
967
+ actionEn: "Make sure Hermes Link and Hermes Agent are installed in the same system environment, and that the terminal can run `hermes` directly. If Hermes is installed in a custom location, set HERMES_BIN."
968
+ }
969
+ ],
970
+ options.language
971
+ );
775
972
  }
776
973
  options.onProgress?.("hermes_api_server");
777
- const apiServerConfig = await readHermesApiServerConfig(profileName, configPath);
778
- if (apiServerConfig.enabled !== true) {
779
- throwPairingPreflightError([
780
- {
781
- code: "hermes_api_server_disabled",
782
- zh: "Hermes API Server \u8FD8\u6CA1\u6709\u5F00\u542F\u3002",
783
- en: "Hermes API Server is not enabled.",
784
- actionZh: "\u8BF7\u8FD0\u884C `hermeslink doctor` \u8BA9 Link \u81EA\u52A8\u8865\u9F50 API Server \u914D\u7F6E\uFF0C\u6216\u5728 Hermes \u914D\u7F6E\u4E2D\u542F\u7528 platforms.api_server\u3002",
785
- actionEn: "Run `hermeslink doctor` so Link can prepare the API Server config, or enable platforms.api_server in Hermes config."
786
- }
787
- ]);
788
- }
789
974
  try {
790
975
  const ensureAvailable = options.ensureApiServerAvailable ?? ensureHermesApiServerAvailable;
791
976
  const availability = await ensureAvailable({
@@ -806,32 +991,40 @@ async function assertPairingPreflightReady(options = {}) {
806
991
  host: availability.configResult.apiServer.host ?? null,
807
992
  port: availability.configResult.apiServer.port ?? null
808
993
  },
994
+ notice: availability.configResult.notice,
995
+ backupPath: availability.configResult.backupPath,
809
996
  hermesVersion: {
810
997
  raw: hermesVersion.raw,
811
998
  version: hermesVersion.version
812
999
  }
813
1000
  };
814
1001
  } catch (error) {
815
- throwPairingPreflightError([
816
- {
817
- code: "hermes_api_server_unavailable",
818
- zh: "Hermes API Server \u5F53\u524D\u4E0D\u53EF\u7528\uFF0CLink \u4E0D\u80FD\u786E\u8BA4 App \u914D\u5BF9\u540E\u53EF\u4EE5\u53D1\u9001\u6D88\u606F\u3002",
819
- en: "Hermes API Server is not available, so Link cannot confirm that the App will be able to send messages after pairing.",
820
- actionZh: "\u8BF7\u5148\u8FD0\u884C `hermes gateway run --replace` \u6216 `hermeslink doctor`\uFF0C\u786E\u8BA4 /health \u53EF\u4EE5\u8BBF\u95EE\u540E\u518D\u91CD\u65B0\u6267\u884C `hermeslink pair`\u3002",
821
- actionEn: "Run `hermes gateway run --replace` or `hermeslink doctor` first, then retry `hermeslink pair` after /health is reachable.",
822
- detail: error instanceof Error ? error.message : String(error)
823
- }
824
- ]);
1002
+ throwPairingPreflightError(
1003
+ [
1004
+ {
1005
+ code: "hermes_api_server_unavailable",
1006
+ zh: "Hermes API Server \u9884\u68C0\u5931\u8D25\u3002",
1007
+ en: "Hermes API Server preflight failed.",
1008
+ actionZh: "\u8BF7\u6839\u636E\u4E0B\u9762\u201C\u7EC6\u8282\u201D\u4E2D\u7684\u68C0\u6D4B\u73AF\u8282\u548C\u68C0\u6D4B\u7ED3\u679C\u5904\u7406\u540E\uFF0C\u518D\u91CD\u65B0\u6267\u884C `hermeslink pair`\u3002",
1009
+ actionEn: "Use the check step and result in Detail below, then run `hermeslink pair` again.",
1010
+ detail: options.language === void 0 ? error instanceof Error ? error.message : String(error) : formatHermesApiServerUnavailable(error, options.language)
1011
+ }
1012
+ ],
1013
+ options.language
1014
+ );
825
1015
  }
826
1016
  }
827
- function throwPairingPreflightError(failures) {
1017
+ function throwPairingPreflightError(failures, language) {
828
1018
  throw new LinkHttpError(
829
1019
  503,
830
1020
  failures[0]?.code ?? "pairing_preflight_failed",
831
- formatPairingPreflightMessage(failures)
1021
+ formatPairingPreflightMessage(failures, language)
832
1022
  );
833
1023
  }
834
- function formatPairingPreflightMessage(failures) {
1024
+ function formatPairingPreflightMessage(failures, language) {
1025
+ if (language) {
1026
+ return formatLocalizedPairingPreflightMessage(failures, language);
1027
+ }
835
1028
  const lines = [
836
1029
  "\u914D\u5BF9\u524D\u68C0\u67E5\u6CA1\u6709\u901A\u8FC7\uFF0C\u6682\u65F6\u4E0D\u4F1A\u5411 HermesPilot Server \u6216 Relay \u7533\u8BF7\u914D\u5BF9\u4E8C\u7EF4\u7801/\u914D\u5BF9\u7801\u3002",
837
1030
  "Pairing preflight failed. Link did not request a pairing QR code or pairing code from HermesPilot Server or Relay.",
@@ -849,6 +1042,25 @@ function formatPairingPreflightMessage(failures) {
849
1042
  });
850
1043
  return lines.join("\n");
851
1044
  }
1045
+ function formatLocalizedPairingPreflightMessage(failures, language) {
1046
+ const lines = language === "zh-CN" ? ["\u914D\u5BF9\u524D\u68C0\u67E5\u6CA1\u6709\u901A\u8FC7\uFF0C\u6682\u65F6\u4E0D\u4F1A\u7533\u8BF7\u914D\u5BF9\u4E8C\u7EF4\u7801/\u914D\u5BF9\u7801\u3002", ""] : [
1047
+ "Pairing preflight failed. Link did not request a pairing QR code or pairing code.",
1048
+ ""
1049
+ ];
1050
+ failures.forEach((failure, index) => {
1051
+ const prefix = failures.length > 1 ? `${index + 1}. ` : "";
1052
+ lines.push(`${prefix}${language === "zh-CN" ? failure.zh : failure.en}`);
1053
+ lines.push(
1054
+ language === "zh-CN" ? ` \u5904\u7406\u5EFA\u8BAE\uFF1A${failure.actionZh}` : ` Suggested fix: ${failure.actionEn}`
1055
+ );
1056
+ if (failure.detail) {
1057
+ lines.push(
1058
+ language === "zh-CN" ? ` \u7EC6\u8282\uFF1A${failure.detail}` : ` Detail: ${failure.detail}`
1059
+ );
1060
+ }
1061
+ });
1062
+ return lines.join("\n");
1063
+ }
852
1064
  async function isDirectory(filePath) {
853
1065
  return stat(filePath).then((value) => value.isDirectory()).catch(() => false);
854
1066
  }
@@ -1242,12 +1454,19 @@ program.command("pair").description(helpText("pair.description")).action(async (
1242
1454
  if (environment.warning) {
1243
1455
  console.log(t("doctor.networkWarning", { message: environment.warning }));
1244
1456
  }
1245
- await assertPairingPreflightReady({
1457
+ const preflight = await assertPairingPreflightReady({
1246
1458
  paths,
1459
+ language,
1247
1460
  onProgress: (stage) => {
1248
1461
  console.log(t(pairingPreflightProgressKey(stage)));
1249
1462
  }
1250
1463
  });
1464
+ if (preflight.notice) {
1465
+ console.log(preflight.notice);
1466
+ if (preflight.backupPath) {
1467
+ console.log(`Hermes config backup: ${preflight.backupPath}`);
1468
+ }
1469
+ }
1251
1470
  console.log(t("pair.preparing"));
1252
1471
  await ensureIdentity(paths);
1253
1472
  const hadActiveDevices = await hasActiveDevices(paths);
@@ -1455,7 +1674,7 @@ program.command("doctor").option("--install", helpText("doctor.installOnly")).de
1455
1674
  const availability = await ensureHermesApiServerAvailable({ timeoutMs: 5e3 });
1456
1675
  console.log(t(availability.started ? "doctor.apiStarted" : "doctor.apiReady"));
1457
1676
  } catch (error) {
1458
- console.log(t("doctor.apiUnavailable", { message: error instanceof Error ? error.message : String(error) }));
1677
+ console.log(formatHermesApiServerUnavailable(error, language));
1459
1678
  }
1460
1679
  });
1461
1680
  if (isCliEntrypoint()) {
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-RBMFF32Z.js";
3
+ } from "../chunk-PCJGVIKH.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.6.1",
3
+ "version": "0.6.3",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",
@@ -40,27 +40,27 @@
40
40
  "publish:npm": "npm publish --access public"
41
41
  },
42
42
  "dependencies": {
43
- "@koa/router": "^15.4.0",
44
- "better-sqlite3": "^12.9.0",
45
- "commander": "^12.1.0",
46
- "koa": "^2.15.3",
47
- "qrcode": "^1.5.4",
48
- "qrcode-terminal": "^0.12.0",
49
- "ws": "^8.18.0",
50
- "yaml": "^2.6.1",
51
- "zod": "^3.24.1"
43
+ "@koa/router": "15.4.0",
44
+ "better-sqlite3": "12.9.0",
45
+ "commander": "12.1.0",
46
+ "koa": "2.15.3",
47
+ "qrcode": "1.5.4",
48
+ "qrcode-terminal": "0.12.0",
49
+ "ws": "8.18.0",
50
+ "yaml": "2.6.1",
51
+ "zod": "3.24.1"
52
52
  },
53
53
  "devDependencies": {
54
- "@types/better-sqlite3": "^7.6.13",
55
- "@types/koa": "^2.15.0",
56
- "@types/node": "^20.19.39",
57
- "@types/qrcode": "^1.5.6",
58
- "@types/qrcode-terminal": "^0.12.2",
59
- "@types/ws": "^8.5.13",
60
- "tsup": "^8.3.5",
61
- "tsx": "^4.19.2",
62
- "typescript": "^5.7.2",
63
- "vitest": "^2.1.8"
54
+ "@types/better-sqlite3": "7.6.13",
55
+ "@types/koa": "2.15.0",
56
+ "@types/node": "20.19.39",
57
+ "@types/qrcode": "1.5.6",
58
+ "@types/qrcode-terminal": "0.12.2",
59
+ "@types/ws": "8.5.13",
60
+ "tsup": "8.3.5",
61
+ "tsx": "4.19.2",
62
+ "typescript": "5.7.2",
63
+ "vitest": "2.1.8"
64
64
  },
65
65
  "engines": {
66
66
  "node": ">=20.0.0"