@hermespilot/link 0.2.7 → 0.2.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.
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  clearPairingClaim,
7
7
  currentCliScriptPath,
8
8
  daemonLogFile,
9
+ detectRuntimeEnvironment,
9
10
  ensureHermesApiServerAvailable,
10
11
  ensureHermesApiServerConfig,
11
12
  ensureIdentity,
@@ -15,21 +16,25 @@ import {
15
16
  hasActiveDevices,
16
17
  loadConfig,
17
18
  loadIdentity,
19
+ normalizeLanHost,
18
20
  preparePairing,
19
21
  probeLocalLinkService,
20
22
  readHermesApiServerConfig,
21
23
  readPairingClaim,
24
+ reportLinkStatusToServer,
22
25
  resolveHermesConfigPath,
23
26
  resolveHermesProfileDir,
24
27
  resolveRuntimePaths,
25
28
  runDaemonSupervisor,
29
+ saveConfig,
26
30
  startDaemonProcess,
27
31
  startLinkService,
28
32
  stopDaemonProcess
29
- } from "../chunk-SSJ7WDD7.js";
33
+ } from "../chunk-VLTX75SY.js";
30
34
 
31
35
  // src/cli/index.ts
32
36
  import { Command } from "commander";
37
+ import { createInterface } from "readline/promises";
33
38
  import qrcode from "qrcode-terminal";
34
39
 
35
40
  // src/autostart/autostart.ts
