@hermespilot/link 0.3.8 → 0.3.9

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.
@@ -3970,7 +3970,7 @@ import os2 from "os";
3970
3970
  import path5 from "path";
3971
3971
 
3972
3972
  // src/constants.ts
3973
- var LINK_VERSION = "0.3.8";
3973
+ var LINK_VERSION = "0.3.9";
3974
3974
  var LINK_COMMAND = "hermeslink";
3975
3975
  var LINK_DEFAULT_PORT = 52379;
3976
3976
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4455,6 +4455,9 @@ var DEFAULT_START_TIMEOUT_MS = 12e3;
4455
4455
  var HEALTH_TIMEOUT_MS = 1500;
4456
4456
  var MIN_API_SERVER_VERSION = "0.4.0";
4457
4457
  var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/u;
4458
+ var DASHBOARD_STATUS_URL = "http://127.0.0.1:9119/api/status";
4459
+ var DASHBOARD_STATUS_TIMEOUT_MS = 1500;
4460
+ var MAX_VERSION_LOG_OUTPUT_LENGTH = 1200;
4458
4461
  var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
4459
4462
  async function ensureHermesApiServerAvailable(options = {}) {
4460
4463
  const profileName = normalizeProfileName(options.profileName);
@@ -4571,28 +4574,56 @@ async function reloadHermesGateway(options = {}) {
4571
4574
  }
4572
4575
  return ensureHermesApiServerAvailable({ ...options, forceRestart: true });
4573
4576
  }
