@hermespilot/link 0.6.6 → 0.6.8

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.
@@ -1359,6 +1359,7 @@ function isNodeError(error, code) {
1359
1359
  }
1360
1360
 
1361
1361
  // src/storage/atomic-json.ts
1362
+ var jsonUpdateQueues = /* @__PURE__ */ new Map();
1362
1363
  async function readJsonFile(filePath) {
1363
1364
  try {
1364
1365
  const raw = await readFile(filePath, "utf8");
@@ -1375,6 +1376,24 @@ async function writeJsonFile(filePath, value, mode = 384) {
1375
1376
  `;
1376
1377
  await atomicWriteFilePreservingMetadata(filePath, payload, { mode });
1377
1378
  }
1379
+ async function updateJsonFile(filePath, update, mode = 384) {
1380
+ const previous = jsonUpdateQueues.get(filePath) ?? Promise.resolve();
1381
+ let next;
1382
+ const operation = previous.catch(() => void 0).then(async () => {
1383
+ const current = await readJsonFile(filePath);
1384
+ next = await update(current);
1385
+ await writeJsonFile(filePath, next, mode);
1386
+ });
1387
+ const queued = operation.catch(() => void 0);
1388
+ jsonUpdateQueues.set(filePath, queued);
1389
+ void queued.finally(() => {
1390
+ if (jsonUpdateQueues.get(filePath) === queued) {
1391
+ jsonUpdateQueues.delete(filePath);
1392
+ }
1393
+ });
1394
+ await operation;
1395
+ return next;
1396
+ }
1378
1397
  function isNodeError2(error, code) {
1379
1398
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
1380
1399
  }
@@ -1465,6 +1484,9 @@ var messages = {
1465
1484
  "config.notice.autoFilled": "Hermes API Server was auto-filled with {fields}; existing port/host/key were not overwritten.",
1466
1485
  "config.notice.repairDefault": "Hermes API Server kept the default port and rotated the key to repair an old Gateway or a key mismatch that caused 401.",
1467
1486
  "config.notice.repairProfile": "Hermes API Server was reassigned a local port and had its key rotated to repair an old Gateway or a key mismatch that caused 401.",
1487
+ "config.yaml.invalid": "Hermes Profile config is not valid YAML, so Link cannot write settings yet. Fix {path}, then try again.{details}{more}",
1488
+ "config.yaml.details": " YAML error: {errors}",
1489
+ "config.yaml.more": " and {count} more error(s).",
1468
1490
  "daemon.description": "Run Hermes Link in the foreground",
1469
1491
  "daemon.foreground": "Hermes Link foreground daemon is running. Press Ctrl+C to stop.",
1470
1492
  "logs.description": "Show or follow Hermes Link logs",
@@ -1520,6 +1542,7 @@ var messages = {
1520
1542
  "pair.autostartFailed": "Pairing succeeded, but boot autostart could not be enabled: {message}",
1521
1543
  "doctor.description": "Run local diagnostics",
1522
1544
  "doctor.installOnly": "only check npm global command and PATH setup",
1545
+ "doctor.noProfiles": "skip Hermes Profile diagnostics",
1523
1546
  "doctor.installHeader": "Install/PATH diagnostics:",
1524
1547
  "doctor.installNpmPrefix": "npm global prefix: {value}",
1525
1548
  "doctor.installGlobalBin": "npm global bin directory: {value}",
@@ -1549,6 +1572,13 @@ var messages = {
1549
1572
  "doctor.apiReady": "Hermes API Server: ready",
1550
1573
  "doctor.apiStarted": "Hermes API Server: started and ready",
1551
1574
  "doctor.apiUnavailable": "Hermes API Server: unavailable. {message}",
1575
+ "doctor.profilesHeader": "Hermes Profiles:",
1576
+ "doctor.profilesNone": "- no profiles found",
1577
+ "doctor.profileLine": "- {profile}: {endpoint}; {state}",
1578
+ "doctor.profileReady": "ready",
1579
+ "doctor.profileNotRunning": "not running ({message})",
1580
+ "doctor.profilePreparedState": "prepared config; {state}",
1581
+ "doctor.profilePrepareFailed": "prepare failed ({message})",
1552
1582
  "doctor.apiUnavailable.summary": "Hermes API Server: unavailable",
1553
1583
  "doctor.apiUnavailable.profile": "Profile: {profile}; port: {port}",
1554
1584
  "doctor.apiUnavailable.diagnosisDivider": "----- API Server diagnosis -----",
@@ -1668,6 +1698,9 @@ var messages = {
1668
1698
  "config.notice.autoFilled": "\u5DF2\u4E3A Hermes API Server \u81EA\u52A8\u8865\u5145 {fields}\uFF1B\u672A\u8986\u76D6\u5DF2\u6709 port/host/key\u3002",
1669
1699
  "config.notice.repairDefault": "\u5DF2\u4E3A Hermes API Server \u4FDD\u6301\u9ED8\u8BA4\u7AEF\u53E3\u5E76\u8F6E\u6362 key\uFF0C\u7528\u4E8E\u4FEE\u590D\u65E7 Gateway \u6216 key \u4E0D\u4E00\u81F4\u5BFC\u81F4\u7684 401\u3002",
1670
1700
  "config.notice.repairProfile": "\u5DF2\u4E3A Hermes API Server \u91CD\u65B0\u5206\u914D\u672C\u673A\u7AEF\u53E3\u5E76\u8F6E\u6362 key\uFF0C\u7528\u4E8E\u4FEE\u590D\u65E7 Gateway \u6216 key \u4E0D\u4E00\u81F4\u5BFC\u81F4\u7684 401\u3002",
1701
+ "config.yaml.invalid": "Hermes Profile \u914D\u7F6E\u6587\u4EF6\u4E0D\u662F\u6709\u6548\u7684 YAML\uFF0C\u6682\u65F6\u65E0\u6CD5\u5199\u5165\u914D\u7F6E\u3002\u8BF7\u5148\u4FEE\u590D {path} \u540E\u91CD\u8BD5\u3002{details}{more}",
1702
+ "config.yaml.details": " YAML \u9519\u8BEF\uFF1A{errors}",
1703
+ "config.yaml.more": " \u7B49 {count} \u5904\u9519\u8BEF\u3002",
1671
1704
  "daemon.description": "\u4EE5\u524D\u53F0\u65B9\u5F0F\u8FD0\u884C Hermes Link",
1672
1705
  "daemon.foreground": "Hermes Link \u524D\u53F0\u670D\u52A1\u6B63\u5728\u8FD0\u884C\u3002\u6309 Ctrl+C \u505C\u6B62\u3002",
1673
1706
  "logs.description": "\u663E\u793A\u6216\u8DDF\u8E2A Hermes Link \u65E5\u5FD7",
@@ -1723,6 +1756,7 @@ var messages = {
1723
1756
  "pair.autostartFailed": "\u914D\u5BF9\u5DF2\u6210\u529F\uFF0C\u4F46\u542F\u7528\u5F00\u673A\u81EA\u542F\u5931\u8D25\uFF1A{message}",
1724
1757
  "doctor.description": "\u8FD0\u884C\u672C\u673A\u8BCA\u65AD",
1725
1758
  "doctor.installOnly": "\u53EA\u68C0\u67E5 npm \u5168\u5C40\u547D\u4EE4\u548C PATH \u8BBE\u7F6E",
1759
+ "doctor.noProfiles": "\u8DF3\u8FC7 Hermes Profile \u8BCA\u65AD",
1726
1760
  "doctor.installHeader": "\u5B89\u88C5 / PATH \u8BCA\u65AD\uFF1A",
1727
1761
  "doctor.installNpmPrefix": "npm \u5168\u5C40 prefix\uFF1A{value}",
1728
1762
  "doctor.installGlobalBin": "npm \u5168\u5C40 bin \u76EE\u5F55\uFF1A{value}",
@@ -1752,6 +1786,13 @@ var messages = {
1752
1786
  "doctor.apiReady": "Hermes API Server\uFF1A\u5DF2\u5C31\u7EEA",
1753
1787
  "doctor.apiStarted": "Hermes API Server\uFF1A\u5DF2\u81EA\u52A8\u542F\u52A8\u5E76\u5C31\u7EEA",
1754
1788
  "doctor.apiUnavailable": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528\u3002{message}",
1789
+ "doctor.profilesHeader": "Hermes Profiles\uFF1A",
1790
+ "doctor.profilesNone": "- \u6CA1\u6709\u627E\u5230 Profile",
1791
+ "doctor.profileLine": "- {profile}\uFF1A{endpoint}\uFF1B{state}",
1792
+ "doctor.profileReady": "\u5DF2\u5C31\u7EEA",
1793
+ "doctor.profileNotRunning": "\u5C1A\u672A\u8FD0\u884C\uFF08{message}\uFF09",
1794
+ "doctor.profilePreparedState": "\u5DF2\u81EA\u52A8\u51C6\u5907\u914D\u7F6E\uFF1B{state}",
1795
+ "doctor.profilePrepareFailed": "\u51C6\u5907\u5931\u8D25\uFF08{message}\uFF09",
1755
1796
  "doctor.apiUnavailable.summary": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528",
1756
1797
  "doctor.apiUnavailable.profile": "Profile\uFF1A{profile}\uFF1B\u7AEF\u53E3\uFF1A{port}",
1757
1798
  "doctor.apiUnavailable.diagnosisDivider": "----- API Server \u8BCA\u65AD -----",
@@ -1894,6 +1935,17 @@ var MODEL_CONFIG_RESTART_HINT = "\u6A21\u578B\u914D\u7F6E\u5DF2\u4FDD\u5B58\u300
1894
1935
  var MODEL_DEFAULTS_APPLIED_HINT = "\u9ED8\u8BA4\u6A21\u578B\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002\u65B0\u7684 Run \u4F1A\u76F4\u63A5\u8BFB\u53D6\u6700\u65B0\u914D\u7F6E\uFF0C\u65E0\u9700\u91CD\u8F7D Hermes Gateway\u3002";
1895
1936
  var PROFILE_PERMISSIONS_RESTART_HINT = "\u6743\u9650\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002\u540E\u7EED\u4EE5\u8BE5 Profile \u53D1\u8D77\u7684\u65B0 Run \u4F1A\u8BFB\u53D6\u6700\u65B0\u914D\u7F6E\uFF1B\u5982\u679C\u8BE5 Profile \u7684 Gateway \u5DF2\u7ECF\u5728\u8FD0\u884C\uFF0C\u9700\u8981\u91CD\u8F7D\u5BF9\u5E94 Gateway\u3002";
1896
1937
  var PROFILE_TOOL_CONFIG_RESTART_HINT = "\u5DE5\u5177\u540E\u7AEF\u914D\u7F6E\u5DF2\u4FDD\u5B58\u3002\u540E\u7EED\u4EE5\u8BE5 Profile \u53D1\u8D77\u7684\u65B0 Run \u4F1A\u8BFB\u53D6\u6700\u65B0\u914D\u7F6E\uFF1B\u5982\u679C\u8BE5 Profile \u7684 Gateway \u5DF2\u7ECF\u5728\u8FD0\u884C\uFF0C\u9700\u8981\u91CD\u8F7D\u5BF9\u5E94 Gateway\u3002";
1938
+ var HermesConfigYamlError = class extends Error {
1939
+ constructor(configPath, errors, language) {
1940
+ super(buildHermesConfigYamlErrorMessage(configPath, errors, language));
1941
+ this.configPath = configPath;
1942
+ this.errors = errors;
1943
+ this.name = "HermesConfigYamlError";
1944
+ }
1945
+ configPath;
1946
+ errors;
1947
+ code = "hermes_config_yaml_invalid";
1948
+ };
1897
1949
  var REASONING_EFFORTS = [
1898
1950
  "none",
1899
1951
  "minimal",
@@ -2805,12 +2857,12 @@ async function saveHermesProfilePermissions(profileName, input, configPath = res
2805
2857
  restartHint: PROFILE_PERMISSIONS_RESTART_HINT
2806
2858
  };
2807
2859
  }
2808
- async function addHermesCommandAllowlistEntry(profileName, entry, configPath = resolveHermesConfigPath(profileName)) {
2860
+ async function addHermesCommandAllowlistEntry(profileName, entry, configPath = resolveHermesConfigPath(profileName), language) {
2809
2861
  const normalizedEntry = entry.trim();
2810
2862
  if (!normalizedEntry) {
2811
2863
  throw new Error("command_allowlist entry must be non-empty");
2812
2864
  }
2813
- const { document, config, existingRaw } = await readHermesConfigDocument(configPath);
2865
+ const { document, config, existingRaw } = await readHermesConfigDocument(configPath, language);
2814
2866
  const current = readStringList(config.command_allowlist);
2815
2867
  if (current.includes(normalizedEntry)) {
2816
2868
  return {
@@ -3132,7 +3184,7 @@ async function repairHermesApiServerConfigUnlocked(profileName = "default", conf
3132
3184
  notice: buildRepairNotice(language, profileName)
3133
3185
  };
3134
3186
  }
3135
- async function readHermesConfigDocument(configPath) {
3187
+ async function readHermesConfigDocument(configPath, language) {
3136
3188
  const existingRaw = await readFile2(configPath, "utf8").catch(
3137
3189
  (error) => {
3138
3190
  if (isNodeError3(error, "ENOENT")) {
@@ -3142,12 +3194,34 @@ async function readHermesConfigDocument(configPath) {
3142
3194
  }
3143
3195
  );
3144
3196
  const document = existingRaw ? YAML.parseDocument(existingRaw) : new YAML.Document({});
3197
+ assertValidHermesConfigDocument(configPath, document, language);
3145
3198
  return {
3146
3199
  document,
3147
3200
  config: toRecord(document.toJSON()),
3148
3201
  existingRaw
3149
3202
  };
3150
3203
  }
3204
+ function assertValidHermesConfigDocument(configPath, document, language) {
3205
+ const errors = document.errors.map((error) => error.message.trim()).filter((message) => message.length > 0);
3206
+ if (errors.length > 0) {
3207
+ throw new HermesConfigYamlError(configPath, errors, language);
3208
+ }
3209
+ }
3210
+ function buildHermesConfigYamlErrorMessage(configPath, errors, language) {
3211
+ const firstErrors = errors.slice(0, 3);
3212
+ const resolvedLanguage = language ?? "zh-CN";
3213
+ const details = firstErrors.length > 0 ? translate(resolvedLanguage, "config.yaml.details", {
3214
+ errors: firstErrors.join("; ")
3215
+ }) : "";
3216
+ const more = errors.length > firstErrors.length ? translate(resolvedLanguage, "config.yaml.more", {
3217
+ count: errors.length - firstErrors.length
3218
+ }) : "";
3219
+ return translate(resolvedLanguage, "config.yaml.invalid", {
3220
+ path: configPath,
3221
+ details,
3222
+ more
3223
+ });
3224
+ }
3151
3225
  async function writeHermesConfigDocument(input) {
3152
3226
  const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
3153
3227
  if (backupPath) {
@@ -4894,12 +4968,24 @@ async function addConfiguredApiServerPort(ports, profileName, excludedProfileNam
4894
4968
  throw error;
4895
4969
  });
4896
4970
  if (!raw.trim()) {
4971
+ const envPort2 = readApiServerPort(
4972
+ (await readHermesApiServerEnvOverrides(profileName)).port
4973
+ );
4974
+ if (envPort2 !== null) {
4975
+ ports.add(envPort2);
4976
+ }
4897
4977
  return;
4898
4978
  }
4899
4979
  const config = toRecord(YAML.parse(raw));
4900
4980
  const apiServer = toRecord(toRecord(config.platforms).api_server);
4901
- const port = readApiServerConfig(apiServer).port;
4902
- if (typeof port === "number" && Number.isFinite(port)) {
4981
+ const envPort = readApiServerPort(
4982
+ (await readHermesApiServerEnvOverrides(profileName)).port
4983
+ );
4984
+ const configPort = readApiServerPort(readApiServerConfig(apiServer).port);
4985
+ for (const port of [envPort, configPort]) {
4986
+ if (port === null) {
4987
+ continue;
4988
+ }
4903
4989
  ports.add(port);
4904
4990
  }
4905
4991
  }
@@ -5418,7 +5504,7 @@ import os2 from "os";
5418
5504
  import path5 from "path";
5419
5505
 
5420
5506
  // src/constants.ts
5421
- var LINK_VERSION = "0.6.6";
5507
+ var LINK_VERSION = "0.6.8";
5422
5508
  var LINK_COMMAND = "hermeslink";
5423
5509
  var LINK_DEFAULT_PORT = 52379;
5424
5510
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -9056,6 +9142,7 @@ var ConversationMaintenanceCoordinator = class {
9056
9142
  clearPlans;
9057
9143
  archivePlans;
9058
9144
  async prepareClearAllConversationPlan(targetStatus = "active") {
9145
+ assertArchivedClearPlanTarget(targetStatus);
9059
9146
  const targets = [];
9060
9147
  for (const conversationId of await this.deps.store.listConversationIds()) {
9061
9148
  const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
@@ -9078,6 +9165,7 @@ var ConversationMaintenanceCoordinator = class {
9078
9165
  }
9079
9166
  async executeClearAllConversationPlan(planId) {
9080
9167
  let plan = await this.clearPlans.read(planId);
9168
+ assertArchivedClearPlanTarget(plan.target_status ?? "active");
9081
9169
  if (plan.status === "completed") {
9082
9170
  return plan;
9083
9171
  }
@@ -9140,6 +9228,7 @@ var ConversationMaintenanceCoordinator = class {
9140
9228
  }
9141
9229
  async startClearAllConversationPlan(planId) {
9142
9230
  const plan = await this.clearPlans.read(planId);
9231
+ assertArchivedClearPlanTarget(plan.target_status ?? "active");
9143
9232
  if (plan.status === "completed" || plan.status === "executing") {
9144
9233
  return plan;
9145
9234
  }
@@ -9621,6 +9710,16 @@ var ConversationMaintenanceCoordinator = class {
9621
9710
  return plan;
9622
9711
  }
9623
9712
  };
9713
+ function assertArchivedClearPlanTarget(targetStatus) {
9714
+ if (targetStatus === "archived") {
9715
+ return;
9716
+ }
9717
+ throw new LinkHttpError(
9718
+ 409,
9719
+ "active_conversation_clear_plan_disabled",
9720
+ "Bulk deletion of active conversations is disabled. Archive active conversations first, or delete explicitly selected conversations."
9721
+ );
9722
+ }
9624
9723
  function isVoiceAttachmentInput(attachment) {
9625
9724
  return attachment.kind === "voice" || attachment.type === "voice" || attachment.is_voice_note === true || attachment.isVoiceNote === true;
9626
9725
  }
@@ -18816,8 +18915,15 @@ var ConversationService = class {
18816
18915
  );
18817
18916
  const result = await addHermesCommandAllowlistEntry(
18818
18917
  profileName,
18819
- patternKey
18820
- );
18918
+ patternKey,
18919
+ void 0,
18920
+ input.language
18921
+ ).catch((error) => {
18922
+ if (error instanceof HermesConfigYamlError) {
18923
+ throw new LinkHttpError(409, error.code, error.message);
18924
+ }
18925
+ throw error;
18926
+ });
18821
18927
  commandAllowlistUpdated = result.changed;
18822
18928
  configPath = result.configPath;
18823
18929
  requiresGatewayReload = result.requiresGatewayReload;
@@ -19529,6 +19635,36 @@ function readString16(body, key) {
19529
19635
  const value = body[key];
19530
19636
  return typeof value === "string" && value.trim() ? value.trim() : null;
19531
19637
  }
19638
+ function readPreferredLanguage(ctx) {
19639
+ const candidates = [
19640
+ ctx.get("x-hermespilot-language"),
19641
+ ctx.get("x-app-language"),
19642
+ ctx.get("accept-language")
19643
+ ];
19644
+ for (const candidate of candidates) {
19645
+ for (const part of candidate.split(",")) {
19646
+ const normalized = part.split(";")[0]?.trim();
19647
+ if (!normalized) {
19648
+ continue;
19649
+ }
19650
+ const language = readSupportedLanguage(normalized);
19651
+ if (language) {
19652
+ return language;
19653
+ }
19654
+ }
19655
+ }
19656
+ return resolveLanguage();
19657
+ }
19658
+ function readSupportedLanguage(value) {
19659
+ const normalized = value.trim().replace("_", "-").toLowerCase();
19660
+ if (normalized.startsWith("zh")) {
19661
+ return "zh-CN";
19662
+ }
19663
+ if (normalized.startsWith("en")) {
19664
+ return "en";
19665
+ }
19666
+ return null;
19667
+ }
19532
19668
  function readOptionalProfileName(body) {
19533
19669
  return readString16(body, "profile") ?? readString16(body, "profile_name") ?? readString16(body, "profileName") ?? void 0;
19534
19670
  }
@@ -19875,7 +20011,7 @@ function isExpectedClientDisconnectError(error) {
19875
20011
 
19876
20012
  // src/http/routes/conversations.ts
19877
20013
  function registerConversationRoutes(router, options) {
19878
- const { paths, conversations } = options;
20014
+ const { paths, logger, conversations } = options;
19879
20015
  router.get("/api/v1/conversations", async (ctx) => {
19880
20016
  await authenticateRequest(ctx, paths);
19881
20017
  ctx.set("cache-control", "no-store");
@@ -20081,12 +20217,20 @@ function registerConversationRoutes(router, options) {
20081
20217
  ctx.body = { ok: true };
20082
20218
  });
20083
20219
  router.post("/api/v1/conversations/clear-plans", async (ctx) => {
20084
- await authenticateRequest(ctx, paths);
20220
+ const auth = await authenticateRequest(ctx, paths);
20085
20221
  const body = await readJsonBody(ctx.req);
20086
20222
  const targetStatus = readConversationClearPlanTargetStatus(body);
20087
20223
  const plan = await conversations.prepareClearAllConversationPlan(
20088
20224
  targetStatus
20089
20225
  );
20226
+ void logger.warn(
20227
+ "conversation_clear_plan_prepared",
20228
+ conversationMutationAuditFields(ctx, auth, {
20229
+ plan_id: plan.id,
20230
+ target_status: plan.target_status,
20231
+ total_count: plan.total_count
20232
+ })
20233
+ );
20090
20234
  ctx.status = 201;
20091
20235
  ctx.body = {
20092
20236
  ok: true,
@@ -20104,10 +20248,19 @@ function registerConversationRoutes(router, options) {
20104
20248
  router.post(
20105
20249
  "/api/v1/conversations/clear-plans/:planId/execute",
20106
20250
  async (ctx) => {
20107
- await authenticateRequest(ctx, paths);
20251
+ const auth = await authenticateRequest(ctx, paths);
20108
20252
  const plan = await conversations.startClearAllConversationPlan(
20109
20253
  ctx.params.planId
20110
20254
  );
20255
+ void logger.warn(
20256
+ "conversation_clear_plan_execute_requested",
20257
+ conversationMutationAuditFields(ctx, auth, {
20258
+ plan_id: plan.id,
20259
+ target_status: plan.target_status,
20260
+ total_count: plan.total_count,
20261
+ status: plan.status
20262
+ })
20263
+ );
20111
20264
  ctx.status = plan.status === "completed" ? 200 : 202;
20112
20265
  ctx.body = {
20113
20266
  ok: true,
@@ -20156,7 +20309,7 @@ function registerConversationRoutes(router, options) {
20156
20309
  }
20157
20310
  );
20158
20311
  router.delete("/api/v1/conversations", async (ctx) => {
20159
- await authenticateRequest(ctx, paths);
20312
+ const auth = await authenticateRequest(ctx, paths);
20160
20313
  const body = await readJsonBody(ctx.req);
20161
20314
  const conversationIds = readStringArray(
20162
20315
  body,
@@ -20171,6 +20324,14 @@ function registerConversationRoutes(router, options) {
20171
20324
  );
20172
20325
  }
20173
20326
  const deleted = await conversations.deleteConversations(conversationIds);
20327
+ void logger.warn(
20328
+ "conversation_bulk_delete_requested",
20329
+ conversationMutationAuditFields(ctx, auth, {
20330
+ requested_count: conversationIds.length,
20331
+ deleted_count: deleted.deleted_count,
20332
+ failed_count: deleted.failed_count
20333
+ })
20334
+ );
20174
20335
  const ok = deleted.failed_count === 0;
20175
20336
  ctx.status = ok ? 200 : 409;
20176
20337
  ctx.body = {
@@ -20209,7 +20370,8 @@ function registerConversationRoutes(router, options) {
20209
20370
  ...await conversations.resolveApproval({
20210
20371
  conversationId: ctx.params.conversationId,
20211
20372
  approvalId: ctx.params.approvalId,
20212
- decision: scope
20373
+ decision: scope,
20374
+ language: readPreferredLanguage(ctx)
20213
20375
  })
20214
20376
  };
20215
20377
  }
@@ -20223,7 +20385,8 @@ function registerConversationRoutes(router, options) {
20223
20385
  ...await conversations.resolveApproval({
20224
20386
  conversationId: ctx.params.conversationId,
20225
20387
  approvalId: ctx.params.approvalId,
20226
- decision: "deny"
20388
+ decision: "deny",
20389
+ language: readPreferredLanguage(ctx)
20227
20390
  })
20228
20391
  };
20229
20392
  }
@@ -20248,10 +20411,21 @@ function registerConversationRoutes(router, options) {
20248
20411
  }
20249
20412
  );
20250
20413
  router.delete("/api/v1/conversations/:conversationId", async (ctx) => {
20251
- await authenticateRequest(ctx, paths);
20414
+ const auth = await authenticateRequest(ctx, paths);
20415
+ const result = await conversations.deleteConversation(
20416
+ ctx.params.conversationId
20417
+ );
20418
+ void logger.warn(
20419
+ "conversation_delete_requested",
20420
+ conversationMutationAuditFields(ctx, auth, {
20421
+ conversation_id: result.conversation_id,
20422
+ hermes_deleted: result.hermes_deleted,
20423
+ hermes_session_count: result.hermes_session_ids?.length ?? 0
20424
+ })
20425
+ );
20252
20426
  ctx.body = {
20253
20427
  ok: true,
20254
- ...await conversations.deleteConversation(ctx.params.conversationId),
20428
+ ...result,
20255
20429
  blob_gc_completed: true
20256
20430
  };
20257
20431
  });
@@ -20314,6 +20488,21 @@ function readConversationClearPlanTargetStatus(body) {
20314
20488
  "Conversation clear plan target status is invalid"
20315
20489
  );
20316
20490
  }
20491
+ function conversationMutationAuditFields(ctx, auth, fields) {
20492
+ return {
20493
+ method: ctx.method,
20494
+ path: ctx.path,
20495
+ auth_kind: auth.kind,
20496
+ device_id: auth.device?.id ?? null,
20497
+ device_label: auth.device?.label ?? null,
20498
+ device_platform: auth.device?.platform ?? null,
20499
+ device_model: auth.device?.model ?? null,
20500
+ account_id: auth.accountId ?? null,
20501
+ app_instance_id: auth.appInstanceId ?? null,
20502
+ user_agent: ctx.get("user-agent") || null,
20503
+ ...fields
20504
+ };
20505
+ }
20317
20506
  function readNonNegativeIntegerHeader(value) {
20318
20507
  const raw = Array.isArray(value) ? value[0] : value;
20319
20508
  if (!raw) {
@@ -20448,7 +20637,9 @@ var PROFILE_DELETE_VERIFY_INTERVAL_MS = 150;
20448
20637
  var execFileAsync4 = promisify4(execFile4);
20449
20638
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
20450
20639
  const profiles = /* @__PURE__ */ new Map();
20451
- profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
20640
+ if (await hasDefaultProfileConfigSource()) {
20641
+ profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
20642
+ }
20452
20643
  const profilesDir = resolveHermesProfilesDir();
20453
20644
  const entries = await readdir9(profilesDir, { withFileTypes: true }).catch(
20454
20645
  (error) => {
@@ -20473,6 +20664,36 @@ async function listHermesProfiles(paths = resolveRuntimePaths()) {
20473
20664
  return left.name.localeCompare(right.name);
20474
20665
  });
20475
20666
  }
20667
+ async function prepareHermesProfilesForUse(paths = resolveRuntimePaths()) {
20668
+ const profiles = await listHermesProfiles(paths);
20669
+ const prepared = [];
20670
+ for (const profile of profiles) {
20671
+ try {
20672
+ const result = await ensureHermesApiServerKey(
20673
+ profile.name,
20674
+ profile.configPath
20675
+ );
20676
+ prepared.push({
20677
+ profile,
20678
+ apiServer: result.apiServer,
20679
+ changed: result.changed,
20680
+ backupPath: result.backupPath,
20681
+ notice: result.notice,
20682
+ error: null
20683
+ });
20684
+ } catch (error) {
20685
+ prepared.push({
20686
+ profile,
20687
+ apiServer: null,
20688
+ changed: false,
20689
+ backupPath: null,
20690
+ notice: null,
20691
+ error: errorMessage(error)
20692
+ });
20693
+ }
20694
+ }
20695
+ return prepared;
20696
+ }
20476
20697
  async function getHermesProfileStatus(name, paths = resolveRuntimePaths()) {
20477
20698
  assertProfileName(name);
20478
20699
  const profile = await profileInfo(name, paths);
@@ -20596,6 +20817,19 @@ async function pathExists(targetPath) {
20596
20817
  throw error;
20597
20818
  });
20598
20819
  }
20820
+ async function hasDefaultProfileConfigSource() {
20821
+ const profilePath = resolveHermesProfileDir(DEFAULT_PROFILE);
20822
+ const profileStat = await stat13(profilePath).catch((error) => {
20823
+ if (isNodeError15(error, "ENOENT")) {
20824
+ return null;
20825
+ }
20826
+ throw error;
20827
+ });
20828
+ if (!profileStat?.isDirectory()) {
20829
+ return false;
20830
+ }
20831
+ return await pathExists(resolveHermesConfigPath(DEFAULT_PROFILE)) || await pathExists(path20.join(profilePath, ".env"));
20832
+ }
20599
20833
  async function deleteHermesProfileWithCli(name) {
20600
20834
  try {
20601
20835
  await execFileAsync4(resolveHermesBin(), ["profile", "delete", name, "--yes"], {
@@ -20747,6 +20981,9 @@ function readExecErrorOutput2(error) {
20747
20981
  function isNodeError15(error, code) {
20748
20982
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
20749
20983
  }
20984
+ function errorMessage(error) {
20985
+ return error instanceof Error ? error.message : String(error);
20986
+ }
20750
20987
  function escapeRegExp2(value) {
20751
20988
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20752
20989
  }
@@ -21473,7 +21710,11 @@ function registerProfileCatalogRoutes(router, options) {
21473
21710
  });
21474
21711
  }
21475
21712
  async function readProfileCatalogItem(profile) {
21476
- const [capabilities, permissions, modelConfigs] = await Promise.all([
21713
+ const [apiServer, capabilities, permissions, modelConfigs] = await Promise.all([
21714
+ readCatalogField(
21715
+ "apiServer",
21716
+ () => readHermesApiServerConfig(profile.name, profile.configPath)
21717
+ ),
21477
21718
  readCatalogField(
21478
21719
  "capabilities",
21479
21720
  () => readHermesProfileCapabilities(profile.name)
@@ -21486,10 +21727,17 @@ async function readProfileCatalogItem(profile) {
21486
21727
  ]);
21487
21728
  return {
21488
21729
  profile,
21730
+ apiServer: apiServer.value ? {
21731
+ host: apiServer.value.host ?? null,
21732
+ port: apiServer.value.port ?? null,
21733
+ configured: Boolean(apiServer.value.key),
21734
+ changed: false
21735
+ } : null,
21489
21736
  capabilities: capabilities.value,
21490
21737
  permissions: permissions.value,
21491
21738
  modelConfigs: modelConfigs.value,
21492
21739
  errors: [
21740
+ ...apiServer.errors,
21493
21741
  ...capabilities.errors,
21494
21742
  ...permissions.errors,
21495
21743
  ...modelConfigs.errors
@@ -21502,11 +21750,11 @@ async function readCatalogField(field, load) {
21502
21750
  } catch (error) {
21503
21751
  return {
21504
21752
  value: null,
21505
- errors: [{ field, message: errorMessage(error) }]
21753
+ errors: [{ field, message: errorMessage2(error) }]
21506
21754
  };
21507
21755
  }
21508
21756
  }
21509
- function errorMessage(error) {
21757
+ function errorMessage2(error) {
21510
21758
  return error instanceof Error ? error.message : String(error);
21511
21759
  }
21512
21760
 
@@ -21619,7 +21867,7 @@ async function ensureHermesLinkSkillInstalledForProfiles(options = {}) {
21619
21867
  const skillChanged = await writeHermesLinkSkill(skillPath);
21620
21868
  const profiles = await listHermesProfiles(paths);
21621
21869
  const results = [];
21622
- for (const profile of profiles) {
21870
+ for (const profile of withDefaultProfilePlaceholder(profiles)) {
21623
21871
  try {
21624
21872
  results.push(await ensureProfileUsesExternalSkillDir(profile, externalDir));
21625
21873
  } catch (error) {
@@ -21673,6 +21921,25 @@ async function ensureHermesLinkSkillInstalledBestEffort(options = {}) {
21673
21921
  });
21674
21922
  }
21675
21923
  }
21924
+ function withDefaultProfilePlaceholder(profiles) {
21925
+ if (profiles.some((profile) => profile.name === "default")) {
21926
+ return profiles;
21927
+ }
21928
+ return [
21929
+ {
21930
+ uid: "default",
21931
+ name: "default",
21932
+ active: false,
21933
+ path: resolveHermesProfileDir("default"),
21934
+ configPath: resolveHermesConfigPath("default"),
21935
+ displayName: null,
21936
+ description: null,
21937
+ avatarType: "default",
21938
+ avatarUrl: null
21939
+ },
21940
+ ...profiles
21941
+ ];
21942
+ }
21676
21943
  function resolveHermesLinkSkillExternalDir(paths = resolveRuntimePaths()) {
21677
21944
  return path21.join(paths.homeDir, HERMES_LINK_SKILL_ROOT_DIR);
21678
21945
  }
@@ -25153,6 +25420,16 @@ function registerProfileRoutes(router, options) {
25153
25420
  profiles: await listHermesProfiles(paths)
25154
25421
  };
25155
25422
  });
25423
+ router.post("/api/v1/profiles/prepare", async (ctx) => {
25424
+ await authenticateRequest(ctx, paths);
25425
+ ctx.set("cache-control", "no-store");
25426
+ ctx.body = {
25427
+ ok: true,
25428
+ profiles: (await prepareHermesProfilesForUse(paths)).map(
25429
+ formatProfilePreparation
25430
+ )
25431
+ };
25432
+ });
25156
25433
  router.get("/api/v1/profile-creation/status", async (ctx) => {
25157
25434
  await authenticateRequest(ctx, paths);
25158
25435
  ctx.set("cache-control", "no-store");
@@ -25252,6 +25529,20 @@ function registerProfileRoutes(router, options) {
25252
25529
  ctx.status = 204;
25253
25530
  });
25254
25531
  }
25532
+ function formatProfilePreparation(item) {
25533
+ return {
25534
+ profile: item.profile,
25535
+ apiServer: item.apiServer ? {
25536
+ host: item.apiServer.host ?? null,
25537
+ port: item.apiServer.port ?? null,
25538
+ configured: Boolean(item.apiServer.key),
25539
+ changed: item.changed
25540
+ } : null,
25541
+ changed: item.changed,
25542
+ notice: item.notice,
25543
+ error: item.error
25544
+ };
25545
+ }
25255
25546
  function readProfileName(body) {
25256
25547
  if (typeof body.name !== "string") {
25257
25548
  throw new Error("invalid profile name");
@@ -26055,12 +26346,10 @@ function computeRelayBackoffMs(attempt, options = {}) {
26055
26346
  return exponential + Math.floor(exponential * ratio);
26056
26347
  }
26057
26348
  async function updateRelayReconnectState(paths, update) {
26058
- const state = await readLinkState(paths);
26059
- const next = {
26349
+ await updateJsonFile(paths.stateFile, (state) => ({
26060
26350
  ...state,
26061
- relayReconnect: update(normalizeRelayReconnectState(state.relayReconnect))
26062
- };
26063
- await writeJsonFile(paths.stateFile, next);
26351
+ relayReconnect: update(normalizeRelayReconnectState(state?.relayReconnect))
26352
+ }));
26064
26353
  }
26065
26354
  async function readLinkState(paths) {
26066
26355
  const state = await readJsonFile(paths.stateFile);
@@ -26167,6 +26456,9 @@ function readInteger4(value) {
26167
26456
  }
26168
26457
 
26169
26458
  // src/relay/control-client.ts
26459
+ var DEFAULT_RELAY_HANDSHAKE_TIMEOUT_MS = 2e4;
26460
+ var DEFAULT_RELAY_PING_INTERVAL_MS = 3 * 6e4;
26461
+ var DEFAULT_RELAY_PONG_TIMEOUT_MS = 3e4;
26170
26462
  function connectRelayControl(options) {
26171
26463
  const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
26172
26464
  wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
@@ -26175,10 +26467,15 @@ function connectRelayControl(options) {
26175
26467
  const maxReconnectAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
26176
26468
  const backoffBaseMs = options.backoffBaseMs ?? DEFAULT_RELAY_RECONNECT_BASE_MS;
26177
26469
  const backoffMaxMs = options.backoffMaxMs ?? DEFAULT_RELAY_RECONNECT_MAX_MS;
26470
+ const handshakeTimeoutMs = positiveInteger2(options.handshakeTimeoutMs, DEFAULT_RELAY_HANDSHAKE_TIMEOUT_MS);
26471
+ const pingIntervalMs = positiveInteger2(options.pingIntervalMs, DEFAULT_RELAY_PING_INTERVAL_MS);
26472
+ const pongTimeoutMs = positiveInteger2(options.pongTimeoutMs, DEFAULT_RELAY_PONG_TIMEOUT_MS);
26178
26473
  let reconnectAttempts = 0;
26179
26474
  let closedByUser = false;
26180
26475
  let socket = null;
26181
26476
  let retryTimer = null;
26477
+ let pingTimer = null;
26478
+ let pongTimer = null;
26182
26479
  let abortControllers = /* @__PURE__ */ new Map();
26183
26480
  let fatalRelayRejection = null;
26184
26481
  let relayRetryAfterMs = null;
@@ -26206,15 +26503,19 @@ function connectRelayControl(options) {
26206
26503
  });
26207
26504
  };
26208
26505
  const connect = () => {
26506
+ clearRetryTimer();
26507
+ clearHeartbeatTimers();
26209
26508
  options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
26210
26509
  fatalRelayRejection = null;
26211
26510
  relayRetryAfterMs = null;
26212
26511
  let closeHandled = false;
26512
+ let localCloseReason;
26213
26513
  const handleConnectionClosed = (reason) => {
26214
26514
  if (closeHandled) {
26215
26515
  return;
26216
26516
  }
26217
26517
  closeHandled = true;
26518
+ clearHeartbeatTimers();
26218
26519
  abortAll(abortControllers);
26219
26520
  abortControllers = /* @__PURE__ */ new Map();
26220
26521
  if (fatalRelayRejection) {
@@ -26241,30 +26542,48 @@ function connectRelayControl(options) {
26241
26542
  scheduleTimer(backoffMaxMs, "retrying", `Relay reconnect scheduling failed: ${message}`);
26242
26543
  });
26243
26544
  };
26244
- socket = new WebSocket(wsUrl, {
26545
+ const currentSocket = new WebSocket(wsUrl, {
26546
+ handshakeTimeout: handshakeTimeoutMs,
26245
26547
  headers: {
26246
26548
  "x-hermes-link-version": LINK_VERSION
26247
26549
  }
26248
26550
  });
26249
- socket.on("open", () => {
26551
+ socket = currentSocket;
26552
+ currentSocket.on("open", () => {
26553
+ if (socket !== currentSocket) {
26554
+ return;
26555
+ }
26250
26556
  reconnectAttempts = 0;
26251
26557
  void clearRelayReconnectState(paths).catch(() => void 0);
26252
26558
  options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
26253
- const currentSocket = socket;
26254
- if (currentSocket && latestNetworkRoutes) {
26559
+ startHeartbeat(currentSocket, (message) => {
26560
+ localCloseReason = message;
26561
+ options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts, message });
26562
+ currentSocket.terminate();
26563
+ });
26564
+ if (latestNetworkRoutes) {
26255
26565
  sendNetworkRoutes(currentSocket, options.linkId, latestNetworkRoutes);
26256
26566
  }
26257
26567
  });
26258
- socket.on("message", (raw) => {
26259
- if (!socket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
26568
+ currentSocket.on("pong", () => {
26569
+ if (socket !== currentSocket) {
26260
26570
  return;
26261
26571
  }
26262
- void handleFrame(socket, String(raw), options.localPort, abortControllers, streamBatchPolicy).catch((error) => {
26572
+ clearPongTimer();
26573
+ });
26574
+ currentSocket.on("message", (raw) => {
26575
+ if (socket !== currentSocket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
26576
+ return;
26577
+ }
26578
+ void handleFrame(currentSocket, String(raw), options.localPort, abortControllers, streamBatchPolicy).catch((error) => {
26263
26579
  const message = error instanceof Error ? error.message : "Relay request failed";
26264
- socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
26580
+ currentSocket.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
26265
26581
  });
26266
26582
  });
26267
- socket.on("unexpected-response", (request, response) => {
26583
+ currentSocket.on("unexpected-response", (request, response) => {
26584
+ if (socket !== currentSocket) {
26585
+ return;
26586
+ }
26268
26587
  const statusCode = response.statusCode ?? 0;
26269
26588
  fatalRelayRejection = resolveFatalRelayRejectionFromStatus(statusCode);
26270
26589
  relayRetryAfterMs = readRetryAfterMs(response);
@@ -26278,7 +26597,10 @@ function connectRelayControl(options) {
26278
26597
  handleConnectionClosed(message);
26279
26598
  request.destroy();
26280
26599
  });
26281
- socket.on("error", (error) => {
26600
+ currentSocket.on("error", (error) => {
26601
+ if (socket !== currentSocket) {
26602
+ return;
26603
+ }
26282
26604
  const message = error instanceof Error ? error.message : "Relay websocket error";
26283
26605
  fatalRelayRejection = resolveFatalRelayRejection(message);
26284
26606
  options.onStatus?.({
@@ -26287,8 +26609,12 @@ function connectRelayControl(options) {
26287
26609
  message: fatalRelayRejection ?? message
26288
26610
  });
26289
26611
  });
26290
- socket.on("close", () => {
26291
- handleConnectionClosed();
26612
+ currentSocket.on("close", (code, reason) => {
26613
+ if (socket !== currentSocket) {
26614
+ return;
26615
+ }
26616
+ socket = null;
26617
+ handleConnectionClosed(localCloseReason ?? formatCloseReason(code, reason));
26292
26618
  });
26293
26619
  };
26294
26620
  startConnect();
@@ -26318,10 +26644,59 @@ function connectRelayControl(options) {
26318
26644
  return await readRelayCooldownDelayMs(paths).catch(() => 0);
26319
26645
  }
26320
26646
  function scheduleTimer(delay4, state, message) {
26647
+ clearRetryTimer();
26321
26648
  options.onStatus?.({ state, attempt: reconnectAttempts, message });
26322
26649
  retryTimer = setTimeout(connect, delay4);
26323
26650
  retryTimer.unref?.();
26324
26651
  }
26652
+ function clearRetryTimer() {
26653
+ if (retryTimer == null) {
26654
+ return;
26655
+ }
26656
+ clearTimeout(retryTimer);
26657
+ retryTimer = null;
26658
+ }
26659
+ function startHeartbeat(currentSocket, onTimeout) {
26660
+ clearHeartbeatTimers();
26661
+ pingTimer = setInterval(() => {
26662
+ if (socket !== currentSocket || currentSocket.readyState !== WebSocket.OPEN) {
26663
+ clearHeartbeatTimers();
26664
+ return;
26665
+ }
26666
+ if (pongTimer != null) {
26667
+ return;
26668
+ }
26669
+ try {
26670
+ currentSocket.ping();
26671
+ } catch {
26672
+ onTimeout("Relay websocket ping failed");
26673
+ return;
26674
+ }
26675
+ pongTimer = setTimeout(() => {
26676
+ if (socket !== currentSocket || currentSocket.readyState !== WebSocket.OPEN) {
26677
+ clearPongTimer();
26678
+ return;
26679
+ }
26680
+ onTimeout(`Relay websocket pong timed out after ${pongTimeoutMs}ms`);
26681
+ }, pongTimeoutMs);
26682
+ pongTimer.unref?.();
26683
+ }, pingIntervalMs);
26684
+ pingTimer.unref?.();
26685
+ }
26686
+ function clearHeartbeatTimers() {
26687
+ if (pingTimer != null) {
26688
+ clearInterval(pingTimer);
26689
+ pingTimer = null;
26690
+ }
26691
+ clearPongTimer();
26692
+ }
26693
+ function clearPongTimer() {
26694
+ if (pongTimer == null) {
26695
+ return;
26696
+ }
26697
+ clearTimeout(pongTimer);
26698
+ pongTimer = null;
26699
+ }
26325
26700
  return {
26326
26701
  publishNetworkRoutes(routes) {
26327
26702
  latestNetworkRoutes = routes;
@@ -26335,10 +26710,8 @@ function connectRelayControl(options) {
26335
26710
  },
26336
26711
  close() {
26337
26712
  closedByUser = true;
26338
- if (retryTimer) {
26339
- clearTimeout(retryTimer);
26340
- retryTimer = null;
26341
- }
26713
+ clearRetryTimer();
26714
+ clearHeartbeatTimers();
26342
26715
  abortAll(abortControllers);
26343
26716
  socket?.terminate();
26344
26717
  }
@@ -26390,6 +26763,16 @@ function readRetryAfterMs(response) {
26390
26763
  }
26391
26764
  return Math.max(0, dateMs - Date.now());
26392
26765
  }
26766
+ function formatCloseReason(code, reason) {
26767
+ const text = reason.toString("utf8").trim();
26768
+ if (code === 1e3 && !text) {
26769
+ return void 0;
26770
+ }
26771
+ return text ? `Relay websocket closed (${code}): ${text}` : `Relay websocket closed (${code})`;
26772
+ }
26773
+ function positiveInteger2(value, fallback) {
26774
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : fallback;
26775
+ }
26393
26776
  async function handleFrame(socket, raw, localPort, abortControllers, streamBatchPolicy) {
26394
26777
  const frame = JSON.parse(raw);
26395
26778
  if (frame.type === "relay.config.update") {
@@ -26515,8 +26898,7 @@ async function readRelayStatusSnapshot(paths) {
26515
26898
  return normalizeRelayStatusSnapshot(state.relayStatus);
26516
26899
  }
26517
26900
  async function writeRelayStatusSnapshot(paths, status, now = /* @__PURE__ */ new Date()) {
26518
- const current = await readLinkState2(paths);
26519
- await writeJsonFile(paths.stateFile, {
26901
+ await updateLinkState(paths, (current) => ({
26520
26902
  ...current,
26521
26903
  relayStatus: {
26522
26904
  state: status.state,
@@ -26524,7 +26906,10 @@ async function writeRelayStatusSnapshot(paths, status, now = /* @__PURE__ */ new
26524
26906
  message: normalizeMessage(status.message),
26525
26907
  updatedAt: now.toISOString()
26526
26908
  }
26527
- });
26909
+ }));
26910
+ }
26911
+ async function updateLinkState(paths, update) {
26912
+ await updateJsonFile(paths.stateFile, (state) => update(state && typeof state === "object" ? state : {}));
26528
26913
  }
26529
26914
  async function readLinkState2(paths) {
26530
26915
  const state = await readJsonFile(paths.stateFile);
@@ -26943,12 +27328,10 @@ async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
26943
27328
  };
26944
27329
  }
26945
27330
  async function updateNetworkReportState(paths, update) {
26946
- const state = await readLinkState3(paths);
26947
- const next = {
27331
+ await updateJsonFile(paths.stateFile, (state) => ({
26948
27332
  ...state,
26949
- networkReport: update(normalizeNetworkReportState(state.networkReport))
26950
- };
26951
- await writeJsonFile(paths.stateFile, next);
27333
+ networkReport: update(normalizeNetworkReportState(state?.networkReport))
27334
+ }));
26952
27335
  }
26953
27336
  async function readLinkState3(paths) {
26954
27337
  const state = await readJsonFile(paths.stateFile);
@@ -27413,6 +27796,15 @@ async function startLinkService(options = {}) {
27413
27796
  });
27414
27797
  return streamBatchPolicy;
27415
27798
  };
27799
+ let profilePreparation = Promise.resolve();
27800
+ const triggerProfilePreparation = (source) => {
27801
+ profilePreparation = profilePreparation.catch(() => void 0).then(() => prepareHermesProfilesForUse(paths)).then((profiles) => logProfilePreparationResult(logger, source, profiles)).catch((error) => {
27802
+ void logger.warn("profile_preparation_failed", {
27803
+ source,
27804
+ error: error instanceof Error ? error.message : String(error)
27805
+ });
27806
+ });
27807
+ };
27416
27808
  let hermesSessionSync = Promise.resolve();
27417
27809
  const triggerHermesSessionSync = () => {
27418
27810
  hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
@@ -27432,6 +27824,7 @@ async function startLinkService(options = {}) {
27432
27824
  logger,
27433
27825
  source: "pairing_claimed"
27434
27826
  });
27827
+ triggerProfilePreparation("pairing_claimed");
27435
27828
  triggerHermesSessionSync();
27436
27829
  void loadRelayStreamBatchPolicy("pairing_claimed");
27437
27830
  await options.onPairingClaimed?.();
@@ -27492,6 +27885,9 @@ async function startLinkService(options = {}) {
27492
27885
  port: config.port,
27493
27886
  link_id: identity?.link_id ?? null
27494
27887
  });
27888
+ if (identity?.link_id) {
27889
+ triggerProfilePreparation("service_startup");
27890
+ }
27495
27891
  triggerHermesSessionSync();
27496
27892
  const scheduler = startCronDeliveryScheduler({
27497
27893
  paths,
@@ -27576,6 +27972,7 @@ async function startLinkService(options = {}) {
27576
27972
  scheduler.close(),
27577
27973
  hermesSessionSyncScheduler.close(),
27578
27974
  lanIpMonitor?.close(),
27975
+ profilePreparation.catch(() => void 0),
27579
27976
  hermesSessionSync.catch(() => void 0)
27580
27977
  ]);
27581
27978
  await logger.info("service_stopped");
@@ -27601,6 +27998,32 @@ function waitForRelayReadyTimeout(timeoutMs) {
27601
27998
  timer.unref?.();
27602
27999
  });
27603
28000
  }
28001
+ async function logProfilePreparationResult(logger, source, profiles) {
28002
+ const changed = profiles.filter((profile) => profile.changed);
28003
+ const failed = profiles.filter((profile) => profile.error);
28004
+ const summary = {
28005
+ source,
28006
+ total: profiles.length,
28007
+ prepared: profiles.length - failed.length,
28008
+ changed: changed.length,
28009
+ failed: failed.length,
28010
+ changed_profiles: changed.map((profile) => profile.profile.name)
28011
+ };
28012
+ if (changed.length > 0 || failed.length > 0) {
28013
+ await logger.warn("profiles_prepared", summary);
28014
+ } else {
28015
+ await logger.info("profiles_prepared", summary);
28016
+ }
28017
+ if (failed.length > 0) {
28018
+ await logger.warn("profile_preparation_partial_failure", {
28019
+ source,
28020
+ failed_profiles: failed.map((profile) => ({
28021
+ profile: profile.profile.name,
28022
+ error: profile.error
28023
+ }))
28024
+ });
28025
+ }
28026
+ }
27604
28027
  function pidFilePath(paths = resolveRuntimePaths()) {
27605
28028
  return `${paths.runDir}/hermeslink.pid`;
27606
28029
  }
@@ -30569,7 +30992,7 @@ async function createApp(options = {}) {
30569
30992
  conversations,
30570
30993
  syncCronDeliveries
30571
30994
  });
30572
- registerConversationRoutes(router, { paths, conversations });
30995
+ registerConversationRoutes(router, { paths, logger, conversations });
30573
30996
  registerRunRoutes(router, { paths, logger, conversations });
30574
30997
  registerProfileRoutes(router, { paths, logger, conversations });
30575
30998
  app.use(router.routes());
@@ -30585,6 +31008,7 @@ export {
30585
31008
  resolveLanguage,
30586
31009
  translate,
30587
31010
  localizeErrorMessage,
31011
+ DEFAULT_HERMES_API_SERVER_PORT,
30588
31012
  resolveHermesProfileDir,
30589
31013
  resolveHermesConfigPath,
30590
31014
  ensureHermesApiServerConfig,
@@ -30599,6 +31023,7 @@ export {
30599
31023
  HermesApiServerUnavailableError,
30600
31024
  ensureHermesApiServerAvailable,
30601
31025
  readHermesVersion,
31026
+ readHermesApiServerHealth,
30602
31027
  defaultLinkConfig,
30603
31028
  loadConfig,
30604
31029
  saveConfig,
@@ -30609,6 +31034,7 @@ export {
30609
31034
  getIdentityStatus,
30610
31035
  ConversationService,
30611
31036
  hasActiveDevices,
31037
+ prepareHermesProfilesForUse,
30612
31038
  ensureHermesLinkSkillInstalledBestEffort,
30613
31039
  detectRuntimeEnvironment,
30614
31040
  preparePairing,
package/dist/cli/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ConversationService,
4
+ DEFAULT_HERMES_API_SERVER_PORT,
4
5
  HermesApiServerUnavailableError,
5
6
  LINK_COMMAND,
6
7
  LINK_VERSION,
@@ -29,8 +30,10 @@ import {
29
30
  localizeErrorMessage,
30
31
  normalizeLanHost,
31
32
  parseLogLevel,
33
+ prepareHermesProfilesForUse,
32
34
  preparePairing,
33
35
  probeLocalLinkService,
36
+ readHermesApiServerHealth,
34
37
  readHermesVersion,
35
38
  readPairingClaim,
36
39
  readRecentGatewayLogEntries,
@@ -48,7 +51,7 @@ import {
48
51
  startLinkService,
49
52
  stopDaemonProcess,
50
53
  translate
51
- } from "../chunk-4OXUJNO6.js";
54
+ } from "../chunk-57ZJLOQA.js";
52
55
 
53
56
  // src/cli/index.ts
54
57
  import { Command } from "commander";
@@ -1158,7 +1161,7 @@ logsCommand.command("flush").description(helpText("logs.flush.description")).opt
1158
1161
  })
1159
1162
  );
1160
1163
  });
1161
- program.command("doctor").option("--install", helpText("doctor.installOnly")).description(helpText("doctor.description")).action(async (options) => {
1164
+ program.command("doctor").option("--install", helpText("doctor.installOnly")).option("--no-profiles", helpText("doctor.noProfiles")).description(helpText("doctor.description")).action(async (options) => {
1162
1165
  const installInfo = readInstallPathInfo();
1163
1166
  const installLanguage = await loadCliLanguage().catch(() => detectSystemLanguage());
1164
1167
  const installT = translate.bind(null, installLanguage);
@@ -1200,6 +1203,9 @@ program.command("doctor").option("--install", helpText("doctor.installOnly")).de
1200
1203
  } catch (error) {
1201
1204
  console.log(formatHermesApiServerUnavailable(error, language));
1202
1205
  }
1206
+ if (options.profiles !== false) {
1207
+ await printProfileDiagnostics(t);
1208
+ }
1203
1209
  });
1204
1210
  if (isCliEntrypoint()) {
1205
1211
  program.parseAsync(process.argv).catch(async (error) => {
@@ -1212,6 +1218,45 @@ async function loadCliLanguage() {
1212
1218
  const config = await loadConfig();
1213
1219
  return resolveLanguage(config.language);
1214
1220
  }
1221
+ async function printProfileDiagnostics(t) {
1222
+ console.log(t("doctor.profilesHeader"));
1223
+ const profiles = await prepareHermesProfilesForUse();
1224
+ if (profiles.length === 0) {
1225
+ console.log(t("doctor.profilesNone"));
1226
+ return;
1227
+ }
1228
+ for (const profile of profiles) {
1229
+ console.log(await formatProfileDiagnosticLine(profile, t));
1230
+ }
1231
+ }
1232
+ async function formatProfileDiagnosticLine(profile, t) {
1233
+ if (profile.error || !profile.apiServer) {
1234
+ return t("doctor.profileLine", {
1235
+ profile: profile.profile.name,
1236
+ endpoint: t("status.unknown"),
1237
+ state: t("doctor.profilePrepareFailed", {
1238
+ message: profile.error ?? t("status.unknown")
1239
+ })
1240
+ });
1241
+ }
1242
+ const host = profile.apiServer.host ?? "127.0.0.1";
1243
+ const port = profile.apiServer.port ?? DEFAULT_HERMES_API_SERVER_PORT;
1244
+ const health = await readHermesApiServerHealth(profile.apiServer).catch(
1245
+ (error) => ({
1246
+ healthy: false,
1247
+ issue: error instanceof Error ? error.message : String(error)
1248
+ })
1249
+ );
1250
+ const baseState = health.healthy ? t("doctor.profileReady") : t("doctor.profileNotRunning", {
1251
+ message: health.issue ?? t("status.unknown")
1252
+ });
1253
+ const state = profile.changed ? t("doctor.profilePreparedState", { state: baseState }) : baseState;
1254
+ return t("doctor.profileLine", {
1255
+ profile: profile.profile.name,
1256
+ endpoint: `${host}:${port}`,
1257
+ state
1258
+ });
1259
+ }
1215
1260
  function buildRelayStatusPayload(input) {
1216
1261
  if (!input.paired) {
1217
1262
  return emptyRelayStatus(input.relayConfigured, "not_paired");
@@ -381,6 +381,8 @@ declare class FileLogger {
381
381
  private rotateIfNeeded;
382
382
  }
383
383
 
384
+ type SupportedLanguage = 'zh-CN' | 'en';
385
+
384
386
  interface HermesSessionSyncResult {
385
387
  scanned_profiles: number;
386
388
  scanned_sessions: number;
@@ -510,6 +512,7 @@ declare class ConversationService {
510
512
  conversationId: string;
511
513
  approvalId: string;
512
514
  decision: LinkApprovalDecision;
515
+ language?: SupportedLanguage;
513
516
  }): Promise<{
514
517
  conversation_id: string;
515
518
  message_id: string;
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-4OXUJNO6.js";
3
+ } from "../chunk-57ZJLOQA.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.6",
3
+ "version": "0.6.8",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",