@hermespilot/link 0.2.8 → 0.3.0

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.
@@ -3724,7 +3724,7 @@ import os2 from "os";
3724
3724
  import path5 from "path";
3725
3725
 
3726
3726
  // src/constants.ts
3727
- var LINK_VERSION = "0.2.8";
3727
+ var LINK_VERSION = "0.3.0";
3728
3728
  var LINK_COMMAND = "hermeslink";
3729
3729
  var LINK_DEFAULT_PORT = 52379;
3730
3730
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -19122,35 +19122,49 @@ async function preparePairing(paths = resolveRuntimePaths()) {
19122
19122
  public_key_pem: identity.public_key_pem
19123
19123
  });
19124
19124
  const relayBaseUrl = created.relayBaseUrl || config.relayBaseUrl;
19125
- const assigned = await bootstrapRelayLink({
19126
- relayBaseUrl,
19127
- relayBootstrapToken: created.relayBootstrapToken,
19128
- identity,
19129
- paths
19130
- });
19131
- const updatedIdentity = await loadIdentity(paths) ?? identity;
19132
- const routes = await discoverRouteCandidates({
19133
- port: config.port,
19134
- relayBaseUrl,
19135
- relayBootstrapToken: created.relayBootstrapToken,
19136
- configuredLanHost: config.lanHost,
19137
- linkId: assigned.linkId,
19138
- installId: updatedIdentity.install_id,
19139
- publicKeyPem: updatedIdentity.public_key_pem
19140
- });
19141
- await patchServerJson(config.serverBaseUrl, `/api/v1/link-pairings/${created.sessionId}/link`, created.pairingToken, {
19142
- install_id: updatedIdentity.install_id,
19143
- link_id: assigned.linkId,
19144
- link_version: LINK_VERSION,
19145
- display_name: systemInfo.defaultDisplayName,
19146
- platform: systemInfo.platform,
19147
- hostname: systemInfo.hostname ?? void 0,
19148
- lan_ips: routes.lanIps,
19149
- public_ipv4s: routes.publicIpv4s,
19150
- public_ipv6s: routes.publicIpv6s,
19151
- preferred_urls: routes.preferredUrls,
19152
- environment: routes.environment
19153
- });
19125
+ let assigned;
19126
+ let updatedIdentity;
19127
+ let routes;
19128
+ try {
19129
+ assigned = await bootstrapRelayLink({
19130
+ relayBaseUrl,
19131
+ relayBootstrapToken: created.relayBootstrapToken,
19132
+ identity,
19133
+ paths
19134
+ });
19135
+ updatedIdentity = await loadIdentity(paths) ?? identity;
19136
+ routes = await discoverRouteCandidates({
19137
+ port: config.port,
19138
+ relayBaseUrl,
19139
+ relayBootstrapToken: created.relayBootstrapToken,
19140
+ configuredLanHost: config.lanHost,
19141
+ linkId: assigned.linkId,
19142
+ installId: updatedIdentity.install_id,
19143
+ publicKeyPem: updatedIdentity.public_key_pem
19144
+ });
19145
+ await patchServerJson(config.serverBaseUrl, `/api/v1/link-pairings/${created.sessionId}/link`, created.pairingToken, {
19146
+ install_id: updatedIdentity.install_id,
19147
+ link_id: assigned.linkId,
19148
+ link_version: LINK_VERSION,
19149
+ display_name: systemInfo.defaultDisplayName,
19150
+ platform: systemInfo.platform,
19151
+ hostname: systemInfo.hostname ?? void 0,
19152
+ lan_ips: routes.lanIps,
19153
+ public_ipv4s: routes.publicIpv4s,
19154
+ public_ipv6s: routes.publicIpv6s,
19155
+ preferred_urls: routes.preferredUrls,
19156
+ environment: routes.environment
19157
+ });
19158
+ } catch (error) {
19159
+ await reportPairingErrorToServer({
19160
+ serverBaseUrl: config.serverBaseUrl,
19161
+ sessionId: created.sessionId,
19162
+ source: "link",
19163
+ pairingToken: created.pairingToken,
19164
+ error: pairingErrorSnapshot("prepare_pairing", error)
19165
+ });
19166
+ throw error;
19167
+ }
19154
19168
  const qrPayload = {
19155
19169
  kind: "hermes_link_pairing",
19156
19170
  version: 1,
@@ -19241,14 +19255,26 @@ async function clearPairingClaim(sessionId, paths = resolveRuntimePaths()) {
19241
19255
  async function claimPairing(input) {
19242
19256
  const paths = input.paths ?? resolveRuntimePaths();
19243
19257
  const [identity, config] = await Promise.all([loadRequiredIdentity2(paths), loadConfig(paths)]);
19244
- const verified = await postServerJson(
19245
- config.serverBaseUrl,
19246
- `/api/v1/link-pairings/${input.sessionId}/claim/verify`,
19247
- {
19248
- claim_token: input.claimToken,
19249
- app_instance_id: input.appInstanceId ?? void 0
19250
- }
19251
- );
19258
+ let verified;
19259
+ try {
19260
+ verified = await postServerJson(
19261
+ config.serverBaseUrl,
19262
+ `/api/v1/link-pairings/${input.sessionId}/claim/verify`,
19263
+ {
19264
+ claim_token: input.claimToken,
19265
+ app_instance_id: input.appInstanceId ?? void 0
19266
+ }
19267
+ );
19268
+ } catch (error) {
19269
+ await reportPairingErrorToServer({
19270
+ serverBaseUrl: config.serverBaseUrl,
19271
+ sessionId: input.sessionId,
19272
+ source: "link",
19273
+ claimToken: input.claimToken,
19274
+ error: pairingErrorSnapshot("claim_verify", error)
19275
+ });
19276
+ throw error;
19277
+ }
19252
19278
  if (verified.ok !== true || verified.linkId !== identity.link_id) {
19253
19279
  throw new LinkHttpError(409, "pairing_claim_mismatch", "Pairing claim does not match this Link");
19254
19280
  }
@@ -19295,6 +19321,36 @@ async function postServerJson(serverBaseUrl, path24, body) {
19295
19321
  });
19296
19322
  return readJsonResponse2(response);
19297
19323
  }
19324
+ async function reportPairingErrorToServer(input) {
19325
+ const body = {
19326
+ source: input.source,
19327
+ error: input.error
19328
+ };
19329
+ if (input.claimToken) {
19330
+ body.claim_token = input.claimToken;
19331
+ }
19332
+ if (input.pairingToken) {
19333
+ body.pairing_token = input.pairingToken;
19334
+ }
19335
+ await fetch(`${input.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/link-pairings/${encodeURIComponent(input.sessionId)}/error`, {
19336
+ method: "POST",
19337
+ headers: {
19338
+ accept: "application/json",
19339
+ "content-type": "application/json",
19340
+ ...input.pairingToken ? { authorization: `Bearer ${input.pairingToken}` } : {}
19341
+ },
19342
+ signal: AbortSignal.timeout(3e3),
19343
+ body: JSON.stringify(body)
19344
+ }).catch(() => void 0);
19345
+ }
19346
+ function pairingErrorSnapshot(stage, error) {
19347
+ return {
19348
+ stage,
19349
+ message: error instanceof Error ? error.message : String(error),
19350
+ error_name: error instanceof Error ? error.name : void 0,
19351
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString()
19352
+ };
19353
+ }
19298
19354
  async function patchServerJson(serverBaseUrl, path24, token, body) {
19299
19355
  const response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path24}`, {
19300
19356
  method: "PATCH",
@@ -20436,6 +20492,7 @@ export {
20436
20492
  resolveRuntimePaths,
20437
20493
  getLinkLogFile,
20438
20494
  ensureHermesApiServerAvailable,
20495
+ readHermesVersion,
20439
20496
  loadConfig,
20440
20497
  saveConfig,
20441
20498
  normalizeLanHost,
package/dist/cli/index.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  preparePairing,
21
21
  probeLocalLinkService,
22
22
  readHermesApiServerConfig,
23
+ readHermesVersion,
23
24
  readPairingClaim,
24
25
  reportLinkStatusToServer,
25
26
  resolveHermesConfigPath,
@@ -30,7 +31,7 @@ import {
30
31
  startDaemonProcess,
31
32
  startLinkService,
32
33
  stopDaemonProcess
33
- } from "../chunk-W4U6XJ4W.js";
34
+ } from "../chunk-45RR27KS.js";
34
35
 
35
36
  // src/cli/index.ts
36
37
  import { Command } from "commander";
@@ -277,6 +278,7 @@ var messages = {
277
278
  "pair.code": "Pairing code: {value}",
278
279
  "pair.localApi": "Local API: http://127.0.0.1:{port}",
279
280
  "pair.scan": "Open this pairing page in the HermesPilot App or scan the QR code below:",
281
+ "pair.openBrowserFailed": "Could not open the system browser automatically. You can still scan the QR code below or open this URL manually: {url}",
280
282
  "pair.expires": "Pairing expires in 10 minutes. Press Ctrl+C to cancel waiting.",
281
283
  "pair.claimed": "Pairing succeeded. Starting Hermes Link in the background...",
282
284
  "pair.claimedRunning": "Pairing succeeded. Hermes Link is already running in the background.",
@@ -363,6 +365,7 @@ var messages = {
363
365
  "pair.code": "\u914D\u5BF9\u7801\uFF1A{value}",
364
366
  "pair.localApi": "\u672C\u5730 API\uFF1Ahttp://127.0.0.1:{port}",
365
367
  "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",
368
+ "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}",
366
369
  "pair.expires": "\u914D\u5BF9\u4F1A\u8BDD 10 \u5206\u949F\u540E\u8FC7\u671F\u3002\u6309 Ctrl+C \u9000\u51FA\u7B49\u5F85\u3002",
367
370
  "pair.claimed": "\u914D\u5BF9\u5DF2\u6210\u529F\u3002\u6B63\u5728\u628A Hermes Link \u5207\u6362\u5230\u540E\u53F0\u8FD0\u884C...",
368
371
  "pair.claimedRunning": "\u914D\u5BF9\u5DF2\u6210\u529F\u3002Hermes Link \u5DF2\u5728\u540E\u53F0\u6301\u7EED\u8FD0\u884C\u3002",
@@ -507,6 +510,21 @@ async function assertPairingPreflightReady(options = {}) {
507
510
  if (failures.length > 0) {
508
511
  throwPairingPreflightError(failures);
509
512
  }
513
+ const readVersion = options.readHermesVersion ?? readHermesVersion;
514
+ try {
515
+ await readVersion();
516
+ } catch (error) {
517
+ throwPairingPreflightError([
518
+ {
519
+ code: "hermes_cli_unavailable",
520
+ zh: "\u6CA1\u6709\u627E\u5230\u53EF\u7528\u7684 Hermes Agent CLI\u3002Link \u9700\u8981\u548C Hermes \u5728\u540C\u4E00\u4E2A\u7CFB\u7EDF\u73AF\u5883\u91CC\uFF0C\u624D\u80FD\u5728\u914D\u5BF9\u65F6\u542F\u52A8\u548C\u68C0\u6D4B Hermes Gateway\u3002",
521
+ en: "Hermes Agent CLI was not found. Link needs Hermes to be available in the same system environment so it can start and check Hermes Gateway during pairing.",
522
+ actionZh: "\u8BF7\u628A Hermes Agent \u5B89\u88C5\u5728\u8FD9\u53F0\u5BBF\u4E3B\u673A\u4E0A\uFF0C\u6216\u628A `HERMES_BIN` \u6307\u5411 Link \u53EF\u4EE5\u76F4\u63A5\u6267\u884C\u7684 Hermes CLI\u3002\u5F53\u524D\u5982\u679C Hermes \u53EA\u5728\u53E6\u4E00\u4E2A Docker\u3001WSL \u6216\u865A\u62DF\u673A\u73AF\u5883\u91CC\uFF0C\u5BBF\u4E3B\u673A Link \u4E0D\u80FD\u76F4\u63A5\u7528\u5B83\u5B8C\u6210\u914D\u5BF9\u524D\u68C0\u67E5\u3002",
523
+ actionEn: "Install Hermes Agent on this host, or point `HERMES_BIN` to a Hermes CLI that Link can run directly in this system environment. If Hermes only exists inside another Docker, WSL, or VM environment, host Link cannot use it for pairing preflight.",
524
+ detail: error instanceof Error ? error.message : String(error)
525
+ }
526
+ ]);
527
+ }
510
528
  const apiServerConfig = await readHermesApiServerConfig(profileName, configPath);
511
529
  if (apiServerConfig.enabled !== true) {
512
530
  throwPairingPreflightError([
@@ -609,16 +627,29 @@ async function openSystemBrowser(url) {
609
627
  return await spawnDetached("xdg-open", [url]);
610
628
  }
611
629
  async function spawnDetached(command, args) {
612
- try {
613
- const child = spawn(command, args, {
614
- detached: true,
615
- stdio: "ignore"
616
- });
617
- child.unref();
618
- return true;
619
- } catch {
620
- return false;
621
- }
630
+ return await new Promise((resolve) => {
631
+ let settled = false;
632
+ const settle = (ok) => {
633
+ if (settled) {
634
+ return;
635
+ }
636
+ settled = true;
637
+ resolve(ok);
638
+ };
639
+ try {
640
+ const child = spawn(command, args, {
641
+ detached: true,
642
+ stdio: "ignore"
643
+ });
644
+ child.once("error", () => settle(false));
645
+ child.once("spawn", () => {
646
+ child.unref();
647
+ settle(true);
648
+ });
649
+ } catch {
650
+ settle(false);
651
+ }
652
+ });
622
653
  }
623
654
 
624
655
  // src/cli/index.ts
@@ -777,7 +808,10 @@ program.command("pair").description(helpText("pair.description")).action(async (
777
808
  }
778
809
  console.log(t("pair.scan"));
779
810
  console.log(`Pairing page: ${pairingPageUrl}`);
780
- void openSystemBrowser(pairingPageUrl);
811
+ const browserOpened = await openSystemBrowser(pairingPageUrl);
812
+ if (!browserOpened) {
813
+ console.log(t("pair.openBrowserFailed", { url: pairingPageUrl }));
814
+ }
781
815
  qrcode.generate(qrValue, { small: true });
782
816
  console.log(t("pair.expires"));
783
817
  const result = await waitForPairingOrShutdown(prepared.sessionId, paths);
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-W4U6XJ4W.js";
3
+ } from "../chunk-45RR27KS.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.8",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Hermes Link companion service and CLI for connecting hermes-agent through HermesPilot",
6
6
  "license": "MIT",