@hermespilot/link 0.4.6 → 0.4.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.
package/README.md CHANGED
@@ -15,6 +15,14 @@ npm install -g @hermespilot/link
15
15
 
16
16
  The package installs the `hermeslink` command. It does not start the service automatically during installation.
17
17
 
18
+ If your shell cannot find `hermeslink` right after install, your npm global bin directory is probably not on `PATH`. On Unix-like systems you can run:
19
+
20
+ ```bash
21
+ export PATH="$(npm prefix -g)/bin:$PATH"
22
+ ```
23
+
24
+ You can also invoke the installed binary directly with `$(npm prefix -g)/bin/hermeslink`.
25
+
18
26
  ## Common commands
19
27
 
20
28
  ```bash
@@ -4117,7 +4117,8 @@ function isConversationMissingError(error) {
4117
4117
 
4118
4118
  // src/hermes/gateway.ts
4119
4119
  import { execFile as execFile2, spawn } from "child_process";
4120
- import { access, readFile as readFile5, stat as stat4 } from "fs/promises";
4120
+ import { constants as fsConstants } from "fs";
4121
+ import { access, readFile as readFile5, realpath, stat as stat4 } from "fs/promises";
4121
4122
  import path7 from "path";
4122
4123
  import { setTimeout as delay } from "timers/promises";
4123
4124
  import { promisify as promisify2 } from "util";
@@ -4132,7 +4133,7 @@ import os2 from "os";
4132
4133
  import path5 from "path";
4133
4134
 
4134
4135
  // src/constants.ts
4135
- var LINK_VERSION = "0.4.6";
4136
+ var LINK_VERSION = "0.4.8";
4136
4137
  var LINK_COMMAND = "hermeslink";
4137
4138
  var LINK_DEFAULT_PORT = 52379;
4138
4139
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4631,6 +4632,8 @@ var PROFILE_NAME_PATTERN = /^[a-zA-Z0-9._-]{1,64}$/u;
4631
4632
  var DASHBOARD_STATUS_URL = "http://127.0.0.1:9119/api/status";
4632
4633
  var DASHBOARD_STATUS_TIMEOUT_MS = 1500;
4633
4634
  var DEFAULT_VERSION_CACHE_TTL_MS = 6e4;
4635
+ var VERSION_METADATA_TIMEOUT_MS = 3e3;
4636
+ var VERSION_COMMAND_TIMEOUT_MS = 5e3;
4634
4637
  var MAX_VERSION_LOG_OUTPUT_LENGTH = 1200;
4635
4638
  var HERMES_GATEWAY_ENV_BLOCKLIST_PREFIXES = ["API_SERVER_"];
4636
4639
  var gatewayStartInFlightByProfile = /* @__PURE__ */ new Map();
@@ -4826,9 +4829,26 @@ async function readHermesVersion(options = {}) {
4826
4829
  }
4827
4830
  async function execHermesVersion(hermesBin, logger) {
4828
4831
  const failures = [];
4832
+ try {
4833
+ const metadataVersion = await readHermesPythonMetadataVersion(
4834
+ hermesBin,
4835
+ VERSION_METADATA_TIMEOUT_MS
4836
+ );
4837
+ if (metadataVersion) {
4838
+ return metadataVersion;
4839
+ }
4840
+ } catch (error) {
4841
+ const failure = describeVersionMetadataFailure(hermesBin, error);
4842
+ failures.push(failure);
4843
+ void logger?.debug("hermes_version_metadata_probe_failed", failure.fields);
4844
+ }
4829
4845
  for (const args of [["--version"], ["version"]]) {
4830
4846
  try {
4831
- return await spawnHermesVersionCommand(hermesBin, args, 5e3);
4847
+ return await spawnHermesVersionCommand(
4848
+ hermesBin,
4849
+ args,
4850
+ VERSION_COMMAND_TIMEOUT_MS
4851
+ );
4832
4852
  } catch (error) {
4833
4853
  const failure = describeVersionCommandFailure(hermesBin, args, error);
4834
4854
  failures.push(failure);
@@ -4844,6 +4864,109 @@ async function execHermesVersion(hermesBin, logger) {
4844
4864
  });
4845
4865
  throw new Error(summary);
4846
4866
  }
4867
+ async function readHermesPythonMetadataVersion(hermesBin, timeoutMs) {
4868
+ const hermesPath = await resolveExecutablePath(hermesBin);
4869
+ if (!hermesPath) {
4870
+ return null;
4871
+ }
4872
+ const firstLine = await readFirstLine(hermesPath);
4873
+ const interpreter = parsePythonShebang(firstLine);
4874
+ if (!interpreter) {
4875
+ return null;
4876
+ }
4877
+ return await spawnHermesMetadataCommand(interpreter, timeoutMs);
4878
+ }
4879
+ async function spawnHermesMetadataCommand(interpreter, timeoutMs) {
4880
+ const script = [
4881
+ "import importlib.metadata as metadata",
4882
+ "try:",
4883
+ ' version = metadata.version("hermes-agent")',
4884
+ ' release_date = ""',
4885
+ "except Exception:",
4886
+ " import hermes_cli",
4887
+ ' version = getattr(hermes_cli, "__version__", "")',
4888
+ ' release_date = getattr(hermes_cli, "__release_date__", "")',
4889
+ "if not version:",
4890
+ ' raise SystemExit("Hermes Agent version metadata not found")',
4891
+ 'suffix = f" ({release_date})" if release_date else ""',
4892
+ 'print(f"Hermes Agent v{version}{suffix}")',
4893
+ ""
4894
+ ].join("\n");
4895
+ return await new Promise((resolve, reject) => {
4896
+ const child = spawn(
4897
+ interpreter.command,
4898
+ [...interpreter.args, "-c", script],
4899
+ {
4900
+ stdio: ["ignore", "pipe", "pipe"],
4901
+ windowsHide: true,
4902
+ env: { ...process.env, HERMES_NONINTERACTIVE: "1" }
4903
+ }
4904
+ );
4905
+ let stdout = "";
4906
+ let stderr = "";
4907
+ let settled = false;
4908
+ const timer = setTimeout(() => {
4909
+ finish(() => {
4910
+ reject(
4911
+ createVersionCommandError(
4912
+ `Hermes Python metadata probe timed out after ${timeoutMs}ms`,
4913
+ { stdout, stderr, killed: true }
4914
+ )
4915
+ );
4916
+ }, true);
4917
+ }, timeoutMs);
4918
+ const finish = (callback, kill = false) => {
4919
+ if (settled) {
4920
+ return;
4921
+ }
4922
+ settled = true;
4923
+ clearTimeout(timer);
4924
+ if (kill && child.pid && !child.killed) {
4925
+ child.kill();
4926
+ }
4927
+ callback();
4928
+ };
4929
+ child.stdout?.on("data", (chunk) => {
4930
+ stdout += chunk.toString();
4931
+ });
4932
+ child.stderr?.on("data", (chunk) => {
4933
+ stderr += chunk.toString();
4934
+ });
4935
+ child.once("error", (error) => {
4936
+ finish(() => {
4937
+ reject(
4938
+ createVersionCommandError(error.message, {
4939
+ stdout,
4940
+ stderr,
4941
+ cause: error
4942
+ })
4943
+ );
4944
+ });
4945
+ });
4946
+ child.once("close", (code, signal) => {
4947
+ finish(() => {
4948
+ const raw = `${stdout}
4949
+ ${stderr}`.trim();
4950
+ if (code === 0 && parseHermesVersion(raw)) {
4951
+ resolve({ stdout: raw });
4952
+ return;
4953
+ }
4954
+ reject(
4955
+ createVersionCommandError(
4956
+ `Hermes Python metadata probe exited with code ${code ?? "null"}`,
4957
+ {
4958
+ stdout,
4959
+ stderr,
4960
+ code,
4961
+ signal,
4962
+ killed: child.killed
4963
+ }
4964
+ )
4965
+ );
4966
+ });
4967
+ });
4968
+ });
4969
+ }
4847
4970
  async function spawnHermesVersionCommand(hermesBin, args, timeoutMs) {
4848
4971
  return await new Promise((resolve, reject) => {
4849
4972
  const child = spawn(hermesBin, args, {
@@ -5424,6 +5547,20 @@ function createVersionCommandError(message, details) {
5424
5547
  }
5425
5548
  return error;
5426
5549
  }
5550
+ function describeVersionMetadataFailure(hermesBin, error) {
5551
+ const message = error instanceof Error ? error.message : String(error);
5552
+ const details = readExecErrorDetails(error, message);
5553
+ return {
5554
+ summary: `${hermesBin} Python metadata probe failed: ${message}`,
5555
+ fields: {
5556
+ hermes_bin: hermesBin,
5557
+ command: "python-metadata",
5558
+ cwd: process.cwd(),
5559
+ error: message,
5560
+ ...details
5561
+ }
5562
+ };
5563
+ }
5427
5564
  function describeVersionCommandFailure(hermesBin, args, error) {
5428
5565
  const message = error instanceof Error ? error.message : String(error);
5429
5566
  const details = readExecErrorDetails(error, message);
@@ -5473,6 +5610,91 @@ function readExecErrorDetails(error, message) {
5473
5610
  function truncateVersionLogOutput(value) {
5474
5611
  return value.length > MAX_VERSION_LOG_OUTPUT_LENGTH ? `${value.slice(0, MAX_VERSION_LOG_OUTPUT_LENGTH)}...` : value;
5475
5612
  }
5613
+ async function resolveExecutablePath(command) {
5614
+ const candidates = executablePathCandidates(command);
5615
+ for (const candidate of candidates) {
5616
+ try {
5617
+ await access(candidate, fsConstants.X_OK);
5618
+ return await realpath(candidate).catch(() => candidate);
5619
+ } catch {
5620
+ }
5621
+ }
5622
+ return null;
5623
+ }
5624
+ function executablePathCandidates(command) {
5625
+ const normalized = command.trim();
5626
+ if (!normalized) {
5627
+ return [];
5628
+ }
5629
+ if (/[\\/]/u.test(normalized)) {
5630
+ return withWindowsExecutableExtensions(normalized);
5631
+ }
5632
+ const pathEntries = (process.env.PATH ?? "").split(path7.delimiter).filter(Boolean);
5633
+ return pathEntries.flatMap(
5634
+ (entry) => withWindowsExecutableExtensions(path7.join(entry, normalized))
5635
+ );
5636
+ }
5637
+ function withWindowsExecutableExtensions(filePath) {
5638
+ if (process.platform !== "win32" || path7.extname(filePath)) {
5639
+ return [filePath];
5640
+ }
5641
+ const extensions = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean);
5642
+ return [filePath, ...extensions.map((extension) => `${filePath}${extension}`)];
5643
+ }
5644
+ async function readFirstLine(filePath) {
5645
+ const raw = await readFile5(filePath, "utf8");
5646
+ return raw.split(/\r?\n/u, 1)[0] ?? "";
5647
+ }
5648
+ function parsePythonShebang(line) {
5649
+ if (!line.startsWith("#!")) {
5650
+ return null;
5651
+ }
5652
+ const parts = splitShellWords(line.slice(2).trim());
5653
+ if (parts.length === 0) {
5654
+ return null;
5655
+ }
5656
+ const commandName = path7.basename(parts[0] ?? "").toLowerCase();
5657
+ if (commandName === "env") {
5658
+ const args = expandEnvShebangArgs(parts.slice(1));
5659
+ const pythonIndex = args.findIndex((part) => isPythonCommandName(part));
5660
+ if (pythonIndex < 0) {
5661
+ return null;
5662
+ }
5663
+ return {
5664
+ command: parts[0],
5665
+ args
5666
+ };
5667
+ }
5668
+ if (!isPythonCommandName(commandName)) {
5669
+ return null;
5670
+ }
5671
+ return {
5672
+ command: parts[0],
5673
+ args: parts.slice(1)
5674
+ };
5675
+ }
5676
+ function isPythonCommandName(command) {
5677
+ return /^python(?:\d+(?:\.\d+)?)?$/iu.test(path7.basename(command));
5678
+ }
5679
+ function expandEnvShebangArgs(args) {
5680
+ const expanded = [];
5681
+ for (let index = 0; index < args.length; index += 1) {
5682
+ const arg = args[index];
5683
+ if (arg === "-S" && args[index + 1]) {
5684
+ expanded.push(...splitShellWords(args[index + 1]));
5685
+ index += 1;
5686
+ continue;
5687
+ }
5688
+ if (arg !== "-S") {
5689
+ expanded.push(arg);
5690
+ }
5691
+ }
5692
+ return expanded;
5693
+ }
5694
+ function splitShellWords(value) {
5695
+ const matches = value.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/gu);
5696
+ return [...matches].map((match) => match[1] ?? match[2] ?? match[3] ?? "");
5697
+ }
5476
5698
  function compareSemver(left, right) {
5477
5699
  const leftParts = left.split(".").map((part) => Number.parseInt(part, 10));
5478
5700
  const rightParts = right.split(".").map((part) => Number.parseInt(part, 10));
@@ -7871,12 +8093,12 @@ var ConversationMetadataCoordinator = class {
7871
8093
  return { ...next, last_event_seq: event.seq, updated_at: event.created_at };
7872
8094
  }
7873
8095
  scheduleGeneratedTitleRefresh(conversationId) {
7874
- for (const delay2 of GENERATED_TITLE_RETRY_DELAYS_MS) {
8096
+ for (const delay3 of GENERATED_TITLE_RETRY_DELAYS_MS) {
7875
8097
  setTimeout(() => {
7876
8098
  void this.generateTitleFromFirstRound(conversationId).catch(
7877
8099
  () => void 0
7878
8100
  );
7879
- }, delay2);
8101
+ }, delay3);
7880
8102
  }
7881
8103
  }
7882
8104
  async renameConversation(conversationId, title, input) {
@@ -12677,7 +12899,7 @@ async function resolveHermesPythonCommand() {
12677
12899
  if (explicit) {
12678
12900
  return { command: explicit, args: [] };
12679
12901
  }
12680
- const hermesBin = await resolveExecutablePath(resolveHermesBin());
12902
+ const hermesBin = await resolveExecutablePath2(resolveHermesBin());
12681
12903
  if (hermesBin) {
12682
12904
  const shebang = await readShebang(hermesBin);
12683
12905
  const command = shebangToPythonCommand(shebang);
@@ -12690,7 +12912,7 @@ async function resolveHermesPythonCommand() {
12690
12912
  args: []
12691
12913
  };
12692
12914
  }
12693
- async function resolveExecutablePath(command) {
12915
+ async function resolveExecutablePath2(command) {
12694
12916
  if (path17.isAbsolute(command)) {
12695
12917
  return await isExecutableFile(command) ? command : null;
12696
12918
  }
@@ -16342,11 +16564,15 @@ function createHttpErrorMiddleware(logger) {
16342
16564
  import { execFile as execFile4 } from "child_process";
16343
16565
  import { readdir as readdir9, readFile as readFile12, rename as rename3, stat as stat12 } from "fs/promises";
16344
16566
  import path18 from "path";
16567
+ import { setTimeout as delay2 } from "timers/promises";
16345
16568
  import { promisify as promisify4 } from "util";
16346
16569
  import YAML2 from "yaml";
16347
16570
  var DEFAULT_PROFILE = "default";
16348
16571
  var PROFILE_NAME_PATTERN4 = /^[a-zA-Z0-9._-]{1,64}$/;
16349
16572
  var PROFILE_DELETE_TIMEOUT_MS = 3e4;
16573
+ var PROFILE_GATEWAY_STOP_TIMEOUT_MS = 3e3;
16574
+ var PROFILE_DELETE_STABLE_ABSENCE_MS = 1200;
16575
+ var PROFILE_DELETE_VERIFY_INTERVAL_MS = 150;
16350
16576
  var execFileAsync4 = promisify4(execFile4);
16351
16577
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
16352
16578
  const profiles = /* @__PURE__ */ new Map();
@@ -16427,8 +16653,8 @@ async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
16427
16653
  `Profile "${name}" does not exist`
16428
16654
  );
16429
16655
  }
16430
- await deleteHermesProfileWithCli(name);
16431
- if (await pathExists(profile.path)) {
16656
+ await deleteHermesProfileWithCliAndVerify(name, profile.path);
16657
+ if (!await waitForProfilePathToRemainAbsent(profile.path)) {
16432
16658
  throw new LinkHttpError(
16433
16659
  502,
16434
16660
  "hermes_profile_delete_incomplete",
@@ -16512,6 +16738,116 @@ async function deleteHermesProfileWithCli(name) {
16512
16738
  );
16513
16739
  }
16514
16740
  }
16741
+ async function deleteHermesProfileWithCliAndVerify(name, profilePath) {
16742
+ for (let attempt = 0; attempt < 2; attempt += 1) {
16743
+ await stopHermesGatewayProcessesForProfile(name);
16744
+ if (attempt > 0 && !await pathExists(profilePath)) {
16745
+ return;
16746
+ }
16747
+ await deleteHermesProfileWithCli(name);
16748
+ if (await waitForProfilePathToRemainAbsent(profilePath)) {
16749
+ return;
16750
+ }
16751
+ }
16752
+ }
16753
+ async function stopHermesGatewayProcessesForProfile(name) {
16754
+ const pids = await findHermesGatewayProcessIdsForProfile(name);
16755
+ if (pids.length === 0) {
16756
+ return;
16757
+ }
16758
+ for (const pid of pids) {
16759
+ killProcess(pid, "SIGTERM");
16760
+ }
16761
+ const remainingAfterTerm = await waitForProcessesToExit(
16762
+ pids,
16763
+ PROFILE_GATEWAY_STOP_TIMEOUT_MS
16764
+ );
16765
+ if (remainingAfterTerm.length === 0) {
16766
+ return;
16767
+ }
16768
+ for (const pid of remainingAfterTerm) {
16769
+ killProcess(pid, "SIGKILL");
16770
+ }
16771
+ const remainingAfterKill = await waitForProcessesToExit(
16772
+ remainingAfterTerm,
16773
+ PROFILE_GATEWAY_STOP_TIMEOUT_MS
16774
+ );
16775
+ if (remainingAfterKill.length > 0) {
16776
+ throw new LinkHttpError(
16777
+ 502,
16778
+ "hermes_profile_gateway_stop_failed",
16779
+ `Could not stop Hermes Gateway process(es) for Profile "${name}": ${remainingAfterKill.join(", ")}`
16780
+ );
16781
+ }
16782
+ }
16783
+ async function findHermesGatewayProcessIdsForProfile(name) {
16784
+ if (process.platform === "win32") {
16785
+ return [];
16786
+ }
16787
+ try {
16788
+ const output = await execFileAsync4("ps", ["-axo", "pid=,command="], {
16789
+ timeout: 3e3,
16790
+ windowsHide: true
16791
+ });
16792
+ return parseHermesGatewayProcessIds(output.stdout.toString(), name);
16793
+ } catch {
16794
+ return [];
16795
+ }
16796
+ }
16797
+ function parseHermesGatewayProcessIds(output, name) {
16798
+ const pids = [];
16799
+ for (const line of output.split(/\r?\n/)) {
16800
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
16801
+ if (!match) {
16802
+ continue;
16803
+ }
16804
+ const pid = Number(match[1]);
16805
+ const command = match[2];
16806
+ if (Number.isSafeInteger(pid) && pid !== process.pid && isHermesGatewayRunCommandForProfile(command, name)) {
16807
+ pids.push(pid);
16808
+ }
16809
+ }
16810
+ return pids;
16811
+ }
16812
+ function isHermesGatewayRunCommandForProfile(command, name) {
16813
+ const escapedName = escapeRegExp2(name);
16814
+ return new RegExp(
16815
+ String.raw`(?:^|\s)-p\s+${escapedName}\s+gateway\s+run(?:\s|$)`
16816
+ ).test(command);
16817
+ }
16818
+ function killProcess(pid, signal) {
16819
+ try {
16820
+ process.kill(pid, signal);
16821
+ } catch {
16822
+ }
16823
+ }
16824
+ async function waitForProcessesToExit(pids, timeoutMs) {
16825
+ const deadline = Date.now() + timeoutMs;
16826
+ let remaining = pids.filter(isProcessRunning);
16827
+ while (remaining.length > 0 && Date.now() < deadline) {
16828
+ await delay2(100);
16829
+ remaining = remaining.filter(isProcessRunning);
16830
+ }
16831
+ return remaining;
16832
+ }
16833
+ function isProcessRunning(pid) {
16834
+ try {
16835
+ process.kill(pid, 0);
16836
+ return true;
16837
+ } catch (error) {
16838
+ return isNodeError14(error, "EPERM");
16839
+ }
16840
+ }
16841
+ async function waitForProfilePathToRemainAbsent(profilePath) {
16842
+ const deadline = Date.now() + PROFILE_DELETE_STABLE_ABSENCE_MS;
16843
+ while (Date.now() < deadline) {
16844
+ if (await pathExists(profilePath)) {
16845
+ return false;
16846
+ }
16847
+ await delay2(PROFILE_DELETE_VERIFY_INTERVAL_MS);
16848
+ }
16849
+ return !await pathExists(profilePath);
16850
+ }
16515
16851
  function formatExecError(error) {
16516
16852
  if (!(error instanceof Error)) {
16517
16853
  return String(error);
@@ -16539,6 +16875,9 @@ function readExecErrorOutput2(error) {
16539
16875
  function isNodeError14(error, code) {
16540
16876
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
16541
16877
  }
16878
+ function escapeRegExp2(value) {
16879
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16880
+ }
16542
16881
  async function countSkills(root) {
16543
16882
  const entries = await readdir9(root, { withFileTypes: true }).catch(
16544
16883
  (error) => {
@@ -17715,7 +18054,7 @@ async function writeEnvValues(profileName, values) {
17715
18054
  const nextLines = lines.map((line) => {
17716
18055
  const trimmed = line.trim();
17717
18056
  for (const [key, value] of remaining) {
17718
- const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp2(key)}=`, "u");
18057
+ const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp3(key)}=`, "u");
17719
18058
  if (keyPattern.test(trimmed)) {
17720
18059
  remaining.delete(key);
17721
18060
  return `${key}=${formatEnvValue2(value)}`;
@@ -17897,7 +18236,7 @@ function formatEnvValue2(value) {
17897
18236
  }
17898
18237
  return JSON.stringify(value);
17899
18238
  }
17900
- function escapeRegExp2(value) {
18239
+ function escapeRegExp3(value) {
17901
18240
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
17902
18241
  }
17903
18242
  function isNodeError15(error, code) {
@@ -19226,7 +19565,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
19226
19565
  `\u4E0D\u5141\u8BB8\u4ECE App \u5199\u5165 ${key}\u3002`
19227
19566
  );
19228
19567
  }
19229
- const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp3(key)}=`, "u");
19568
+ const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp4(key)}=`, "u");
19230
19569
  let replaced = false;
19231
19570
  for (let index = 0; index < nextLines.length; index += 1) {
19232
19571
  if (keyPattern.test(nextLines[index].trim())) {
@@ -19447,7 +19786,7 @@ function readBoolean3(value) {
19447
19786
  function formatEnvValue3(value) {
19448
19787
  return /^[A-Za-z0-9_./:@-]*$/u.test(value) ? value : JSON.stringify(value);
19449
19788
  }
19450
- function escapeRegExp3(value) {
19789
+ function escapeRegExp4(value) {
19451
19790
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
19452
19791
  }
19453
19792
  function isNodeError16(error, code) {
@@ -21172,9 +21511,9 @@ function connectRelayControl(options) {
21172
21511
  return;
21173
21512
  }
21174
21513
  reconnectAttempts += 1;
21175
- const delay2 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
21176
- options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay2}ms` });
21177
- retryTimer = setTimeout(connect, delay2);
21514
+ const delay3 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
21515
+ options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay3}ms` });
21516
+ retryTimer = setTimeout(connect, delay3);
21178
21517
  retryTimer.unref?.();
21179
21518
  });
21180
21519
  };
package/dist/cli/index.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  startDaemonProcess,
37
37
  startLinkService,
38
38
  stopDaemonProcess
39
- } from "../chunk-DUQDO2LQ.js";
39
+ } from "../chunk-B42L327D.js";
40
40
 
41
41
  // src/cli/index.ts
42
42
  import { Command } from "commander";
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-DUQDO2LQ.js";
3
+ } from "../chunk-B42L327D.js";
4
4
  export {
5
5
  createApp
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hermespilot/link",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",
@@ -1,3 +1,5 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import path from "node:path";
1
3
  import process from "node:process";
2
4
 
3
5
  function isTruthy(value) {
@@ -14,6 +16,49 @@ function shouldPrintInstallHint() {
14
16
  return process.env.npm_config_global === "true";
15
17
  }
16
18
 
19
+ function resolveNpmCommand() {
20
+ return process.platform === "win32" ? "npm.cmd" : "npm";
21
+ }
22
+
23
+ function resolveGlobalPrefix() {
24
+ const envPrefix =
25
+ process.env.npm_config_prefix?.trim() ??
26
+ process.env.npm_config_global_prefix?.trim() ??
27
+ "";
28
+ if (envPrefix) {
29
+ return envPrefix;
30
+ }
31
+ try {
32
+ const output = execFileSync(resolveNpmCommand(), ["prefix", "-g"], {
33
+ encoding: "utf8",
34
+ });
35
+ return output.trim();
36
+ } catch {
37
+ return "";
38
+ }
39
+ }
40
+
41
+ function resolveGlobalBinDir(prefix) {
42
+ return process.platform === "win32" ? prefix : path.join(prefix, "bin");
43
+ }
44
+
45
+ function resolveHermesLinkCommandPath(binDir) {
46
+ return process.platform === "win32"
47
+ ? path.join(binDir, "hermeslink.cmd")
48
+ : path.join(binDir, "hermeslink");
49
+ }
50
+
51
+ function pathIncludes(target) {
52
+ if (!target) {
53
+ return false;
54
+ }
55
+ const normalizedTarget = path.resolve(target);
56
+ return (process.env.PATH ?? "")
57
+ .split(path.delimiter)
58
+ .filter(Boolean)
59
+ .some((entry) => path.resolve(entry) === normalizedTarget);
60
+ }
61
+
17
62
  function detectLanguage() {
18
63
  const candidates = [
19
64
  process.env.HERMESLINK_LANG,
@@ -42,13 +87,35 @@ async function main() {
42
87
  }
43
88
 
44
89
  const language = detectLanguage();
90
+ const globalPrefix = resolveGlobalPrefix();
91
+ const globalBinDir = globalPrefix ? resolveGlobalBinDir(globalPrefix) : "";
92
+ const commandPath = globalBinDir ? resolveHermesLinkCommandPath(globalBinDir) : "";
93
+ const binOnPath = globalBinDir ? pathIncludes(globalBinDir) : false;
45
94
  console.log("");
46
95
  if (language === "zh-CN") {
47
96
  console.log("Hermes Link 已安装。");
48
97
  console.log("运行 `hermeslink pair`,把这台电脑连接到 HermesPilot App。");
98
+ if (globalBinDir && !binOnPath) {
99
+ console.log(
100
+ `如果当前 shell 找不到 \`hermeslink\`,说明 npm 全局 bin 目录没有进 PATH:${globalBinDir}`,
101
+ );
102
+ console.log(`你可以直接运行:${commandPath} pair`);
103
+ if (process.platform !== "win32") {
104
+ console.log(`或者先补 PATH:export PATH="${globalBinDir}:$PATH"`);
105
+ }
106
+ }
49
107
  } else {
50
108
  console.log("Hermes Link installed.");
51
109
  console.log("Run `hermeslink pair` to connect this computer with HermesPilot App.");
110
+ if (globalBinDir && !binOnPath) {
111
+ console.log(
112
+ `If your shell cannot find \`hermeslink\`, npm's global bin directory is not on PATH: ${globalBinDir}`,
113
+ );
114
+ console.log(`You can run it directly: ${commandPath} pair`);
115
+ if (process.platform !== "win32") {
116
+ console.log(`Or add it to PATH first: export PATH="${globalBinDir}:$PATH"`);
117
+ }
118
+ }
52
119
  }
53
120
  console.log("");
54
121
  }