@@ -221,6 +226,9 @@ var messages = {
221
226
  "status.runtime": "Runtime: {value}",
222
227
  "status.mode": "Mode: {value}",
223
228
  "status.port": "Local port: {value}",
229
+ "status.lanHost": "Configured LAN host: {value}",
230
+ "status.notSet": "not set",
231
+ "status.environmentWarning": "Network note: {message}",
224
232
  "status.linkId": "Link ID: {value}",
225
233
  "status.notPaired": "not paired",
226
234
  "start.description": "Start Hermes Link daemon",
@@ -234,6 +242,15 @@ var messages = {
234
242
  "stop.stopped": "Hermes Link stopped.",
235
243
  "stop.notRunning": "Hermes Link is not running.",
236
244
  "restart.description": "Restart the background Hermes Link daemon",
245
+ "config.description": "Manage local Hermes Link configuration",
246
+ "config.set.description": "Set a configuration value",
247
+ "config.unset.description": "Unset a configuration value",
248
+ "config.unknownKey": "Unknown config key: {key}",
249
+ "config.lanHostInvalid": "lan-host must be a private LAN IPv4 address, such as 192.168.1.23.",
250
+ "config.lanHostSet": "Configured LAN host: {value}",
251
+ "config.lanHostUnset": "Configured LAN host cleared.",
252
+ "config.reported": "Updated HermesPilot Server with the latest LAN address.",
253
+ "config.reportSkippedUnpaired": "Hermes Link is not paired yet. The LAN address will be reported after pairing.",
237
254
  "daemon.description": "Run Hermes Link in the foreground",
238
255
  "daemon.foreground": "Hermes Link foreground daemon is running. Press Ctrl+C to stop.",
239
256
  "logs.description": "Show Hermes Link log paths",
@@ -260,9 +277,13 @@ var messages = {
260
277
  "pair.code": "Pairing code: {value}",
261
278
  "pair.localApi": "Local API: http://127.0.0.1:{port}",
262
279
  "pair.scan": "Open this pairing page in the HermesPilot App or scan the QR code below:",
280
+ "pair.openBrowserFailed": "Could not open the system browser automatically. You can still scan the QR code below or open this URL manually: {url}",
263
281
  "pair.expires": "Pairing expires in 10 minutes. Press Ctrl+C to cancel waiting.",
264
282
  "pair.claimed": "Pairing succeeded. Starting Hermes Link in the background...",
265
283
  "pair.claimedRunning": "Pairing succeeded. Hermes Link is already running in the background.",
284
+ "pair.relayOnlyNotice": "Network note: this {kind} environment does not expose a phone-reachable LAN/public direct address by default. The App will connect through Relay.",
285
+ "pair.relayOnlyLanHostHint": "If you manually expose this Link from Windows or your router, run `hermeslink config set lan-host <Windows LAN IP>` to publish the reachable LAN address.",
286
+ "pair.relayOnlySafetyHint": "Hermes Link will not automatically change Windows/WSL bridge, firewall, or portproxy settings because those are system-level network exposure choices.",
266
287
  "pair.autostartUnchanged": "Existing paired devices found. Boot autostart settings were left unchanged.",
267
288
  "pair.autostartFailed": "Pairing succeeded, but boot autostart could not be enabled: {message}",
268
289
  "doctor.description": "Run local diagnostics",
@@ -270,6 +291,8 @@ var messages = {
270
291
  "doctor.installId": "Install ID: {value}",
271
292
  "doctor.linkId": "Link ID: {value}",
272
293
  "doctor.notAssigned": "not assigned",
294
+ "doctor.lanHost": "Configured LAN host: {value}",
295
+ "doctor.networkWarning": "Network note: {message}",
273
296
  "doctor.apiReady": "Hermes API Server: ready",
274
297
  "doctor.apiStarted": "Hermes API Server: started and ready",
275
298
  "doctor.apiUnavailable": "Hermes API Server: unavailable. {message}",
@@ -290,6 +313,9 @@ var messages = {
290
313
  "status.runtime": "\u8FD0\u884C\u76EE\u5F55\uFF1A{value}",
291
314
  "status.mode": "\u6A21\u5F0F\uFF1A{value}",
292
315
  "status.port": "\u672C\u5730\u7AEF\u53E3\uFF1A{value}",
316
+ "status.lanHost": "\u5DF2\u914D\u7F6E\u5C40\u57DF\u7F51\u4E3B\u673A\uFF1A{value}",
317
+ "status.notSet": "\u672A\u8BBE\u7F6E",
318
+ "status.environmentWarning": "\u7F51\u7EDC\u63D0\u793A\uFF1A{message}",
293
319
  "status.linkId": "Link ID\uFF1A{value}",
294
320
  "status.notPaired": "\u5C1A\u672A\u914D\u5BF9",
295
321
  "start.description": "\u542F\u52A8 Hermes Link \u670D\u52A1",
@@ -303,6 +329,15 @@ var messages = {
303
329
  "stop.stopped": "Hermes Link \u5DF2\u505C\u6B62\u3002",
304
330
  "stop.notRunning": "Hermes Link \u6CA1\u6709\u5728\u8FD0\u884C\u3002",
305
331
  "restart.description": "\u91CD\u542F\u540E\u53F0 Hermes Link \u670D\u52A1",
332
+ "config.description": "\u7BA1\u7406\u672C\u673A Hermes Link \u914D\u7F6E",
333
+ "config.set.description": "\u8BBE\u7F6E\u914D\u7F6E\u9879",
334
+ "config.unset.description": "\u6E05\u9664\u914D\u7F6E\u9879",
335
+ "config.unknownKey": "\u672A\u77E5\u914D\u7F6E\u9879\uFF1A{key}",
336
+ "config.lanHostInvalid": "lan-host \u5FC5\u987B\u662F\u5C40\u57DF\u7F51 IPv4 \u5730\u5740\uFF0C\u4F8B\u5982 192.168.1.23\u3002",
337
+ "config.lanHostSet": "\u5DF2\u914D\u7F6E\u5C40\u57DF\u7F51\u4E3B\u673A\uFF1A{value}",
338
+ "config.lanHostUnset": "\u5DF2\u6E05\u9664\u5C40\u57DF\u7F51\u4E3B\u673A\u914D\u7F6E\u3002",
339
+ "config.reported": "\u5DF2\u628A\u6700\u65B0\u5C40\u57DF\u7F51\u5730\u5740\u66F4\u65B0\u5230 HermesPilot Server\u3002",
340
+ "config.reportSkippedUnpaired": "Hermes Link \u8FD8\u6CA1\u6709\u914D\u5BF9\uFF0C\u5C40\u57DF\u7F51\u5730\u5740\u4F1A\u5728\u914D\u5BF9\u540E\u4E0A\u62A5\u3002",
306
341
  "daemon.description": "\u4EE5\u524D\u53F0\u65B9\u5F0F\u8FD0\u884C Hermes Link",
307
342
  "daemon.foreground": "Hermes Link \u524D\u53F0\u670D\u52A1\u6B63\u5728\u8FD0\u884C\u3002\u6309 Ctrl+C \u505C\u6B62\u3002",
308
343
  "logs.description": "\u663E\u793A Hermes Link \u65E5\u5FD7\u8DEF\u5F84",
@@ -329,9 +364,13 @@ var messages = {
329
364
  "pair.code": "\u914D\u5BF9\u7801\uFF1A{value}",
330
365
  "pair.localApi": "\u672C\u5730 API\uFF1Ahttp://127.0.0.1:{port}",
331
366
  "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",
367
+ "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}",
332
368
  "pair.expires": "\u914D\u5BF9\u4F1A\u8BDD 10 \u5206\u949F\u540E\u8FC7\u671F\u3002\u6309 Ctrl+C \u9000\u51FA\u7B49\u5F85\u3002",
333
369
  "pair.claimed": "\u914D\u5BF9\u5DF2\u6210\u529F\u3002\u6B63\u5728\u628A Hermes Link \u5207\u6362\u5230\u540E\u53F0\u8FD0\u884C...",
334
370
  "pair.claimedRunning": "\u914D\u5BF9\u5DF2\u6210\u529F\u3002Hermes Link \u5DF2\u5728\u540E\u53F0\u6301\u7EED\u8FD0\u884C\u3002",
371
+ "pair.relayOnlyNotice": "\u7F51\u7EDC\u63D0\u793A\uFF1A\u5F53\u524D\u662F {kind} \u73AF\u5883\uFF0C\u9ED8\u8BA4\u4E0D\u4F1A\u66B4\u9732\u624B\u673A\u53EF\u8BBF\u95EE\u7684\u5C40\u57DF\u7F51\u6216\u516C\u7F51\u76F4\u8FDE\u5730\u5740\u3002App \u4F1A\u901A\u8FC7 Relay \u8FDE\u63A5\u3002",
372
+ "pair.relayOnlyLanHostHint": "\u5982\u679C\u4F60\u5DF2\u7ECF\u5728 Windows \u6216\u8DEF\u7531\u5668\u4FA7\u624B\u52A8\u628A\u8FD9\u4E2A Link \u66B4\u9732\u5230\u5C40\u57DF\u7F51\uFF0C\u53EF\u4EE5\u8FD0\u884C `hermeslink config set lan-host <Windows \u5C40\u57DF\u7F51 IP>` \u66F4\u65B0\u53EF\u8BBF\u95EE\u5730\u5740\u3002",
373
+ "pair.relayOnlySafetyHint": "Hermes Link \u4E0D\u4F1A\u81EA\u52A8\u4FEE\u6539 Windows/WSL \u6865\u63A5\u3001\u9632\u706B\u5899\u6216\u7AEF\u53E3\u4EE3\u7406\u914D\u7F6E\uFF0C\u56E0\u4E3A\u8FD9\u4E9B\u5C5E\u4E8E\u7CFB\u7EDF\u7EA7\u7F51\u7EDC\u66B4\u9732\u8BBE\u7F6E\u3002",
335
374
  "pair.autostartUnchanged": "\u68C0\u6D4B\u5230\u5DF2\u6709\u914D\u5BF9\u8BBE\u5907\uFF0C\u5F00\u673A\u81EA\u542F\u8BBE\u7F6E\u4FDD\u6301\u4E0D\u53D8\u3002",
336
375
  "pair.autostartFailed": "\u914D\u5BF9\u5DF2\u6210\u529F\uFF0C\u4F46\u542F\u7528\u5F00\u673A\u81EA\u542F\u5931\u8D25\uFF1A{message}",
337
376
  "doctor.description": "\u8FD0\u884C\u672C\u673A\u8BCA\u65AD",
@@ -339,6 +378,8 @@ var messages = {
339
378
  "doctor.installId": "Install ID\uFF1A{value}",
340
379
  "doctor.linkId": "Link ID\uFF1A{value}",
341
380
  "doctor.notAssigned": "\u5C1A\u672A\u5206\u914D",
381
+ "doctor.lanHost": "\u5DF2\u914D\u7F6E\u5C40\u57DF\u7F51\u4E3B\u673A\uFF1A{value}",
382
+ "doctor.networkWarning": "\u7F51\u7EDC\u63D0\u793A\uFF1A{message}",
342
383
  "doctor.apiReady": "Hermes API Server\uFF1A\u5DF2\u5C31\u7EEA",
343
384
  "doctor.apiStarted": "Hermes API Server\uFF1A\u5DF2\u81EA\u52A8\u542F\u52A8\u5E76\u5C31\u7EEA",
344
385
  "doctor.apiUnavailable": "Hermes API Server\uFF1A\u4E0D\u53EF\u7528\u3002{message}",
@@ -570,16 +611,29 @@ async function openSystemBrowser(url) {
570
611
  return await spawnDetached("xdg-open", [url]);
571
612
  }
572
613
  async function spawnDetached(command, args) {
573
- try {
574
- const child = spawn(command, args, {
575
- detached: true,
576
- stdio: "ignore"
577
- });
578
- child.unref();
579
- return true;
580
- } catch {
581
- return false;
582
- }
614
+ return await new Promise((resolve) => {
615
+ let settled = false;
616
+ const settle = (ok) => {
617
+ if (settled) {
618
+ return;
619
+ }
620
+ settled = true;
621
+ resolve(ok);
622
+ };
623
+ try {
624
+ const child = spawn(command, args, {
625
+ detached: true,
626
+ stdio: "ignore"
627
+ });
628
+ child.once("error", () => settle(false));
629
+ child.once("spawn", () => {
630
+ child.unref();
631
+ settle(true);
632
+ });
633
+ } catch {
634
+ settle(false);
635
+ }
636
+ });
583
637
  }
584
638
 
585
639
  // src/cli/index.ts
@@ -598,6 +652,8 @@ program.command("status").option("--json", helpText("status.json")).description(
598
652
  paired: Boolean(identity?.link_id),
599
653
  mode: identity?.link_id ? "paired" : "local-only",
600
654
  port: config.port,
655
+ lanHost: config.lanHost,
656
+ environment: detectRuntimeEnvironment(),
601
657
  identity: identity ? getIdentityStatus(identity) : null,
602
658
  relay: {
603
659
  configured: Boolean(config.relayBaseUrl),
@@ -612,8 +668,43 @@ program.command("status").option("--json", helpText("status.json")).description(
612
668
  console.log(t("status.runtime", { value: payload.runtimeHome }));
613
669
  console.log(t("status.mode", { value: payload.mode }));
614
670
  console.log(t("status.port", { value: payload.port }));
671
+ console.log(t("status.lanHost", { value: payload.lanHost ?? t("status.notSet") }));
672
+ if (payload.environment.warning) {
673
+ console.log(t("status.environmentWarning", { message: payload.environment.warning }));
674
+ }
615
675
  console.log(t("status.linkId", { value: payload.identity?.linkId ?? t("status.notPaired") }));
616
676
  });
677
+ var configCommand = program.command("config").description(helpText("config.description"));
678
+ configCommand.command("set").argument("<key>").argument("<value>").description(helpText("config.set.description")).action(async (key, value) => {
679
+ const paths = resolveRuntimePaths();
680
+ const current = await loadConfig(paths);
681
+ const language = resolveLanguage(current.language);
682
+ const t = translate.bind(null, language);
683
+ const normalizedKey = key.trim().toLowerCase();
684
+ if (normalizedKey !== "lan-host") {
685
+ throw new Error(t("config.unknownKey", { key }));
686
+ }
687
+ const lanHost = normalizeLanHost(value);
688
+ if (!lanHost) {
689
+ throw new Error(t("config.lanHostInvalid"));
690
+ }
691
+ const next = await saveConfig({ lanHost }, paths);
692
+ console.log(t("config.lanHostSet", { value: next.lanHost ?? lanHost }));
693
+ await reportConfigNetworkUpdate(paths, t);
694
+ });
695
+ configCommand.command("unset").argument("<key>").description(helpText("config.unset.description")).action(async (key) => {
696
+ const paths = resolveRuntimePaths();
697
+ const current = await loadConfig(paths);
698
+ const language = resolveLanguage(current.language);
699
+ const t = translate.bind(null, language);
700
+ const normalizedKey = key.trim().toLowerCase();
701
+ if (normalizedKey !== "lan-host") {
702
+ throw new Error(t("config.unknownKey", { key }));
703
+ }
704
+ await saveConfig({ lanHost: null }, paths);
705
+ console.log(t("config.lanHostUnset"));
706
+ await reportConfigNetworkUpdate(paths, t);
707
+ });
617
708
  program.command("start").description(helpText("start.description")).action(async () => {
618
709
  const [config, status] = await Promise.all([loadConfig(), getDaemonStatus()]);
619
710
  const language = resolveLanguage(config.language);
@@ -661,11 +752,17 @@ program.command("daemon-supervisor", { hidden: true }).action(async () => {
661
752
  });
662
753
  program.command("pair").description(helpText("pair.description")).action(async () => {
663
754
  const paths = resolveRuntimePaths();
664
- const config = await loadConfig(paths);
665
- const language = resolveLanguage(config.language);
755
+ let config = await loadConfig(paths);
756
+ const languageChoice = await resolvePairingLanguage(paths, config);
757
+ config = languageChoice.config;
758
+ const language = languageChoice.language;
666
759
  const t = translate.bind(null, language);
667
760
  console.log(t("pair.preflight"));
668
761
  const preflight = await assertPairingPreflightReady({ paths });
762
+ const environment = detectRuntimeEnvironment();
763
+ if (environment.warning) {
764
+ console.log(t("doctor.networkWarning", { message: environment.warning }));
765
+ }
669
766
  console.log(t("pair.hermesHome", { path: preflight.hermesHome }));
670
767
  console.log(t("pair.apiReady", { port: preflight.apiServer.port ?? "unknown" }));
671
768
  console.log(t("pair.preparing"));
@@ -695,7 +792,10 @@ program.command("pair").description(helpText("pair.description")).action(async (
695
792
  }
696
793
  console.log(t("pair.scan"));
697
794
  console.log(`Pairing page: ${pairingPageUrl}`);
698
- void openSystemBrowser(pairingPageUrl);
795
+ const browserOpened = await openSystemBrowser(pairingPageUrl);
796
+ if (!browserOpened) {
797
+ console.log(t("pair.openBrowserFailed", { url: pairingPageUrl }));
798
+ }
699
799
  qrcode.generate(qrValue, { small: true });
700
800
  console.log(t("pair.expires"));
701
801
  const result = await waitForPairingOrShutdown(prepared.sessionId, paths);
@@ -705,6 +805,7 @@ program.command("pair").description(helpText("pair.description")).action(async (
705
805
  if (result === "claimed") {
706
806
  await clearPairingClaim(prepared.sessionId, paths);
707
807
  console.log(t(reusedRunningService ? "pair.claimedRunning" : "pair.claimed"));
808
+ printPostPairingNetworkNotice(prepared.routes.environment, config, t);
708
809
  try {
709
810
  if (hadActiveDevices) {
710
811
  console.log(t("pair.autostartUnchanged"));
@@ -789,6 +890,11 @@ program.command("doctor").description(helpText("doctor.description")).action(asy
789
890
  console.log(t("doctor.identityOk"));
790
891
  console.log(t("doctor.installId", { value: identity.install_id }));
791
892
  console.log(t("doctor.linkId", { value: identity.link_id ?? t("doctor.notAssigned") }));
893
+ const environment = detectRuntimeEnvironment();
894
+ if (environment.warning) {
895
+ console.log(t("doctor.networkWarning", { message: environment.warning }));
896
+ }
897
+ console.log(t("doctor.lanHost", { value: config.lanHost ?? t("status.notSet") }));
792
898
  if (hermesConfig.notice) {
793
899
  console.log(hermesConfig.notice);
794
900
  if (hermesConfig.backupPath) {
@@ -811,6 +917,48 @@ async function loadCliLanguage() {
811
917
  const config = await loadConfig();
812
918
  return resolveLanguage(config.language);
813
919
  }
920
+ async function resolvePairingLanguage(paths, config) {
921
+ if (config.language !== "auto") {
922
+ return { config, language: resolveLanguage(config.language) };
923
+ }
924
+ const selected = await promptForLanguage();
925
+ if (!selected) {
926
+ return { config, language: resolveLanguage(config.language) };
927
+ }
928
+ const next = await saveConfig({ language: selected }, paths);
929
+ return { config: next, language: selected };
930
+ }
931
+ async function promptForLanguage() {
932
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
933
+ return null;
934
+ }
935
+ const rl = createInterface({
936
+ input: process.stdin,
937
+ output: process.stdout
938
+ });
939
+ try {
940
+ for (; ; ) {
941
+ const answer = await rl.question(
942
+ [
943
+ "\u8BF7\u9009\u62E9 Hermes Link \u663E\u793A\u8BED\u8A00 / Choose Hermes Link language:",
944
+ " 1. \u4E2D\u6587",
945
+ " 2. English",
946
+ "\u8F93\u5165 1 \u6216 2 \u540E\u56DE\u8F66 / Enter 1 or 2: "
947
+ ].join("\n")
948
+ );
949
+ const normalized = answer.trim().toLowerCase();
950
+ if (normalized === "1" || normalized === "zh" || normalized === "zh-cn" || normalized === "\u4E2D\u6587") {
951
+ return "zh-CN";
952
+ }
953
+ if (normalized === "2" || normalized === "en" || normalized === "english") {
954
+ return "en";
955
+ }
956
+ console.log("\u8BF7\u8F93\u5165 1 \u6216 2\u3002 / Please enter 1 or 2.");
957
+ }
958
+ } finally {
959
+ rl.close();
960
+ }
961
+ }
814
962
  async function waitForShutdown(cleanup) {
815
963
  await new Promise((resolve) => {
816
964
  const stop = () => resolve();
@@ -843,3 +991,20 @@ async function waitForPairingOrShutdown(sessionId, paths) {
843
991
  function sleep(ms) {
844
992
  return new Promise((resolve) => setTimeout(resolve, ms));
845
993
  }
994
+ async function reportConfigNetworkUpdate(paths, t) {
995
+ const identity = await loadIdentity(paths);
996
+ if (!identity?.link_id) {
997
+ console.log(t("config.reportSkippedUnpaired"));
998
+ return;
999
+ }
1000
+ await reportLinkStatusToServer({ paths });
1001
+ console.log(t("config.reported"));
1002
+ }
1003
+ function printPostPairingNetworkNotice(environment, config, t) {
1004
+ if (environment.lanAutoDiscoveryUsable || config.lanHost) {
1005
+ return;
1006
+ }
1007
+ console.log(t("pair.relayOnlyNotice", { kind: environment.kind }));
1008
+ console.log(t("pair.relayOnlyLanHostHint"));
1009
+ console.log(t("pair.relayOnlySafetyHint"));
1010
+ }
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-SSJ7WDD7.js";
3
+ } from "../chunk-VLTX75SY.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.2.7",
3
+ "version": "0.2.9",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",