@hermespilot/link 0.4.6 → 0.4.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.
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
@@ -4132,7 +4132,7 @@ import os2 from "os";
4132
4132
  import path5 from "path";
4133
4133
 
4134
4134
  // src/constants.ts
4135
- var LINK_VERSION = "0.4.6";
4135
+ var LINK_VERSION = "0.4.7";
4136
4136
  var LINK_COMMAND = "hermeslink";
4137
4137
  var LINK_DEFAULT_PORT = 52379;
4138
4138
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -7871,12 +7871,12 @@ var ConversationMetadataCoordinator = class {
7871
7871
  return { ...next, last_event_seq: event.seq, updated_at: event.created_at };
7872
7872
  }
7873
7873
  scheduleGeneratedTitleRefresh(conversationId) {
7874
- for (const delay2 of GENERATED_TITLE_RETRY_DELAYS_MS) {
7874
+ for (const delay3 of GENERATED_TITLE_RETRY_DELAYS_MS) {
7875
7875
  setTimeout(() => {
7876
7876
  void this.generateTitleFromFirstRound(conversationId).catch(
7877
7877
  () => void 0
7878
7878
  );
7879
- }, delay2);
7879
+ }, delay3);
7880
7880
  }
7881
7881
  }
7882
7882
  async renameConversation(conversationId, title, input) {
@@ -16342,11 +16342,15 @@ function createHttpErrorMiddleware(logger) {
16342
16342
  import { execFile as execFile4 } from "child_process";
16343
16343
  import { readdir as readdir9, readFile as readFile12, rename as rename3, stat as stat12 } from "fs/promises";
16344
16344
  import path18 from "path";
16345
+ import { setTimeout as delay2 } from "timers/promises";
16345
16346
  import { promisify as promisify4 } from "util";
16346
16347
  import YAML2 from "yaml";
16347
16348
  var DEFAULT_PROFILE = "default";
16348
16349
  var PROFILE_NAME_PATTERN4 = /^[a-zA-Z0-9._-]{1,64}$/;
16349
16350
  var PROFILE_DELETE_TIMEOUT_MS = 3e4;
16351
+ var PROFILE_GATEWAY_STOP_TIMEOUT_MS = 3e3;
16352
+ var PROFILE_DELETE_STABLE_ABSENCE_MS = 1200;
16353
+ var PROFILE_DELETE_VERIFY_INTERVAL_MS = 150;
16350
16354
  var execFileAsync4 = promisify4(execFile4);
16351
16355
  async function listHermesProfiles(paths = resolveRuntimePaths()) {
16352
16356
  const profiles = /* @__PURE__ */ new Map();
@@ -16427,8 +16431,8 @@ async function deleteHermesProfile(name, paths = resolveRuntimePaths()) {
16427
16431
  `Profile "${name}" does not exist`
16428
16432
  );
16429
16433
  }
16430
- await deleteHermesProfileWithCli(name);
16431
- if (await pathExists(profile.path)) {
16434
+ await deleteHermesProfileWithCliAndVerify(name, profile.path);
16435
+ if (!await waitForProfilePathToRemainAbsent(profile.path)) {
16432
16436
  throw new LinkHttpError(
16433
16437
  502,
16434
16438
  "hermes_profile_delete_incomplete",
@@ -16512,6 +16516,116 @@ async function deleteHermesProfileWithCli(name) {
16512
16516
  );
16513
16517
  }
16514
16518
  }
16519
+ async function deleteHermesProfileWithCliAndVerify(name, profilePath) {
16520
+ for (let attempt = 0; attempt < 2; attempt += 1) {
16521
+ await stopHermesGatewayProcessesForProfile(name);
16522
+ if (attempt > 0 && !await pathExists(profilePath)) {
16523
+ return;
16524
+ }
16525
+ await deleteHermesProfileWithCli(name);
16526
+ if (await waitForProfilePathToRemainAbsent(profilePath)) {
16527
+ return;
16528
+ }
16529
+ }
16530
+ }
16531
+ async function stopHermesGatewayProcessesForProfile(name) {
16532
+ const pids = await findHermesGatewayProcessIdsForProfile(name);
16533
+ if (pids.length === 0) {
16534
+ return;
16535
+ }
16536
+ for (const pid of pids) {
16537
+ killProcess(pid, "SIGTERM");
16538
+ }
16539
+ const remainingAfterTerm = await waitForProcessesToExit(
16540
+ pids,
16541
+ PROFILE_GATEWAY_STOP_TIMEOUT_MS
16542
+ );
16543
+ if (remainingAfterTerm.length === 0) {
16544
+ return;
16545
+ }
16546
+ for (const pid of remainingAfterTerm) {
16547
+ killProcess(pid, "SIGKILL");
16548
+ }
16549
+ const remainingAfterKill = await waitForProcessesToExit(
16550
+ remainingAfterTerm,
16551
+ PROFILE_GATEWAY_STOP_TIMEOUT_MS
16552
+ );
16553
+ if (remainingAfterKill.length > 0) {
16554
+ throw new LinkHttpError(
16555
+ 502,
16556
+ "hermes_profile_gateway_stop_failed",
16557
+ `Could not stop Hermes Gateway process(es) for Profile "${name}": ${remainingAfterKill.join(", ")}`
16558
+ );
16559
+ }
16560
+ }
16561
+ async function findHermesGatewayProcessIdsForProfile(name) {
16562
+ if (process.platform === "win32") {
16563
+ return [];
16564
+ }
16565
+ try {
16566
+ const output = await execFileAsync4("ps", ["-axo", "pid=,command="], {
16567
+ timeout: 3e3,
16568
+ windowsHide: true
16569
+ });
16570
+ return parseHermesGatewayProcessIds(output.stdout.toString(), name);
16571
+ } catch {
16572
+ return [];
16573
+ }
16574
+ }
16575
+ function parseHermesGatewayProcessIds(output, name) {
16576
+ const pids = [];
16577
+ for (const line of output.split(/\r?\n/)) {
16578
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
16579
+ if (!match) {
16580
+ continue;
16581
+ }
16582
+ const pid = Number(match[1]);
16583
+ const command = match[2];
16584
+ if (Number.isSafeInteger(pid) && pid !== process.pid && isHermesGatewayRunCommandForProfile(command, name)) {
16585
+ pids.push(pid);
16586
+ }
16587
+ }
16588
+ return pids;
16589
+ }
16590
+ function isHermesGatewayRunCommandForProfile(command, name) {
16591
+ const escapedName = escapeRegExp2(name);
16592
+ return new RegExp(
16593
+ String.raw`(?:^|\s)-p\s+${escapedName}\s+gateway\s+run(?:\s|$)`
16594
+ ).test(command);
16595
+ }
16596
+ function killProcess(pid, signal) {
16597
+ try {
16598
+ process.kill(pid, signal);
16599
+ } catch {
16600
+ }
16601
+ }
16602
+ async function waitForProcessesToExit(pids, timeoutMs) {
16603
+ const deadline = Date.now() + timeoutMs;
16604
+ let remaining = pids.filter(isProcessRunning);
16605
+ while (remaining.length > 0 && Date.now() < deadline) {
16606
+ await delay2(100);
16607
+ remaining = remaining.filter(isProcessRunning);
16608
+ }
16609
+ return remaining;
16610
+ }
16611
+ function isProcessRunning(pid) {
16612
+ try {
16613
+ process.kill(pid, 0);
16614
+ return true;
16615
+ } catch (error) {
16616
+ return isNodeError14(error, "EPERM");
16617
+ }
16618
+ }
16619
+ async function waitForProfilePathToRemainAbsent(profilePath) {
16620
+ const deadline = Date.now() + PROFILE_DELETE_STABLE_ABSENCE_MS;
16621
+ while (Date.now() < deadline) {
16622
+ if (await pathExists(profilePath)) {
16623
+ return false;
16624
+ }
16625
+ await delay2(PROFILE_DELETE_VERIFY_INTERVAL_MS);
16626
+ }
16627
+ return !await pathExists(profilePath);
16628
+ }
16515
16629
  function formatExecError(error) {
16516
16630
  if (!(error instanceof Error)) {
16517
16631
  return String(error);
@@ -16539,6 +16653,9 @@ function readExecErrorOutput2(error) {
16539
16653
  function isNodeError14(error, code) {
16540
16654
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
16541
16655
  }
16656
+ function escapeRegExp2(value) {
16657
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16658
+ }
16542
16659
  async function countSkills(root) {
16543
16660
  const entries = await readdir9(root, { withFileTypes: true }).catch(
16544
16661
  (error) => {
@@ -17715,7 +17832,7 @@ async function writeEnvValues(profileName, values) {
17715
17832
  const nextLines = lines.map((line) => {
17716
17833
  const trimmed = line.trim();
17717
17834
  for (const [key, value] of remaining) {
17718
- const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp2(key)}=`, "u");
17835
+ const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp3(key)}=`, "u");
17719
17836
  if (keyPattern.test(trimmed)) {
17720
17837
  remaining.delete(key);
17721
17838
  return `${key}=${formatEnvValue2(value)}`;
@@ -17897,7 +18014,7 @@ function formatEnvValue2(value) {
17897
18014
  }
17898
18015
  return JSON.stringify(value);
17899
18016
  }
17900
- function escapeRegExp2(value) {
18017
+ function escapeRegExp3(value) {
17901
18018
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
17902
18019
  }
17903
18020
  function isNodeError15(error, code) {
@@ -19226,7 +19343,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
19226
19343
  `\u4E0D\u5141\u8BB8\u4ECE App \u5199\u5165 ${key}\u3002`
19227
19344
  );
19228
19345
  }
19229
- const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp3(key)}=`, "u");
19346
+ const keyPattern = new RegExp(`^(?:export\\s+)?${escapeRegExp4(key)}=`, "u");
19230
19347
  let replaced = false;
19231
19348
  for (let index = 0; index < nextLines.length; index += 1) {
19232
19349
  if (keyPattern.test(nextLines[index].trim())) {
@@ -19447,7 +19564,7 @@ function readBoolean3(value) {
19447
19564
  function formatEnvValue3(value) {
19448
19565
  return /^[A-Za-z0-9_./:@-]*$/u.test(value) ? value : JSON.stringify(value);
19449
19566
  }
19450
- function escapeRegExp3(value) {
19567
+ function escapeRegExp4(value) {
19451
19568
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
19452
19569
  }
19453
19570
  function isNodeError16(error, code) {
@@ -21172,9 +21289,9 @@ function connectRelayControl(options) {
21172
21289
  return;
21173
21290
  }
21174
21291
  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);
21292
+ const delay3 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
21293
+ options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay3}ms` });
21294
+ retryTimer = setTimeout(connect, delay3);
21178
21295
  retryTimer.unref?.();
21179
21296
  });
21180
21297
  };
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-PRYXZRQI.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-PRYXZRQI.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.7",
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
  }