@hermespilot/link 0.6.5 → 0.6.7

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.
@@ -1465,6 +1465,9 @@ var messages = {
1465
1465
  "config.notice.autoFilled": "Hermes API Server was auto-filled with {fields}; existing port/host/key were not overwritten.",
1466
1466
  "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
1467
  "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.",
1468
+ "config.yaml.invalid": "Hermes Profile config is not valid YAML, so Link cannot write settings yet. Fix {path}, then try again.{details}{more}",
1469
+ "config.yaml.details": " YAML error: {errors}",
1470
+ "config.yaml.more": " and {count} more error(s).",
1468
1471
  "daemon.description": "Run Hermes Link in the foreground",
1469
1472
  "daemon.foreground": "Hermes Link foreground daemon is running. Press Ctrl+C to stop.",
1470
1473
  "logs.description": "Show or follow Hermes Link logs",
@@ -1520,6 +1523,7 @@ var messages = {
1520
1523
  "pair.autostartFailed": "Pairing succeeded, but boot autostart could not be enabled: {message}",
1521
1524
  "doctor.description": "Run local diagnostics",
1522
1525
  "doctor.installOnly": "only check npm global command and PATH setup",
1526
+ "doctor.noProfiles": "skip Hermes Profile diagnostics",
1523
1527
  "doctor.installHeader": "Install/PATH diagnostics:",
1524
1528
  "doctor.installNpmPrefix": "npm global prefix: {value}",
1525
1529
  "doctor.installGlobalBin": "npm global bin directory: {value}",
@@ -1549,6 +1553,13 @@ var messages = {
1549
1553
  "doctor.apiReady": "Hermes API Server: ready",
1550
1554
  "doctor.apiStarted": "Hermes API Server: started and ready",
1551
1555
  "doctor.apiUnavailable": "Hermes API Server: unavailable. {message}",
1556
+ "doctor.profilesHeader": "Hermes Profiles:",
1557
+ "doctor.profilesNone": "- no profiles found",
1558
+ "doctor.profileLine": "- {profile}: {endpoint}; {state}",
1559
+ "doctor.profileReady": "ready",
1560
+ "doctor.profileNotRunning": "not running ({message})",
1561
+ "doctor.profilePreparedState": "prepared config; {state}",
1562
+ "doctor.profilePrepareFailed": "prepare failed ({message})",
1552
1563
  "doctor.apiUnavailable.summary": "Hermes API Server: unavailable",
1553
1564
  "doctor.apiUnavailable.profile": "Profile: {profile}; port: {port}",
1554
1565
  "doctor.apiUnavailable.diagnosisDivider": "----- API Server diagnosis -----",
@@ -1668,6 +1679,9 @@ var messages = {
1668
1679
  "config.notice.autoFilled": "\u5DF2\u4E3A Hermes API Server \u81EA\u52A8\u8865\u5145 {fields}\uFF1B\u672A\u8986\u76D6\u5DF2\u6709 port/host/key\u3002",
1669
1680
  "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
1681
  "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",
1682
+ "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}",
1683
+ "config.yaml.details": " YAML \u9519\u8BEF\uFF1A{errors}",
1684
+ "config.yaml.more": " \u7B49 {count} \u5904\u9519\u8BEF\u3002",
1671
1685
  "daemon.description": "\u4EE5\u524D\u53F0\u65B9\u5F0F\u8FD0\u884C Hermes Link",
1672
1686
  "daemon.foreground": "Hermes Link \u524D\u53F0\u670D\u52A1\u6B63\u5728\u8FD0\u884C\u3002\u6309 Ctrl+C \u505C\u6B62\u3002",
1673
1687
  "logs.description": "\u663E\u793A\u6216\u8DDF\u8E2A Hermes Link \u65E5\u5FD7",
@@ -1723,6 +1737,7 @@ var messages = {
1723
1737
  "pair.autostartFailed": "\u914D\u5BF9\u5DF2\u6210\u529F\uFF0C\u4F46\u542F\u7528\u5F00\u673A\u81EA\u542F\u5931\u8D25\uFF1A{message}",
1724
1738
  "doctor.description": "\u8FD0\u884C\u672C\u673A\u8BCA\u65AD",
1725
1739
  "doctor.installOnly": "\u53EA\u68C0\u67E5 npm \u5168\u5C40\u547D\u4EE4\u548C PATH \u8BBE\u7F6E",
1740
+ "doctor.noProfiles": "\u8DF3\u8FC7 Hermes Profile \u8BCA\u65AD",
1726
1741
  "doctor.installHeader": "\u5B89\u88C5 / PATH \u8BCA\u65AD\uFF1A",
1727
1742
  "doctor.installNpmPrefix": "npm \u5168\u5C40 prefix\uFF1A{value}",
1728
1743
  "doctor.installGlobalBin": "npm \u5168\u5C40 bin \u76EE\u5F55\uFF1A{value}",
@@ -1752,6 +1767,13 @@ var messages = {
1752
1767
  "doctor.apiReady": "Hermes API Server\uFF1A\u5DF2\u5C31\u7EEA",
1753
1768
  "doctor.apiStarted": "Hermes API Server\uFF1A\u5DF2\u81EA\u52A8\u542F\u52A8\u5E76\u5C31\u7EEA",
1754
1769
  "doctor.apiUnavailable": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528\u3002{message}",
1770
+ "doctor.profilesHeader": "Hermes Profiles\uFF1A",
1771
+ "doctor.profilesNone": "- \u6CA1\u6709\u627E\u5230 Profile",
1772
+ "doctor.profileLine": "- {profile}\uFF1A{endpoint}\uFF1B{state}",
1773
+ "doctor.profileReady": "\u5DF2\u5C31\u7EEA",
1774
+ "doctor.profileNotRunning": "\u5C1A\u672A\u8FD0\u884C\uFF08{message}\uFF09",
1775
+ "doctor.profilePreparedState": "\u5DF2\u81EA\u52A8\u51C6\u5907\u914D\u7F6E\uFF1B{state}",
1776
+ "doctor.profilePrepareFailed": "\u51C6\u5907\u5931\u8D25\uFF08{message}\uFF09",
1755
1777
  "doctor.apiUnavailable.summary": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528",
1756
1778
  "doctor.apiUnavailable.profile": "Profile\uFF1A{profile}\uFF1B\u7AEF\u53E3\uFF1A{port}",
1757
1779
  "doctor.apiUnavailable.diagnosisDivider": "----- API Server \u8BCA\u65AD -----",
@@ -1894,6 +1916,17 @@ var MODEL_CONFIG_RESTART_HINT = "\u6A21\u578B\u914D\u7F6E\u5DF2\u4FDD\u5B58\u300
1894
1916
  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
1917
  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
1918
  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";
1919
+ var HermesConfigYamlError = class extends Error {
1920
+ constructor(configPath, errors, language) {
1921
+ super(buildHermesConfigYamlErrorMessage(configPath, errors, language));
1922
+ this.configPath = configPath;
1923
+ this.errors = errors;
1924
+ this.name = "HermesConfigYamlError";
1925
+ }
1926
+ configPath;
1927
+ errors;
1928
+ code = "hermes_config_yaml_invalid";
1929
+ };
1897
1930
  var REASONING_EFFORTS = [
1898
1931
  "none",
1899
1932
  "minimal",
@@ -2805,12 +2838,12 @@ async function saveHermesProfilePermissions(profileName, input, configPath = res
2805
2838
  restartHint: PROFILE_PERMISSIONS_RESTART_HINT
2806
2839
  };
2807
2840
  }
2808
- async function addHermesCommandAllowlistEntry(profileName, entry, configPath = resolveHermesConfigPath(profileName)) {
2841
+ async function addHermesCommandAllowlistEntry(profileName, entry, configPath = resolveHermesConfigPath(profileName), language) {
2809
2842
  const normalizedEntry = entry.trim();
2810
2843
  if (!normalizedEntry) {
2811
2844
  throw new Error("command_allowlist entry must be non-empty");
2812
2845
  }
2813
- const { document, config, existingRaw } = await readHermesConfigDocument(configPath);
2846
+ const { document, config, existingRaw } = await readHermesConfigDocument(configPath, language);
2814
2847
  const current = readStringList(config.command_allowlist);
2815
2848
  if (current.includes(normalizedEntry)) {
2816
2849
  return {
@@ -3132,7 +3165,7 @@ async function repairHermesApiServerConfigUnlocked(profileName = "default", conf
3132
3165
  notice: buildRepairNotice(language, profileName)
3133
3166
  };
3134
3167
  }
3135
- async function readHermesConfigDocument(configPath) {
3168
+ async function readHermesConfigDocument(configPath, language) {
3136
3169
  const existingRaw = await readFile2(configPath, "utf8").catch(
3137
3170
  (error) => {
3138
3171
  if (isNodeError3(error, "ENOENT")) {
@@ -3142,12 +3175,34 @@ async function readHermesConfigDocument(configPath) {
3142
3175
  }
3143
3176
  );
3144
3177
  const document = existingRaw ? YAML.parseDocument(existingRaw) : new YAML.Document({});
3178
+ assertValidHermesConfigDocument(configPath, document, language);
3145
3179
  return {
3146
3180
  document,
3147
3181
  config: toRecord(document.toJSON()),
3148
3182
  existingRaw
3149
3183
  };
3150
3184
  }
3185
+ function assertValidHermesConfigDocument(configPath, document, language) {
3186
+ const errors = document.errors.map((error) => error.message.trim()).filter((message) => message.length > 0);
3187
+ if (errors.length > 0) {
3188
+ throw new HermesConfigYamlError(configPath, errors, language);
3189
+ }
3190
+ }
3191
+ function buildHermesConfigYamlErrorMessage(configPath, errors, language) {
3192
+ const firstErrors = errors.slice(0, 3);
3193
+ const resolvedLanguage = language ?? "zh-CN";
3194
+ const details = firstErrors.length > 0 ? translate(resolvedLanguage, "config.yaml.details", {
3195
+ errors: firstErrors.join("; ")
3196
+ }) : "";
3197
+ const more = errors.length > firstErrors.length ? translate(resolvedLanguage, "config.yaml.more", {
3198
+ count: errors.length - firstErrors.length
3199
+ }) : "";
3200
+ return translate(resolvedLanguage, "config.yaml.invalid", {
3201
+ path: configPath,
3202
+ details,
3203
+ more
3204
+ });
3205
+ }
3151
3206
  async function writeHermesConfigDocument(input) {
3152
3207
  const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
3153
3208
  if (backupPath) {
@@ -4894,12 +4949,24 @@ async function addConfiguredApiServerPort(ports, profileName, excludedProfileNam
4894
4949
  throw error;
4895
4950
  });
4896
4951
  if (!raw.trim()) {
4952
+ const envPort2 = readApiServerPort(
4953
+ (await readHermesApiServerEnvOverrides(profileName)).port
4954
+ );
4955
+ if (envPort2 !== null) {
4956
+ ports.add(envPort2);
4957
+ }
4897
4958
  return;
4898
4959
  }
4899
4960
  const config = toRecord(YAML.parse(raw));
4900
4961
  const apiServer = toRecord(toRecord(config.platforms).api_server);
4901
- const port = readApiServerConfig(apiServer).port;
4902
- if (typeof port === "number" && Number.isFinite(port)) {
4962
+ const envPort = readApiServerPort(
4963
+ (await readHermesApiServerEnvOverrides(profileName)).port
4964
+ );
4965
+ const configPort = readApiServerPort(readApiServerConfig(apiServer).port);
4966
+ for (const port of [envPort, configPort]) {
4967
+ if (port === null) {
4968
+ continue;
4969
+ }
4903
4970
  ports.add(port);
4904
4971
  }
4905
4972
  }
@@ -5418,7 +5485,7 @@ import os2 from "os";
5418
5485
  import path5 from "path";
5419
5486
 
5420
5487
  // src/constants.ts
5421
- var LINK_VERSION = "0.6.5";
5488
+ var LINK_VERSION = "0.6.7";
5422
5489
  var LINK_COMMAND = "hermeslink";
5423
5490
  var LINK_DEFAULT_PORT = 52379;
5424
5491
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -8273,6 +8340,37 @@ function hasRunningRuns(snapshot) {
8273
8340
  function hasQueuedRuns(snapshot) {
8274
8341
  return snapshot.runs.some((run) => run.status === "queued");
8275
8342
  }
8343
+ function buildConversationEventStreamState(snapshot) {
8344
+ const pendingApprovalRunIds = /* @__PURE__ */ new Set();
8345
+ let hasPendingApproval = false;
8346
+ for (const message of snapshot.messages) {
8347
+ if (!message.approvals?.some((approval) => approval.status === "pending")) {
8348
+ continue;
8349
+ }
8350
+ hasPendingApproval = true;
8351
+ if (message.run_id) {
8352
+ pendingApprovalRunIds.add(message.run_id);
8353
+ }
8354
+ }
8355
+ const summaries = snapshot.runs.map(
8356
+ (run) => toEventStreamRunSummary(run, pendingApprovalRunIds.has(run.id))
8357
+ );
8358
+ const activeRuns = summaries.filter((run) => isRealtimeRunStatus(run.status));
8359
+ const unknownRuns = summaries.filter((run) => run.status === "unknown");
8360
+ const requiresUserAction = hasPendingApproval || summaries.some((run) => run.requires_user_action);
8361
+ const hasQueuedRun = activeRuns.some((run) => run.status === "queued");
8362
+ const hasRunningRun = activeRuns.some((run) => run.status === "running");
8363
+ const hasUnknownRun = unknownRuns.length > 0;
8364
+ const reason = requiresUserAction ? "requires_user_action" : hasQueuedRun ? "queued_run" : hasRunningRun ? "active_run" : hasUnknownRun ? "unknown" : "terminal";
8365
+ return {
8366
+ should_subscribe: !requiresUserAction && activeRuns.length > 0,
8367
+ reason,
8368
+ has_active_runs: activeRuns.length > 0 || hasUnknownRun,
8369
+ requires_user_action: requiresUserAction,
8370
+ active_runs: activeRuns,
8371
+ latest_runs: summaries.slice().sort((left, right) => right.updated_at.localeCompare(left.updated_at)).slice(0, 5)
8372
+ };
8373
+ }
8276
8374
  function isRetryableUserMessage(message) {
8277
8375
  return message.role === "user" && message.raw?.format !== "hermes-link-slash-command" && (messageText(message).length > 0 || message.parts.some((part) => Boolean(part.blob)));
8278
8376
  }
@@ -8325,6 +8423,18 @@ function previewText(message) {
8325
8423
  function messageText(message) {
8326
8424
  return message.parts.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("").trim();
8327
8425
  }
8426
+ function toEventStreamRunSummary(run, requiresUserAction) {
8427
+ return {
8428
+ id: run.id,
8429
+ status: run.status,
8430
+ assistant_message_id: run.assistant_message_id,
8431
+ requires_user_action: requiresUserAction,
8432
+ updated_at: run.completed_at ?? run.started_at
8433
+ };
8434
+ }
8435
+ function isRealtimeRunStatus(status) {
8436
+ return status === "queued" || status === "running";
8437
+ }
8328
8438
 
8329
8439
  // src/conversations/slash-commands.ts
8330
8440
  import { randomUUID as randomUUID4 } from "crypto";
@@ -11538,7 +11648,8 @@ var ConversationQueryCoordinator = class {
11538
11648
  has_more_after: endIndex < total,
11539
11649
  oldest_message_id: messages2[0]?.id ?? null,
11540
11650
  newest_message_id: messages2.at(-1)?.id ?? null
11541
- }
11651
+ },
11652
+ event_stream: buildConversationEventStreamState(snapshot)
11542
11653
  };
11543
11654
  }
11544
11655
  async listEvents(conversationId, after = 0) {
@@ -18772,8 +18883,15 @@ var ConversationService = class {
18772
18883
  );
18773
18884
  const result = await addHermesCommandAllowlistEntry(
18774
18885
  profileName,
18775
- patternKey
18776
- );
18886
+ patternKey,
18887
+ void 0,
18888
+ input.language
18889
+ ).catch((error) => {
18890
+ if (error instanceof HermesConfigYamlError) {
18891
+ throw new LinkHttpError(409, error.code, error.message);
18892
+ }
18893
+ throw error;
18894
+ });
18777
18895
  commandAllowlistUpdated = result.changed;
18778
18896
  configPath = result.configPath;
18779
18897
  requiresGatewayReload = result.requiresGatewayReload;
@@ -19485,6 +19603,36 @@ function readString16(body, key) {
19485
19603
  const value = body[key];
19486
19604
  return typeof value === "string" && value.trim() ? value.trim() : null;
19487
19605
  }
19606
+ function readPreferredLanguage(ctx) {
19607
+ const candidates = [
19608
+ ctx.get("x-hermespilot-language"),
19609
+ ctx.get("x-app-language"),
19610
+ ctx.get("accept-language")
19611
+ ];
19612
+ for (const candidate of candidates) {
19613
+ for (const part of candidate.split(",")) {
19614
+ const normalized = part.split(";")[0]?.trim();
19615
+ if (!normalized) {
19616
+ continue;
19617
+ }
19618
+ const language = readSupportedLanguage(normalized);
19619
+ if (language) {
19620
+ return language;
19621
+ }
19622
+ }
19623
+ }
19624
+ return resolveLanguage();
19625
+ }
19626
+ function readSupportedLanguage(value) {
19627
+ const normalized = value.trim().replace("_", "-").toLowerCase();
19628
+ if (normalized.startsWith("zh")) {
19629
+ return "zh-CN";
19630
+ }
19631
+ if (normalized.startsWith("en")) {
19632
+ return "en";
19633
+ }
19634
+ return null;
19635
+ }
19488
19636
  function readOptionalProfileName(body) {
19489
19637
  return readString16(body, "profile") ?? readString16(body, "profile_name") ?? readString16(body, "profileName") ?? void 0;
19490
19638
  }
@@ -20165,7 +20313,8 @@ function registerConversationRoutes(router, options) {
20165
20313
  ...await conversations.resolveApproval({
20166
20314
  conversationId: ctx.params.conversationId,
20167
20315
  approvalId: ctx.params.approvalId,
20168
- decision: scope
20316
+ decision: scope,
20317
+ language: readPreferredLanguage(ctx)
20169
20318
  })
20170
20319
  };
20171
20320
  }
@@ -20179,7 +20328,8 @@ function registerConversationRoutes(router, options) {
20179
20328
  ...await conversations.resolveApproval({
20180
20329
  conversationId: ctx.params.conversationId,
20181
20330
  approvalId: ctx.params.approvalId,
20182
- decision: "deny"
20331
+ decision: "deny",
20332
+ language: readPreferredLanguage(ctx)
20183
20333
  })
20184
20334
  };
20185
20335
  }
@@ -20404,7 +20554,9 @@ var PROFILE_DELETE_VERIFY_INTERVAL_MS = 150;
20404
20554
  var execFileAsync4 = promisify4(execFile4);
20405
20555
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
20406
20556
  const profiles = /* @__PURE__ */ new Map();
20407
- profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
20557
+ if (await hasDefaultProfileConfigSource()) {
20558
+ profiles.set(DEFAULT_PROFILE, await profileInfo(DEFAULT_PROFILE, paths));
20559
+ }
20408
20560
  const profilesDir = resolveHermesProfilesDir();
20409
20561
  const entries = await readdir9(profilesDir, { withFileTypes: true }).catch(
20410
20562
  (error) => {
@@ -20429,6 +20581,36 @@ async function listHermesProfiles(paths = resolveRuntimePaths()) {
20429
20581
  return left.name.localeCompare(right.name);
20430
20582
  });
20431
20583
  }
20584
+ async function prepareHermesProfilesForUse(paths = resolveRuntimePaths()) {
20585
+ const profiles = await listHermesProfiles(paths);
20586
+ const prepared = [];
20587
+ for (const profile of profiles) {
20588
+ try {
20589
+ const result = await ensureHermesApiServerKey(
20590
+ profile.name,
20591
+ profile.configPath
20592
+ );
20593
+ prepared.push({
20594
+ profile,
20595
+ apiServer: result.apiServer,
20596
+ changed: result.changed,
20597
+ backupPath: result.backupPath,
20598
+ notice: result.notice,
20599
+ error: null
20600
+ });
20601
+ } catch (error) {
20602
+ prepared.push({
20603
+ profile,
20604
+ apiServer: null,
20605
+ changed: false,
20606
+ backupPath: null,
20607
+ notice: null,
20608
+ error: errorMessage(error)
20609
+ });
20610
+ }
20611
+ }
20612
+ return prepared;
20613
+ }
20432
20614
  async function getHermesProfileStatus(name, paths = resolveRuntimePaths()) {
20433
20615
  assertProfileName(name);
20434
20616
  const profile = await profileInfo(name, paths);
@@ -20552,6 +20734,19 @@ async function pathExists(targetPath) {
20552
20734
  throw error;
20553
20735
  });
20554
20736
  }
20737
+ async function hasDefaultProfileConfigSource() {
20738
+ const profilePath = resolveHermesProfileDir(DEFAULT_PROFILE);
20739
+ const profileStat = await stat13(profilePath).catch((error) => {
20740
+ if (isNodeError15(error, "ENOENT")) {
20741
+ return null;
20742
+ }
20743
+ throw error;
20744
+ });
20745
+ if (!profileStat?.isDirectory()) {
20746
+ return false;
20747
+ }
20748
+ return await pathExists(resolveHermesConfigPath(DEFAULT_PROFILE)) || await pathExists(path20.join(profilePath, ".env"));
20749
+ }
20555
20750
  async function deleteHermesProfileWithCli(name) {
20556
20751
  try {
20557
20752
  await execFileAsync4(resolveHermesBin(), ["profile", "delete", name, "--yes"], {
@@ -20703,6 +20898,9 @@ function readExecErrorOutput2(error) {
20703
20898
  function isNodeError15(error, code) {
20704
20899
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
20705
20900
  }
20901
+ function errorMessage(error) {
20902
+ return error instanceof Error ? error.message : String(error);
20903
+ }
20706
20904
  function escapeRegExp2(value) {
20707
20905
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20708
20906
  }
@@ -21429,7 +21627,11 @@ function registerProfileCatalogRoutes(router, options) {
21429
21627
  });
21430
21628
  }
21431
21629
  async function readProfileCatalogItem(profile) {
21432
- const [capabilities, permissions, modelConfigs] = await Promise.all([
21630
+ const [apiServer, capabilities, permissions, modelConfigs] = await Promise.all([
21631
+ readCatalogField(
21632
+ "apiServer",
21633
+ () => readHermesApiServerConfig(profile.name, profile.configPath)
21634
+ ),
21433
21635
  readCatalogField(
21434
21636
  "capabilities",
21435
21637
  () => readHermesProfileCapabilities(profile.name)
@@ -21442,10 +21644,17 @@ async function readProfileCatalogItem(profile) {
21442
21644
  ]);
21443
21645
  return {
21444
21646
  profile,
21647
+ apiServer: apiServer.value ? {
21648
+ host: apiServer.value.host ?? null,
21649
+ port: apiServer.value.port ?? null,
21650
+ configured: Boolean(apiServer.value.key),
21651
+ changed: false
21652
+ } : null,
21445
21653
  capabilities: capabilities.value,
21446
21654
  permissions: permissions.value,
21447
21655
  modelConfigs: modelConfigs.value,
21448
21656
  errors: [
21657
+ ...apiServer.errors,
21449
21658
  ...capabilities.errors,
21450
21659
  ...permissions.errors,
21451
21660
  ...modelConfigs.errors
@@ -21458,11 +21667,11 @@ async function readCatalogField(field, load) {
21458
21667
  } catch (error) {
21459
21668
  return {
21460
21669
  value: null,
21461
- errors: [{ field, message: errorMessage(error) }]
21670
+ errors: [{ field, message: errorMessage2(error) }]
21462
21671
  };
21463
21672
  }
21464
21673
  }
21465
- function errorMessage(error) {
21674
+ function errorMessage2(error) {
21466
21675
  return error instanceof Error ? error.message : String(error);
21467
21676
  }
21468
21677
 
@@ -21575,7 +21784,7 @@ async function ensureHermesLinkSkillInstalledForProfiles(options = {}) {
21575
21784
  const skillChanged = await writeHermesLinkSkill(skillPath);
21576
21785
  const profiles = await listHermesProfiles(paths);
21577
21786
  const results = [];
21578
- for (const profile of profiles) {
21787
+ for (const profile of withDefaultProfilePlaceholder(profiles)) {
21579
21788
  try {
21580
21789
  results.push(await ensureProfileUsesExternalSkillDir(profile, externalDir));
21581
21790
  } catch (error) {
@@ -21629,6 +21838,25 @@ async function ensureHermesLinkSkillInstalledBestEffort(options = {}) {
21629
21838
  });
21630
21839
  }
21631
21840
  }
21841
+ function withDefaultProfilePlaceholder(profiles) {
21842
+ if (profiles.some((profile) => profile.name === "default")) {
21843
+ return profiles;
21844
+ }
21845
+ return [
21846
+ {
21847
+ uid: "default",
21848
+ name: "default",
21849
+ active: false,
21850
+ path: resolveHermesProfileDir("default"),
21851
+ configPath: resolveHermesConfigPath("default"),
21852
+ displayName: null,
21853
+ description: null,
21854
+ avatarType: "default",
21855
+ avatarUrl: null
21856
+ },
21857
+ ...profiles
21858
+ ];
21859
+ }
21632
21860
  function resolveHermesLinkSkillExternalDir(paths = resolveRuntimePaths()) {
21633
21861
  return path21.join(paths.homeDir, HERMES_LINK_SKILL_ROOT_DIR);
21634
21862
  }
@@ -25109,6 +25337,16 @@ function registerProfileRoutes(router, options) {
25109
25337
  profiles: await listHermesProfiles(paths)
25110
25338
  };
25111
25339
  });
25340
+ router.post("/api/v1/profiles/prepare", async (ctx) => {
25341
+ await authenticateRequest(ctx, paths);
25342
+ ctx.set("cache-control", "no-store");
25343
+ ctx.body = {
25344
+ ok: true,
25345
+ profiles: (await prepareHermesProfilesForUse(paths)).map(
25346
+ formatProfilePreparation
25347
+ )
25348
+ };
25349
+ });
25112
25350
  router.get("/api/v1/profile-creation/status", async (ctx) => {
25113
25351
  await authenticateRequest(ctx, paths);
25114
25352
  ctx.set("cache-control", "no-store");
@@ -25208,6 +25446,20 @@ function registerProfileRoutes(router, options) {
25208
25446
  ctx.status = 204;
25209
25447
  });
25210
25448
  }
25449
+ function formatProfilePreparation(item) {
25450
+ return {
25451
+ profile: item.profile,
25452
+ apiServer: item.apiServer ? {
25453
+ host: item.apiServer.host ?? null,
25454
+ port: item.apiServer.port ?? null,
25455
+ configured: Boolean(item.apiServer.key),
25456
+ changed: item.changed
25457
+ } : null,
25458
+ changed: item.changed,
25459
+ notice: item.notice,
25460
+ error: item.error
25461
+ };
25462
+ }
25211
25463
  function readProfileName(body) {
25212
25464
  if (typeof body.name !== "string") {
25213
25465
  throw new Error("invalid profile name");
@@ -27369,6 +27621,15 @@ async function startLinkService(options = {}) {
27369
27621
  });
27370
27622
  return streamBatchPolicy;
27371
27623
  };
27624
+ let profilePreparation = Promise.resolve();
27625
+ const triggerProfilePreparation = (source) => {
27626
+ profilePreparation = profilePreparation.catch(() => void 0).then(() => prepareHermesProfilesForUse(paths)).then((profiles) => logProfilePreparationResult(logger, source, profiles)).catch((error) => {
27627
+ void logger.warn("profile_preparation_failed", {
27628
+ source,
27629
+ error: error instanceof Error ? error.message : String(error)
27630
+ });
27631
+ });
27632
+ };
27372
27633
  let hermesSessionSync = Promise.resolve();
27373
27634
  const triggerHermesSessionSync = () => {
27374
27635
  hermesSessionSync = Promise.resolve().then(() => conversations.syncHermesSessions()).then(() => void 0).catch((error) => {
@@ -27388,6 +27649,7 @@ async function startLinkService(options = {}) {
27388
27649
  logger,
27389
27650
  source: "pairing_claimed"
27390
27651
  });
27652
+ triggerProfilePreparation("pairing_claimed");
27391
27653
  triggerHermesSessionSync();
27392
27654
  void loadRelayStreamBatchPolicy("pairing_claimed");
27393
27655
  await options.onPairingClaimed?.();
@@ -27448,6 +27710,9 @@ async function startLinkService(options = {}) {
27448
27710
  port: config.port,
27449
27711
  link_id: identity?.link_id ?? null
27450
27712
  });
27713
+ if (identity?.link_id) {
27714
+ triggerProfilePreparation("service_startup");
27715
+ }
27451
27716
  triggerHermesSessionSync();
27452
27717
  const scheduler = startCronDeliveryScheduler({
27453
27718
  paths,
@@ -27532,6 +27797,7 @@ async function startLinkService(options = {}) {
27532
27797
  scheduler.close(),
27533
27798
  hermesSessionSyncScheduler.close(),
27534
27799
  lanIpMonitor?.close(),
27800
+ profilePreparation.catch(() => void 0),
27535
27801
  hermesSessionSync.catch(() => void 0)
27536
27802
  ]);
27537
27803
  await logger.info("service_stopped");
@@ -27557,6 +27823,32 @@ function waitForRelayReadyTimeout(timeoutMs) {
27557
27823
  timer.unref?.();
27558
27824
  });
27559
27825
  }
27826
+ async function logProfilePreparationResult(logger, source, profiles) {
27827
+ const changed = profiles.filter((profile) => profile.changed);
27828
+ const failed = profiles.filter((profile) => profile.error);
27829
+ const summary = {
27830
+ source,
27831
+ total: profiles.length,
27832
+ prepared: profiles.length - failed.length,
27833
+ changed: changed.length,
27834
+ failed: failed.length,
27835
+ changed_profiles: changed.map((profile) => profile.profile.name)
27836
+ };
27837
+ if (changed.length > 0 || failed.length > 0) {
27838
+ await logger.warn("profiles_prepared", summary);
27839
+ } else {
27840
+ await logger.info("profiles_prepared", summary);
27841
+ }
27842
+ if (failed.length > 0) {
27843
+ await logger.warn("profile_preparation_partial_failure", {
27844
+ source,
27845
+ failed_profiles: failed.map((profile) => ({
27846
+ profile: profile.profile.name,
27847
+ error: profile.error
27848
+ }))
27849
+ });
27850
+ }
27851
+ }
27560
27852
  function pidFilePath(paths = resolveRuntimePaths()) {
27561
27853
  return `${paths.runDir}/hermeslink.pid`;
27562
27854
  }
@@ -30541,6 +30833,7 @@ export {
30541
30833
  resolveLanguage,
30542
30834
  translate,
30543
30835
  localizeErrorMessage,
30836
+ DEFAULT_HERMES_API_SERVER_PORT,
30544
30837
  resolveHermesProfileDir,
30545
30838
  resolveHermesConfigPath,
30546
30839
  ensureHermesApiServerConfig,
@@ -30555,6 +30848,7 @@ export {
30555
30848
  HermesApiServerUnavailableError,
30556
30849
  ensureHermesApiServerAvailable,
30557
30850
  readHermesVersion,
30851
+ readHermesApiServerHealth,
30558
30852
  defaultLinkConfig,
30559
30853
  loadConfig,
30560
30854
  saveConfig,
@@ -30565,6 +30859,7 @@ export {
30565
30859
  getIdentityStatus,
30566
30860
  ConversationService,
30567
30861
  hasActiveDevices,
30862
+ prepareHermesProfilesForUse,
30568
30863
  ensureHermesLinkSkillInstalledBestEffort,
30569
30864
  detectRuntimeEnvironment,
30570
30865
  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-IJOWAEQ4.js";
54
+ } from "../chunk-FWX7ZUP4.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");
@@ -62,6 +62,22 @@ interface ConversationMessagesPageInfo {
62
62
  oldest_message_id: string | null;
63
63
  newest_message_id: string | null;
64
64
  }
65
+ type ConversationEventStreamReason = 'terminal' | 'active_run' | 'queued_run' | 'recovering' | 'requires_user_action' | 'unknown';
66
+ interface ConversationEventStreamRunSummary {
67
+ id: string;
68
+ status: LinkRun['status'];
69
+ assistant_message_id: string;
70
+ requires_user_action: boolean;
71
+ updated_at: string;
72
+ }
73
+ interface ConversationEventStreamState {
74
+ should_subscribe: boolean;
75
+ reason: ConversationEventStreamReason;
76
+ has_active_runs: boolean;
77
+ requires_user_action: boolean;
78
+ active_runs: ConversationEventStreamRunSummary[];
79
+ latest_runs: ConversationEventStreamRunSummary[];
80
+ }
65
81
  interface LinkMessagePart {
66
82
  type: 'text' | 'image' | 'file' | 'audio' | 'video' | 'unsupported';
67
83
  text?: string;
@@ -365,6 +381,8 @@ declare class FileLogger {
365
381
  private rotateIfNeeded;
366
382
  }
367
383
 
384
+ type SupportedLanguage = 'zh-CN' | 'en';
385
+
368
386
  interface HermesSessionSyncResult {
369
387
  scanned_profiles: number;
370
388
  scanned_sessions: number;
@@ -455,6 +473,7 @@ declare class ConversationService {
455
473
  last_event_seq: number;
456
474
  runtime: ConversationRuntimeMetadata;
457
475
  page: ConversationMessagesPageInfo;
476
+ event_stream: ConversationEventStreamState;
458
477
  }>;
459
478
  setConversationModel(conversationId: string, modelId: string): Promise<{
460
479
  conversation_id: string;
@@ -493,6 +512,7 @@ declare class ConversationService {
493
512
  conversationId: string;
494
513
  approvalId: string;
495
514
  decision: LinkApprovalDecision;
515
+ language?: SupportedLanguage;
496
516
  }): Promise<{
497
517
  conversation_id: string;
498
518
  message_id: string;
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-IJOWAEQ4.js";
3
+ } from "../chunk-FWX7ZUP4.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.5",
3
+ "version": "0.6.7",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",