4574
- async function readHermesVersion() {
4575
- const { stdout } = await execHermesVersion();
4576
- const raw = stdout.trim();
4577
- const version = parseHermesVersion(raw);
4578
- return {
4579
- raw,
4580
- version,
4581
- supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
4582
- };
4583
- }
4584
- async function execHermesVersion() {
4577
+ async function readHermesVersion(options = {}) {
4585
4578
  try {
4586
- return await execFileAsync2(resolveHermesBin(), ["version"], {
4587
- timeout: 5e3,
4588
- windowsHide: true
4589
- });
4590
- } catch {
4591
- return await execFileAsync2(resolveHermesBin(), ["--version"], {
4592
- timeout: 5e3,
4593
- windowsHide: true
4579
+ const { stdout } = await execHermesVersion(options.logger);
4580
+ const raw = stdout.trim();
4581
+ const version = parseHermesVersion(raw);
4582
+ return buildHermesVersionInfo(raw, version);
4583
+ } catch (cliError) {
4584
+ const dashboardStatusUrl = options.dashboardStatusUrl ?? DASHBOARD_STATUS_URL;
4585
+ void options.logger?.warn("hermes_version_dashboard_fallback_requested", {
4586
+ dashboard_status_url: dashboardStatusUrl,
4587
+ reason: cliError instanceof Error ? cliError.message : String(cliError)
4594
4588
  });
4589
+ try {
4590
+ const fallback = await readHermesDashboardVersion({
4591
+ fetchImpl: options.fetchImpl,
4592
+ statusUrl: dashboardStatusUrl,
4593
+ timeoutMs: options.dashboardTimeoutMs
4594
+ });
4595
+ void options.logger?.info("hermes_version_dashboard_fallback_succeeded", {
4596
+ dashboard_status_url: dashboardStatusUrl,
4597
+ hermes_version: fallback.version
4598
+ });
4599
+ return fallback;
4600
+ } catch (dashboardError) {
4601
+ void options.logger?.warn("hermes_version_dashboard_fallback_failed", {
4602
+ dashboard_status_url: dashboardStatusUrl,
4603
+ error: dashboardError instanceof Error ? dashboardError.message : String(dashboardError)
4604
+ });
4605
+ throw new Error(
4606
+ `Hermes version detection failed. CLI: ${cliError instanceof Error ? cliError.message : String(cliError)}; dashboard fallback: ${dashboardError instanceof Error ? dashboardError.message : String(dashboardError)}`
4607
+ );
4608
+ }
4609
+ }
4610
+ }
4611
+ async function execHermesVersion(logger) {
4612
+ const hermesBin = resolveHermesBin();
4613
+ const failures = [];
4614
+ for (const args of [["version"], ["--version"]]) {
4615
+ try {
4616
+ return await execFileAsync2(hermesBin, args, {
4617
+ timeout: 5e3,
4618
+ windowsHide: true
4619
+ });
4620
+ } catch (error) {
4621
+ const failure = describeVersionCommandFailure(hermesBin, args, error);
4622
+ failures.push(failure.summary);
4623
+ void logger?.warn("hermes_version_cli_command_failed", failure.fields);
4624
+ }
4595
4625
  }
4626
+ throw new Error(failures.join("; "));
4596
4627
  }
4597
4628
  function assertHermesRunsApiSupported(version, status) {
4598
4629
  if (status !== 404) {
@@ -4617,7 +4648,7 @@ async function startHermesGatewayOnce(paths, profileName, logger) {
4617
4648
  return await gatewayStartInFlightByProfile.get(profileName);
4618
4649
  }
4619
4650
  async function startHermesGateway(paths, profileName, logger) {
4620
- const version = await readHermesVersion().catch((error) => {
4651
+ const version = await readHermesVersion({ logger }).catch((error) => {
4621
4652
  void logger?.error("gateway_hermes_cli_unavailable", {
4622
4653
  error: error instanceof Error ? error.message : String(error)
4623
4654
  });
@@ -4729,7 +4760,7 @@ async function restartHermesGatewayServiceIfAvailable(options) {
4729
4760
  return {
4730
4761
  pid: null,
4731
4762
  logPath,
4732
- version: await readHermesVersion().catch(() => null),
4763
+ version: await readHermesVersion({ logger: options.logger }).catch(() => null),
4733
4764
  ...logHint ? { logHint } : {}
4734
4765
  };
4735
4766
  }
@@ -4972,6 +5003,72 @@ function parseHermesVersion(value) {
4972
5003
  const match = /\bv?(\d+\.\d+\.\d+)\b/u.exec(value);
4973
5004
  return match?.[1] ?? null;
4974
5005
  }
5006
+ function buildHermesVersionInfo(raw, version) {
5007
+ return {
5008
+ raw,
5009
+ version,
5010
+ supportsApiServer: version ? compareSemver(version, MIN_API_SERVER_VERSION) >= 0 : null
5011
+ };
5012
+ }
5013
+ async function readHermesDashboardVersion(options = {}) {
5014
+ const fetcher = options.fetchImpl ?? fetch;
5015
+ const controller = new AbortController();
5016
+ const timer = setTimeout(
5017
+ () => controller.abort(),
5018
+ options.timeoutMs ?? DASHBOARD_STATUS_TIMEOUT_MS
5019
+ );
5020
+ try {
5021
+ const response = await fetcher(options.statusUrl ?? DASHBOARD_STATUS_URL, {
5022
+ method: "GET",
5023
+ headers: { accept: "application/json" },
5024
+ signal: controller.signal
5025
+ });
5026
+ if (!response.ok) {
5027
+ throw new Error(`Hermes dashboard returned HTTP ${response.status}`);
5028
+ }
5029
+ const payload = await response.json().catch(() => null);
5030
+ const record = toRecord2(payload);
5031
+ const versionText = readString4(record, "version");
5032
+ if (!versionText) {
5033
+ throw new Error("Hermes dashboard status did not include a version");
5034
+ }
5035
+ const raw = truncateVersionLogOutput(JSON.stringify(record));
5036
+ const version = parseHermesVersion(versionText) ?? parseHermesVersion(raw);
5037
+ return buildHermesVersionInfo(raw, version);
5038
+ } catch (error) {
5039
+ if (error instanceof Error && error.name === "AbortError") {
5040
+ throw new Error("Hermes dashboard version probe timed out");
5041
+ }
5042
+ throw error;
5043
+ } finally {
5044
+ clearTimeout(timer);
5045
+ }
5046
+ }
5047
+ function describeVersionCommandFailure(hermesBin, args, error) {
5048
+ const message = error instanceof Error ? error.message : String(error);
5049
+ const output = truncateVersionLogOutput(readExecErrorOutput2(error));
5050
+ return {
5051
+ summary: `${hermesBin} ${args.join(" ")} failed: ${message}`,
5052
+ fields: {
5053
+ hermes_bin: hermesBin,
5054
+ command: args.join(" "),
5055
+ error: message,
5056
+ ...output ? { output } : {}
5057
+ }
5058
+ };
5059
+ }
5060
+ function readExecErrorOutput2(error) {
5061
+ if (typeof error !== "object" || error === null) {
5062
+ return "";
5063
+ }
5064
+ const stdout = "stdout" in error && error.stdout != null ? String(error.stdout) : "";
5065
+ const stderr = "stderr" in error && error.stderr != null ? String(error.stderr) : "";
5066
+ return `${stdout}
5067
+ ${stderr}`.trim();
5068
+ }
5069
+ function truncateVersionLogOutput(value) {
5070
+ return value.length > MAX_VERSION_LOG_OUTPUT_LENGTH ? `${value.slice(0, MAX_VERSION_LOG_OUTPUT_LENGTH)}...` : value;
5071
+ }
4975
5072
  function compareSemver(left, right) {
4976
5073
  const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
4977
5074
  const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
@@ -10671,7 +10768,7 @@ async function createHermesRun(input, options = {}) {
10671
10768
  );
10672
10769
  if (response.status === 404 || response.status === 503) {
10673
10770
  assertHermesRunsApiSupported(
10674
- await readHermesVersion().catch(() => null),
10771
+ await readHermesVersion({ logger: options.logger }).catch(() => null),
10675
10772
  response.status
10676
10773
  );
10677
10774
  throw new LinkHttpError(
@@ -10698,7 +10795,7 @@ async function streamHermesRunEvents(runId, options = {}) {
10698
10795
  options
10699
10796
  );
10700
10797
  assertHermesRunsApiSupported(
10701
- await readHermesVersion().catch(() => null),
10798
+ await readHermesVersion({ logger: options.logger }).catch(() => null),
10702
10799
  response.status
10703
10800
  );
10704
10801
  if (!response.ok || !response.body) {
@@ -10741,7 +10838,7 @@ async function streamHermesResponses(input, options = {}) {
10741
10838
  );
10742
10839
  if (response.status === 404 || response.status === 503) {
10743
10840
  assertHermesRunsApiSupported(
10744
- await readHermesVersion().catch(() => null),
10841
+ await readHermesVersion({ logger: options.logger }).catch(() => null),
10745
10842
  response.status
10746
10843
  );
10747
10844
  throw new LinkHttpError(
@@ -19376,7 +19473,10 @@ var runningUpdate = null;
19376
19473
  async function readHermesUpdateCheck(options) {
19377
19474
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
19378
19475
  const [local, remoteResult] = await Promise.all([
19379
- readHermesVersion().catch((error) => ({
19476
+ readHermesVersion({
19477
+ fetchImpl: options.fetchImpl,
19478
+ logger: options.logger
19479
+ }).catch((error) => ({
19380
19480
  raw: error instanceof Error ? error.message : String(error),
19381
19481
  version: null
19382
19482
  })),
@@ -21437,6 +21537,17 @@ import path24 from "path";
21437
21537
  import { rm as rm12 } from "fs/promises";
21438
21538
 
21439
21539
  // src/relay/bootstrap.ts
21540
+ var RelayNetworkError = class extends Error {
21541
+ constructor(relayBaseUrl, causeMessage) {
21542
+ super(
21543
+ `Cannot reach Hermes Relay at ${relayBaseUrl}. Please check your network connection, VPN, or proxy settings, then try again.`
21544
+ );
21545
+ this.relayBaseUrl = relayBaseUrl;
21546
+ this.causeMessage = causeMessage;
21547
+ }
21548
+ relayBaseUrl;
21549
+ causeMessage;
21550
+ };
21440
21551
  async function bootstrapRelayLink(options) {
21441
21552
  const fetcher = options.fetchImpl ?? fetch;
21442
21553
  const baseUrl = options.relayBaseUrl.replace(/\/+$/u, "");
@@ -21477,14 +21588,23 @@ async function bootstrapRelayLink(options) {
21477
21588
  };
21478
21589
  }
21479
21590
  async function postJson(fetcher, url, token, body) {
21480
- const response = await fetcher(url, {
21481
- method: "POST",
21482
- headers: {
21483
- authorization: `Bearer ${token}`,
21484
- "content-type": "application/json"
21485
- },
21486
- body: JSON.stringify(body)
21487
- });
21591
+ let response;
21592
+ try {
21593
+ response = await fetcher(url, {
21594
+ method: "POST",
21595
+ headers: {
21596
+ authorization: `Bearer ${token}`,
21597
+ "content-type": "application/json"
21598
+ },
21599
+ body: JSON.stringify(body)
21600
+ });
21601
+ } catch (error) {
21602
+ const baseUrl = new URL(url).origin;
21603
+ throw new RelayNetworkError(
21604
+ baseUrl,
21605
+ error instanceof Error ? error.message : String(error)
21606
+ );
21607
+ }
21488
21608
  const payload = await response.json().catch(() => null);
21489
21609
  if (!response.ok) {
21490
21610
  const message = readErrorMessage4(payload) ?? `Relay request failed with HTTP ${response.status}`;
@@ -21512,14 +21632,22 @@ async function preparePairing(paths = resolveRuntimePaths()) {
21512
21632
  const config = await loadConfig(paths);
21513
21633
  const identity = await ensureIdentity(paths);
21514
21634
  const systemInfo = readLinkSystemInfo();
21515
- const created = await postServerJson(config.serverBaseUrl, "/api/v1/link-pairings", {
21516
- install_id: identity.install_id,
21517
- link_id: identity.link_id ?? void 0,
21518
- display_name: systemInfo.defaultDisplayName,
21519
- platform: systemInfo.platform,
21520
- hostname: systemInfo.hostname ?? void 0,
21521
- public_key_pem: identity.public_key_pem
21522
- });
21635
+ const created = await postServerJson(
21636
+ config.serverBaseUrl,
21637
+ "/api/v1/link-pairings",
21638
+ {
21639
+ install_id: identity.install_id,
21640
+ link_id: identity.link_id ?? void 0,
21641
+ display_name: systemInfo.defaultDisplayName,
21642
+ platform: systemInfo.platform,
21643
+ hostname: systemInfo.hostname ?? void 0,
21644
+ public_key_pem: identity.public_key_pem
21645
+ },
21646
+ {
21647
+ target: "server",
21648
+ action: "create pairing session"
21649
+ }
21650
+ );
21523
21651
  const relayBaseUrl = created.relayBaseUrl || config.relayBaseUrl;
21524
21652
  let assigned;
21525
21653
  let updatedIdentity;
@@ -21541,28 +21669,43 @@ async function preparePairing(paths = resolveRuntimePaths()) {
21541
21669
  installId: updatedIdentity.install_id,
21542
21670
  publicKeyPem: updatedIdentity.public_key_pem
21543
21671
  });
21544
- await patchServerJson(config.serverBaseUrl, `/api/v1/link-pairings/${created.sessionId}/link`, created.pairingToken, {
21545
- install_id: updatedIdentity.install_id,
21546
- link_id: assigned.linkId,
21547
- link_version: LINK_VERSION,
21548
- display_name: systemInfo.defaultDisplayName,
21549
- platform: systemInfo.platform,
21550
- hostname: systemInfo.hostname ?? void 0,
21551
- lan_ips: routes.lanIps,
21552
- public_ipv4s: routes.publicIpv4s,
21553
- public_ipv6s: routes.publicIpv6s,
21554
- preferred_urls: routes.preferredUrls,
21555
- environment: routes.environment
21556
- });
21672
+ await patchServerJson(
21673
+ config.serverBaseUrl,
21674
+ `/api/v1/link-pairings/${created.sessionId}/link`,
21675
+ created.pairingToken,
21676
+ {
21677
+ install_id: updatedIdentity.install_id,
21678
+ link_id: assigned.linkId,
21679
+ link_version: LINK_VERSION,
21680
+ display_name: systemInfo.defaultDisplayName,
21681
+ platform: systemInfo.platform,
21682
+ hostname: systemInfo.hostname ?? void 0,
21683
+ lan_ips: routes.lanIps,
21684
+ public_ipv4s: routes.publicIpv4s,
21685
+ public_ipv6s: routes.publicIpv6s,
21686
+ preferred_urls: routes.preferredUrls,
21687
+ environment: routes.environment
21688
+ },
21689
+ {
21690
+ target: "server",
21691
+ action: "finalize pairing"
21692
+ }
21693
+ );
21557
21694
  } catch (error) {
21695
+ const reportedError = error instanceof RelayNetworkError ? createPairingNetworkError({
21696
+ target: "relay",
21697
+ action: "connect to Relay",
21698
+ baseUrl: error.relayBaseUrl,
21699
+ detail: error.causeMessage
21700
+ }) : error;
21558
21701
  await reportPairingErrorToServer({
21559
21702
  serverBaseUrl: config.serverBaseUrl,
21560
21703
  sessionId: created.sessionId,
21561
21704
  source: "link",
21562
21705
  pairingToken: created.pairingToken,
21563
- error: pairingErrorSnapshot("prepare_pairing", error)
21706
+ error: pairingErrorSnapshot("prepare_pairing", reportedError)
21564
21707
  });
21565
- throw error;
21708
+ throw reportedError;
21566
21709
  }
21567
21710
  const qrPayload = {
21568
21711
  kind: "hermes_link_pairing",
@@ -21662,6 +21805,10 @@ async function claimPairing(input) {
21662
21805
  {
21663
21806
  claim_token: input.claimToken,
21664
21807
  app_instance_id: input.appInstanceId ?? void 0
21808
+ },
21809
+ {
21810
+ target: "server",
21811
+ action: "verify pairing claim"
21665
21812
  }
21666
21813
  );
21667
21814
  } catch (error) {
@@ -21709,15 +21856,25 @@ async function loadRequiredIdentity2(paths) {
21709
21856
  }
21710
21857
  return identity;
21711
21858
  }
21712
- async function postServerJson(serverBaseUrl, path25, body) {
21713
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21714
- method: "POST",
21715
- headers: {
21716
- accept: "application/json",
21717
- "content-type": "application/json"
21718
- },
21719
- body: JSON.stringify(body)
21720
- });
21859
+ async function postServerJson(serverBaseUrl, path25, body, options) {
21860
+ let response;
21861
+ try {
21862
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21863
+ method: "POST",
21864
+ headers: {
21865
+ accept: "application/json",
21866
+ "content-type": "application/json"
21867
+ },
21868
+ body: JSON.stringify(body)
21869
+ });
21870
+ } catch (error) {
21871
+ throw createPairingNetworkError({
21872
+ target: options.target,
21873
+ action: options.action,
21874
+ baseUrl: serverBaseUrl,
21875
+ detail: error instanceof Error ? error.message : String(error)
21876
+ });
21877
+ }
21721
21878
  return readJsonResponse2(response);
21722
21879
  }
21723
21880
  async function reportPairingErrorToServer(input) {
@@ -21750,16 +21907,26 @@ function pairingErrorSnapshot(stage, error) {
21750
21907
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
21751
21908
  };
21752
21909
  }
21753
- async function patchServerJson(serverBaseUrl, path25, token, body) {
21754
- const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21755
- method: "PATCH",
21756
- headers: {
21757
- accept: "application/json",
21758
- authorization: `Bearer ${token}`,
21759
- "content-type": "application/json"
21760
- },
21761
- body: JSON.stringify(body)
21762
- });
21910
+ async function patchServerJson(serverBaseUrl, path25, token, body, options) {
21911
+ let response;
21912
+ try {
21913
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path25}`, {
21914
+ method: "PATCH",
21915
+ headers: {
21916
+ accept: "application/json",
21917
+ authorization: `Bearer ${token}`,
21918
+ "content-type": "application/json"
21919
+ },
21920
+ body: JSON.stringify(body)
21921
+ });
21922
+ } catch (error) {
21923
+ throw createPairingNetworkError({
21924
+ target: options.target,
21925
+ action: options.action,
21926
+ baseUrl: serverBaseUrl,
21927
+ detail: error instanceof Error ? error.message : String(error)
21928
+ });
21929
+ }
21763
21930
  return readJsonResponse2(response);
21764
21931
  }
21765
21932
  async function readJsonResponse2(response) {
@@ -21781,6 +21948,15 @@ function readErrorMessage5(payload) {
21781
21948
  const message = error.message;
21782
21949
  return typeof message === "string" ? message : null;
21783
21950
  }
21951
+ function createPairingNetworkError(input) {
21952
+ const baseMessage = input.target === "server" ? `HermesPilot Server is unreachable while trying to ${input.action}.` : `Hermes Relay is unreachable while trying to ${input.action}.`;
21953
+ const hint = "If you are using a VPN, proxy, or corporate network, try turning it off and retrying.";
21954
+ return new LinkHttpError(
21955
+ 503,
21956
+ input.target === "server" ? "pairing_server_unreachable" : "pairing_relay_unreachable",
21957
+ `${baseMessage} Please check whether ${input.baseUrl} is reachable. ${hint} Detail: ${input.detail}`
21958
+ );
21959
+ }
21784
21960
  function pairingClaimPath(sessionId, paths) {
21785
21961
  return path24.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
21786
21962
  }
@@ -22916,6 +23092,7 @@ export {
22916
23092
  createFileLogger,
22917
23093
  getLinkLogFile,
22918
23094
  ensureHermesApiServerAvailable,
23095
+ readHermesVersion,
22919
23096
  loadConfig,
22920
23097
  saveConfig,
22921
23098
  normalizeLanHost,
package/dist/cli/index.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  preparePairing,
23
23
  probeLocalLinkService,
24
24
  readHermesApiServerConfig,
25
+ readHermesVersion,
25
26
  readPairingClaim,
26
27
  reportLinkStatusToServer,
27
28
  resolveHermesConfigPath,
@@ -32,7 +33,7 @@ import {
32
33
  startDaemonProcess,
33
34
  startLinkService,
34
35
  stopDaemonProcess
35
- } from "../chunk-RJB5VUT4.js";
36
+ } from "../chunk-7OVDWXR7.js";
36
37
 
37
38
  // src/cli/index.ts
38
39
  import { Command } from "commander";
@@ -273,16 +274,16 @@ var messages = {
273
274
  "autostart.alreadyEnabled": "Boot autostart is already enabled via {method}: {path}",
274
275
  "pair.description": "Create a Hermes Link pairing session",
275
276
  "pair.preflight": "Checking local Hermes configuration before pairing...",
277
+ "pair.preflight.hermesFiles": "Checking Hermes data directory, config, and environment files...",
278
+ "pair.preflight.hermesCli": "Checking whether the Hermes CLI is available...",
279
+ "pair.preflight.hermesApiServer": "Checking whether the Hermes API Server is ready...",
276
280
  "pair.hermesHome": "Hermes home: {path}",
281
+ "pair.hermesVersion": "Hermes CLI: {value}",
277
282
  "pair.apiReady": "Hermes API Server is ready on 127.0.0.1:{port}",
278
- "pair.preparing": "Preparing pairing session through HermesPilot Server and Relay...",
279
- "pair.server": "Server: {url}",
280
- "pair.relay": "Relay: {url}",
281
- "pair.linkId": "Hermes Link ID: {value}",
282
- "pair.code": "Pairing code: {value}",
283
- "pair.localApi": "Local API: http://127.0.0.1:{port}",
284
- "pair.scan": "Open this pairing page in the HermesPilot App or scan the QR code below:",
285
- "pair.openBrowserFailed": "Could not open the system browser automatically. You can still scan the QR code below or open this URL manually: {url}",
283
+ "pair.preparing": "Creating the pairing session...",
284
+ "pair.scan": "Please scan the QR code below in the HermesPilot App:",
285
+ "pair.openPairingPage": "If the QR code is hard to scan, you can open this page locally: {url}",
286
+ "pair.manualCode": "You can also use the HermesPilot App manual connection mode and enter this pairing code:",
286
287
  "pair.expires": "Pairing expires in 10 minutes. Press Ctrl+C to cancel waiting.",
287
288
  "pair.claimed": "Pairing succeeded. Starting Hermes Link in the background...",
288
289
  "pair.claimedRunning": "Pairing succeeded. Hermes Link is already running in the background.",
@@ -298,6 +299,8 @@ var messages = {
298
299
  "doctor.notAssigned": "not assigned",
299
300
  "doctor.lanHost": "Configured LAN host: {value}",
300
301
  "doctor.networkWarning": "Network note: {message}",
302
+ "doctor.hermesCli": "Hermes CLI: {value}",
303
+ "doctor.hermesCliUnavailable": "Hermes CLI is unavailable. Please make sure the `hermes` command can run in this system.",
301
304
  "doctor.apiReady": "Hermes API Server: ready",
302
305
  "doctor.apiStarted": "Hermes API Server: started and ready",
303
306
  "doctor.apiUnavailable": "Hermes API Server: unavailable. {message}",
@@ -306,6 +309,8 @@ var messages = {
306
309
  "error.relayLinkInvalid": "Relay did not return a valid link_id.",
307
310
  "error.relayEmpty": "Relay returned an empty response.",
308
311
  "error.serverHttp": "HermesPilot Server request failed with HTTP {status}.",
312
+ "error.pairingServerUnreachable": "Could not reach HermesPilot Server while creating the pairing session. Check whether {url} is reachable, then try again. If you use a proxy network, add hermes-server.clawpilot.me and hermes-relay.clawpilot.me to the proxy exclusion list, or temporarily turn off VPN/proxy and retry.",
313
+ "error.pairingRelayUnreachable": "Could not reach Hermes Relay while creating the pairing session. Check whether {url} is reachable, then try again. If you use a proxy network, add hermes-server.clawpilot.me and hermes-relay.clawpilot.me to the proxy exclusion list, or temporarily turn off VPN/proxy and retry.",
309
314
  "error.portInUse": "Local port {port} is already in use by another process. Stop that process or change the Hermes Link port, then run `hermeslink pair` again.",
310
315
  "error.pairingRequires": "Pairing needs HermesPilot Server and Relay, but this command could not start a complete pairing session.",
311
316
  "error.pairingRequires.detail": "The deployed services may be healthy, but the installed Link package must call Server for a short-lived relay bootstrap token before it can request a link_id."
@@ -363,16 +368,16 @@ var messages = {
363
368
  "autostart.alreadyEnabled": "\u5F00\u673A\u81EA\u542F\u5DF2\u542F\u7528\uFF0C\u65B9\u5F0F\uFF1A{method}\uFF0C\u6587\u4EF6\uFF1A{path}",
364
369
  "pair.description": "\u521B\u5EFA Hermes Link \u914D\u5BF9\u4F1A\u8BDD",
365
370
  "pair.preflight": "\u6B63\u5728\u914D\u5BF9\u524D\u68C0\u67E5\u672C\u673A Hermes \u914D\u7F6E...",
371
+ "pair.preflight.hermesFiles": "\u6B63\u5728\u68C0\u67E5 Hermes \u6570\u636E\u76EE\u5F55\u3001\u914D\u7F6E\u6587\u4EF6\u548C\u73AF\u5883\u6587\u4EF6...",
372
+ "pair.preflight.hermesCli": "\u6B63\u5728\u68C0\u67E5 Hermes CLI \u662F\u5426\u53EF\u7528...",
373
+ "pair.preflight.hermesApiServer": "\u6B63\u5728\u68C0\u67E5 Hermes API Server \u662F\u5426\u5C31\u7EEA...",
366
374
  "pair.hermesHome": "Hermes \u6570\u636E\u76EE\u5F55\uFF1A{path}",
375
+ "pair.hermesVersion": "Hermes CLI\uFF1A{value}",
367
376
  "pair.apiReady": "Hermes API Server \u5DF2\u5C31\u7EEA\uFF1A127.0.0.1:{port}",
368
- "pair.preparing": "\u6B63\u5728\u901A\u8FC7 HermesPilot Server \u548C Relay \u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD...",
369
- "pair.server": "Server\uFF1A{url}",
370
- "pair.relay": "Relay\uFF1A{url}",
371
- "pair.linkId": "Hermes Link ID\uFF1A{value}",
372
- "pair.code": "\u914D\u5BF9\u7801\uFF1A{value}",
373
- "pair.localApi": "\u672C\u5730 API\uFF1Ahttp://127.0.0.1:{port}",
374
- "pair.scan": "\u8BF7\u5728 HermesPilot App \u4E2D\u6253\u5F00\u8FD9\u4E2A\u914D\u5BF9\u9875\uFF0C\u6216\u626B\u63CF\u4E0B\u9762\u7684\u4E8C\u7EF4\u7801\uFF1A",
375
- "pair.openBrowserFailed": "\u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u7CFB\u7EDF\u6D4F\u89C8\u5668\u3002\u4F60\u4ECD\u7136\u53EF\u4EE5\u626B\u63CF\u4E0B\u9762\u7684\u4E8C\u7EF4\u7801\uFF0C\u6216\u624B\u52A8\u6253\u5F00\u8FD9\u4E2A\u94FE\u63A5\uFF1A{url}",
377
+ "pair.preparing": "\u6B63\u5728\u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD...",
378
+ "pair.scan": "\u8BF7\u5728 HermesPilot App \u4E2D\u626B\u7801\u4E0B\u9762\u7684\u4E8C\u7EF4\u7801\uFF1A",
379
+ "pair.openPairingPage": "\u5982\u679C\u4E8C\u7EF4\u7801\u4E0D\u5BB9\u6613\u626B\u63CF\uFF0C\u4F60\u53EF\u4EE5\u5728\u672C\u673A\u6253\u5F00\u8FD9\u4E2A\u9875\u9762\uFF1A{url}",
380
+ "pair.manualCode": "\u4F60\u4E5F\u53EF\u4EE5\u5728 HermesPilot App \u4E2D\u4F7F\u7528\u624B\u52A8\u8FDE\u63A5\u6A21\u5F0F\uFF0C\u8F93\u5165\u4EE5\u4E0B\u914D\u5BF9\u7801\u8FDB\u884C\u8FDE\u63A5\uFF1A",
376
381
  "pair.expires": "\u914D\u5BF9\u4F1A\u8BDD 10 \u5206\u949F\u540E\u8FC7\u671F\u3002\u6309 Ctrl+C \u9000\u51FA\u7B49\u5F85\u3002",
377
382
  "pair.claimed": "\u914D\u5BF9\u5DF2\u6210\u529F\u3002\u6B63\u5728\u628A Hermes Link \u5207\u6362\u5230\u540E\u53F0\u8FD0\u884C...",
378
383
  "pair.claimedRunning": "\u914D\u5BF9\u5DF2\u6210\u529F\u3002Hermes Link \u5DF2\u5728\u540E\u53F0\u6301\u7EED\u8FD0\u884C\u3002",
@@ -388,6 +393,8 @@ var messages = {
388
393
  "doctor.notAssigned": "\u5C1A\u672A\u5206\u914D",
389
394
  "doctor.lanHost": "\u5DF2\u914D\u7F6E\u5C40\u57DF\u7F51\u4E3B\u673A\uFF1A{value}",
390
395
  "doctor.networkWarning": "\u7F51\u7EDC\u63D0\u793A\uFF1A{message}",
396
+ "doctor.hermesCli": "Hermes CLI\uFF1A{value}",
397
+ "doctor.hermesCliUnavailable": "Hermes CLI\uFF1A\u4E0D\u53EF\u7528\u3002\u8BF7\u786E\u8BA4\u5F53\u524D\u7CFB\u7EDF\u53EF\u4EE5\u76F4\u63A5\u8FD0\u884C `hermes` \u547D\u4EE4\u3002",
391
398
  "doctor.apiReady": "Hermes API Server\uFF1A\u5DF2\u5C31\u7EEA",
392
399
  "doctor.apiStarted": "Hermes API Server\uFF1A\u5DF2\u81EA\u52A8\u542F\u52A8\u5E76\u5C31\u7EEA",
393
400
  "doctor.apiUnavailable": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528\u3002{message}",
@@ -396,6 +403,8 @@ var messages = {
396
403
  "error.relayLinkInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684 link_id\u3002",
397
404
  "error.relayEmpty": "Relay \u8FD4\u56DE\u4E86\u7A7A\u54CD\u5E94\u3002",
398
405
  "error.serverHttp": "HermesPilot Server \u8BF7\u6C42\u5931\u8D25\uFF0CHTTP \u72B6\u6001\u7801\uFF1A{status}\u3002",
406
+ "error.pairingServerUnreachable": "\u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD\u65F6\u65E0\u6CD5\u8FDE\u63A5 HermesPilot Server\u3002\u8BF7\u5148\u786E\u8BA4 {url} \u53EF\u4EE5\u8BBF\u95EE\uFF0C\u7136\u540E\u91CD\u8BD5\u3002\u91CD\u70B9\u63D0\u9192\uFF1A\u5982\u679C\u4F60\u4F7F\u7528\u4E86\u4EE3\u7406\u7F51\u7EDC\uFF0C\u53EF\u4EE5\u628A hermes-server.clawpilot.me \u548C hermes-relay.clawpilot.me \u52A0\u5165\u4EE3\u7406\u6392\u9664\u540D\u5355\uFF0C\u6216\u4E34\u65F6\u5173\u95ED VPN/\u4EE3\u7406\u540E\u518D\u8BD5\u3002",
407
+ "error.pairingRelayUnreachable": "\u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD\u65F6\u65E0\u6CD5\u8FDE\u63A5 Hermes Relay\u3002\u8BF7\u5148\u786E\u8BA4 {url} \u53EF\u4EE5\u8BBF\u95EE\uFF0C\u7136\u540E\u91CD\u8BD5\u3002\u91CD\u70B9\u63D0\u9192\uFF1A\u5982\u679C\u4F60\u4F7F\u7528\u4E86\u4EE3\u7406\u7F51\u7EDC\uFF0C\u53EF\u4EE5\u628A hermes-server.clawpilot.me \u548C hermes-relay.clawpilot.me \u52A0\u5165\u4EE3\u7406\u6392\u9664\u540D\u5355\uFF0C\u6216\u4E34\u65F6\u5173\u95ED VPN/\u4EE3\u7406\u540E\u518D\u8BD5\u3002",
399
408
  "error.portInUse": "\u672C\u5730\u7AEF\u53E3 {port} \u5DF2\u88AB\u5176\u4ED6\u8FDB\u7A0B\u5360\u7528\u3002\u8BF7\u5148\u505C\u6B62\u5360\u7528\u8BE5\u7AEF\u53E3\u7684\u7A0B\u5E8F\uFF0C\u6216\u8C03\u6574 Hermes Link \u7AEF\u53E3\u540E\u91CD\u65B0\u8FD0\u884C `hermeslink pair`\u3002",
400
409
  "error.pairingRequires": "\u914D\u5BF9\u9700\u8981 HermesPilot Server \u548C Relay\uFF0C\u4F46\u5F53\u524D\u547D\u4EE4\u6CA1\u6709\u80FD\u542F\u52A8\u5B8C\u6574\u914D\u5BF9\u4F1A\u8BDD\u3002",
401
410
  "error.pairingRequires.detail": "\u4E91\u7AEF\u670D\u52A1\u53EF\u4EE5\u662F\u5DF2\u90E8\u7F72\u4E14\u5065\u5EB7\u7684\uFF1B\u672C\u673A Link \u4ECD\u5FC5\u987B\u5148\u5411 Server \u7533\u8BF7\u77ED\u671F relay bootstrap token\uFF0C\u624D\u80FD\u518D\u5411 Relay \u7533\u8BF7 link_id\u3002"
@@ -459,6 +468,18 @@ function translateKnownError(message, language) {
459
468
  if (serverHttp?.groups?.status) {
460
469
  return translate(language, "error.serverHttp", { status: serverHttp.groups.status });
461
470
  }
471
+ const pairingServerUnreachable = /^HermesPilot Server is unreachable while trying to [^.]+\. Please check whether (?<url>\S+) is reachable\./u.exec(message);
472
+ if (pairingServerUnreachable?.groups?.url) {
473
+ return translate(language, "error.pairingServerUnreachable", {
474
+ url: pairingServerUnreachable.groups.url
475
+ });
476
+ }
477
+ const pairingRelayUnreachable = /^Hermes Relay is unreachable while trying to [^.]+\. Please check whether (?<url>\S+) is reachable\./u.exec(message);
478
+ if (pairingRelayUnreachable?.groups?.url) {
479
+ return translate(language, "error.pairingRelayUnreachable", {
480
+ url: pairingRelayUnreachable.groups.url
481
+ });
482
+ }
462
483
  if (message.includes("Pairing requires HermesPilot Server and Relay")) {
463
484
  return [translate(language, "error.pairingRequires"), translate(language, "error.pairingRequires.detail")].join("\n");
464
485
  }
@@ -487,6 +508,7 @@ async function assertPairingPreflightReady(options = {}) {
487
508
  const configPath = resolveHermesConfigPath(profileName);
488
509
  const envPath = path2.join(hermesHome, ".env");
489
510
  const failures = [];
511
+ options.onProgress?.("hermes_files");
490
512
  if (!await isDirectory(hermesHome)) {
491
513
  failures.push({
492
514
  code: "hermes_home_missing",
@@ -517,6 +539,23 @@ async function assertPairingPreflightReady(options = {}) {
517
539
  if (failures.length > 0) {
518
540
  throwPairingPreflightError(failures);
519
541
  }
542
+ options.onProgress?.("hermes_cli");
543
+ let hermesVersion;
544
+ try {
545
+ const readVersion = options.readHermesVersion ?? readHermesVersion;
546
+ hermesVersion = await readVersion();
547
+ } catch {
548
+ throwPairingPreflightError([
549
+ {
550
+ code: "hermes_cli_unavailable",
551
+ zh: "\u6682\u65F6\u627E\u4E0D\u5230\u53EF\u7528\u7684 Hermes CLI \u547D\u4EE4\u3002",
552
+ en: "Hermes CLI is not available right now.",
553
+ 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",
554
+ 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."
555
+ }
556
+ ]);
557
+ }
558
+ options.onProgress?.("hermes_api_server");
520
559
  const apiServerConfig = await readHermesApiServerConfig(profileName, configPath);
521
560
  if (apiServerConfig.enabled !== true) {
522
561
  throwPairingPreflightError([
@@ -548,6 +587,10 @@ async function assertPairingPreflightReady(options = {}) {
548
587
  started: availability.started,
549
588
  host: availability.configResult.apiServer.host ?? null,
550
589
  port: availability.configResult.apiServer.port ?? null
590
+ },
591
+ hermesVersion: {
592
+ raw: hermesVersion.raw,
593
+ version: hermesVersion.version
551
594
  }
552
595
  };
553
596
  } catch (error) {
@@ -780,16 +823,17 @@ program.command("pair").description(helpText("pair.description")).action(async (
780
823
  const language = languageChoice.language;
781
824
  const t = translate.bind(null, language);
782
825
  console.log(t("pair.preflight"));
783
- const preflight = await assertPairingPreflightReady({ paths });
784
826
  const environment = detectRuntimeEnvironment();
785
827
  if (environment.warning) {
786
828
  console.log(t("doctor.networkWarning", { message: environment.warning }));
787
829
  }
788
- console.log(t("pair.hermesHome", { path: preflight.hermesHome }));
789
- console.log(t("pair.apiReady", { port: preflight.apiServer.port ?? "unknown" }));
830
+ await assertPairingPreflightReady({
831
+ paths,
832
+ onProgress: (stage) => {
833
+ console.log(t(pairingPreflightProgressKey(stage)));
834
+ }
835
+ });
790
836
  console.log(t("pair.preparing"));
791
- console.log(t("pair.server", { url: config.serverBaseUrl }));
792
- console.log(t("pair.relay", { url: config.relayBaseUrl }));
793
837
  await ensureIdentity(paths);
794
838
  const hadActiveDevices = await hasActiveDevices(paths);
795
839
  const probeBeforePair = await probeLocalLinkService({ port: config.port });
@@ -806,19 +850,12 @@ program.command("pair").description(helpText("pair.description")).action(async (
806
850
  const service = reusedRunningService ? null : await startLinkService({ paths });
807
851
  const qrValue = JSON.stringify(prepared.qrPayload);
808
852
  const pairingPageUrl = `http://127.0.0.1:${config.port}/pair?session_id=${encodeURIComponent(prepared.sessionId)}`;
809
- console.log(t("pair.linkId", { value: prepared.linkId }));
810
- console.log(t("pair.code", { value: prepared.code }));
811
- console.log(t("pair.localApi", { port: config.port }));
812
- if (!reusedRunningService) {
813
- console.log(t("start.listening", { port: config.port }));
814
- }
815
853
  console.log(t("pair.scan"));
816
- console.log(`Pairing page: ${pairingPageUrl}`);
817
- const browserOpened = await openSystemBrowser(pairingPageUrl);
818
- if (!browserOpened) {
819
- console.log(t("pair.openBrowserFailed", { url: pairingPageUrl }));
820
- }
821
854
  qrcode.generate(qrValue, { small: true });
855
+ await openSystemBrowser(pairingPageUrl);
856
+ console.log(t("pair.openPairingPage", { url: pairingPageUrl }));
857
+ console.log(t("pair.manualCode"));
858
+ console.log(prepared.code);
822
859
  console.log(t("pair.expires"));
823
860
  const result = await waitForPairingOrShutdown(prepared.sessionId, paths);
824
861
  if (service) {
@@ -923,6 +960,12 @@ program.command("doctor").description(helpText("doctor.description")).action(asy
923
960
  console.log(`Hermes config backup: ${hermesConfig.backupPath}`);
924
961
  }
925
962
  }
963
+ try {
964
+ const hermesVersion = await readHermesVersion();
965
+ console.log(t("doctor.hermesCli", { value: formatHermesVersion(hermesVersion) }));
966
+ } catch {
967
+ console.log(t("doctor.hermesCliUnavailable"));
968
+ }
926
969
  try {
927
970
  const availability = await ensureHermesApiServerAvailable({ timeoutMs: 5e3 });
928
971
  console.log(t(availability.started ? "doctor.apiStarted" : "doctor.apiReady"));
@@ -939,6 +982,19 @@ async function loadCliLanguage() {
939
982
  const config = await loadConfig();
940
983
  return resolveLanguage(config.language);
941
984
  }
985
+ function formatHermesVersion(version) {
986
+ return version.version ?? version.raw;
987
+ }
988
+ function pairingPreflightProgressKey(stage) {
989
+ switch (stage) {
990
+ case "hermes_files":
991
+ return "pair.preflight.hermesFiles";
992
+ case "hermes_cli":
993
+ return "pair.preflight.hermesCli";
994
+ case "hermes_api_server":
995
+ return "pair.preflight.hermesApiServer";
996
+ }
997
+ }
942
998
  async function deliverStagedFilesFromCli(stagingDir, paths, config) {
943
999
  try {
944
1000
  return await deliverStagedFilesThroughDaemon(stagingDir, config.port);
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-RJB5VUT4.js";
3
+ } from "../chunk-7OVDWXR7.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.3.8",
3
+ "version": "0.3.9",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",