@bulolo/hermes-link 0.2.9 → 0.3.1

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
@@ -3,7 +3,6 @@ import {
3
3
  DAEMON_LOG_FILE,
4
4
  LINK_COMMAND,
5
5
  LINK_VERSION,
6
- bootstrapWithRelay,
7
6
  createRotatingTextLogWriter,
8
7
  currentCliScriptPath,
9
8
  detectRuntimeEnvironment,
@@ -19,11 +18,11 @@ import {
19
18
  readRecentGatewayLogEntries,
20
19
  readRecentLogEntries,
21
20
  resolveRuntimePaths,
22
- saveAssignedLinkId,
23
21
  saveConfig,
24
22
  startLinkService,
25
23
  writeJsonFile
26
- } from "../chunk-GHAP2EHA.js";
24
+ } from "../chunk-ZO2S4ZIO.js";
25
+ import "../chunk-NP3Y2NVF.js";
27
26
 
28
27
  // src/cli/index.ts
29
28
  import { mkdir as mkdir3 } from "fs/promises";
@@ -234,59 +233,49 @@ function pairingSessionPath(sessionId, paths) {
234
233
  }
235
234
  async function runPairingPreflight(options) {
236
235
  const token = await generateAppConnectToken(options.paths);
237
- const routes = options.identity.link_id ? await discoverRouteCandidates({
236
+ const routes = await discoverRouteCandidates({
238
237
  port: options.config.port,
239
- relayBaseUrl: options.config.relayBaseUrl,
240
- linkId: options.identity.link_id,
241
- installId: options.identity.install_id,
242
- publicKeyPem: options.identity.public_key_pem,
243
238
  configuredLanHost: options.config.lanHost
244
- }).catch(() => null) : null;
239
+ }).catch(() => null);
245
240
  const localApiUrl = `http://127.0.0.1:${options.config.port}`;
246
- const preferredUrls = (routes?.preferredUrls ?? []).filter(
247
- (u) => !u.includes("/api/v1/relay/")
248
- );
249
- const bestUrl = preferredUrls[0] ?? localApiUrl;
241
+ const preferredUrls = (routes?.preferredUrls ?? []).length > 0 ? routes.preferredUrls : [localApiUrl];
250
242
  const sessionId = `ps_${token.token.slice(0, 16)}`;
251
243
  const session = {
252
244
  session_id: sessionId,
253
245
  code: token.token,
254
246
  link_id: options.identity.link_id ?? "",
255
247
  display_name: "Hermes Link",
256
- local_api_url: bestUrl,
257
- server_base_url: options.config.serverBaseUrl,
258
- relay_base_url: options.config.relayBaseUrl,
259
- preferred_urls: preferredUrls.length > 0 ? preferredUrls : [localApiUrl],
248
+ local_api_url: preferredUrls[0] ?? localApiUrl,
249
+ preferred_urls: preferredUrls,
260
250
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
261
251
  expires_at: token.expiresAt
262
252
  };
263
253
  await mkdir2(options.paths.pairingDir, { recursive: true, mode: 448 }).catch(() => void 0);
264
254
  await writeJsonFile(pairingSessionPath(sessionId, options.paths), session);
265
- const lanUrls = (routes?.lanIps ?? []).map((ip) => `http://${ip}:${options.config.port}`);
266
- const publicUrls = (routes?.publicIpv4s ?? []).map((ip) => `http://${ip}:${options.config.port}`);
267
- const pairingUrl = buildPairingUrl({
268
- linkId: options.identity.link_id ?? "",
269
- installId: options.identity.install_id,
270
- connectToken: token.token,
271
- port: options.config.port,
272
- lanUrls,
273
- publicUrls
274
- });
255
+ const qrPayload = {
256
+ kind: "hermes_link_pairing",
257
+ version: 1,
258
+ link_id: options.identity.link_id ?? "",
259
+ display_name: "Hermes Link",
260
+ session_id: sessionId,
261
+ code: token.token,
262
+ preferred_urls: preferredUrls
263
+ };
264
+ const pageUrl = buildLocalPairingPageUrl(options.config.port, sessionId, token.token);
275
265
  if (options.openBrowser !== false) {
276
- await openSystemBrowser(pairingUrl).catch(() => void 0);
266
+ await openSystemBrowser(pageUrl).catch(() => void 0);
277
267
  }
278
- return { pairingUrl, connectToken: token.token, bestUrl };
268
+ return {
269
+ qrPayload: JSON.stringify(qrPayload),
270
+ pageUrl,
271
+ connectToken: token.token,
272
+ sessionId,
273
+ preferredUrls
274
+ };
279
275
  }
280
- function buildPairingUrl(params) {
281
- const qs = new URLSearchParams({
282
- link_id: params.linkId,
283
- install_id: params.installId,
284
- connect_token: params.connectToken,
285
- port: String(params.port)
286
- });
287
- if (params.lanUrls.length > 0) qs.set("lan_urls", params.lanUrls.join(","));
288
- if (params.publicUrls.length > 0) qs.set("public_urls", params.publicUrls.join(","));
289
- return `hermesapp://pair?${qs.toString()}`;
276
+ function buildLocalPairingPageUrl(port, sessionId, connectToken) {
277
+ const qs = new URLSearchParams({ session_id: sessionId, connect_token: connectToken });
278
+ return `http://127.0.0.1:${port}/pair?${qs.toString()}`;
290
279
  }
291
280
 
292
281
  // src/cli/index.ts
@@ -403,24 +392,9 @@ async function cmdDaemon(paths) {
403
392
  process.exitCode = 1;
404
393
  return;
405
394
  }
406
- const config = await loadConfig(paths);
407
395
  const identity = await ensureIdentity(paths);
408
- let relayToken = "";
409
- try {
410
- const bootstrapResult = await bootstrapWithRelay({
411
- relayBaseUrl: config.relayBaseUrl,
412
- identity,
413
- port: config.port
414
- });
415
- if (!identity.link_id) {
416
- await saveAssignedLinkId(bootstrapResult.linkId, paths);
417
- }
418
- relayToken = bootstrapResult.token;
419
- } catch (err) {
420
- process.stderr.write(`Warning: Relay bootstrap failed: ${err.message}
421
- `);
422
- }
423
- const service = await startLinkService({ config, identity, paths, relayToken });
396
+ const config = await loadConfig(paths);
397
+ const service = await startLinkService({ config, identity, paths });
424
398
  process.stdout.write(`Hermes Link running on port ${config.port}
425
399
  `);
426
400
  const shutdown = async () => {
@@ -437,22 +411,24 @@ async function cmdDaemonSupervisor(paths) {
437
411
  async function cmdPair(paths) {
438
412
  const config = await loadConfig(paths);
439
413
  const identity = await ensureIdentity(paths);
440
- if (!identity.link_id) {
441
- process.stderr.write("Error: Hermes Link is not connected to relay. Run 'hermeslink start' first.\n");
442
- process.exitCode = 1;
443
- return;
444
- }
445
- const result = await runPairingPreflight({ identity, config, paths });
446
- const pageBase = result.bestUrl.replace(/\/+$/u, "");
447
- const pageUrl = `${pageBase}/pair?connect_token=${encodeURIComponent(result.connectToken)}`;
414
+ const result = await runPairingPreflight({ identity, config, paths, openBrowser: false });
448
415
  process.stdout.write("\n");
449
- qrcode.generate(result.pairingUrl, { small: true });
416
+ qrcode.generate(result.qrPayload, { small: true });
417
+ process.stdout.write(`
418
+ Pairing page: ${result.pageUrl}
419
+ `);
420
+ process.stdout.write(`Session ID: ${result.sessionId}
421
+ `);
422
+ process.stdout.write(`Connect token: ${result.connectToken}
423
+ `);
424
+ process.stdout.write(`Preferred URLs: ${result.preferredUrls.join(", ")}
425
+ `);
450
426
  process.stdout.write(`
451
- Pairing URL: ${result.pairingUrl}
427
+ App \u626B\u63CF\u4E8C\u7EF4\u7801\u540E\uFF0C\u8C03\u7528\u4EE5\u4E0B\u63A5\u53E3\u5B8C\u6210\u914D\u5BF9\uFF1A
452
428
  `);
453
- process.stdout.write(`Pairing page: ${pageUrl}
429
+ process.stdout.write(` POST ${result.preferredUrls[0]}/api/v1/pairing/claim
454
430
  `);
455
- process.stdout.write(`Connect token: ${result.connectToken}
431
+ process.stdout.write(` Body: { "session_id": "${result.sessionId}", "claim_token": "<code>" }
456
432
  `);
457
433
  }
458
434
  async function cmdConfig(paths) {
@@ -530,13 +506,13 @@ async function cmdLogs(paths) {
530
506
  }
531
507
  async function cmdAutostart(paths) {
532
508
  const subcommand = args[1];
533
- if (subcommand === "enable") {
509
+ if (subcommand === "enable" || subcommand === "on") {
534
510
  const status2 = await enableAutostart();
535
511
  process.stdout.write(`Autostart ${status2.enabled ? "enabled" : "could not be enabled"} (${status2.method})
536
512
  `);
537
513
  return;
538
514
  }
539
- if (subcommand === "disable") {
515
+ if (subcommand === "disable" || subcommand === "off") {
540
516
  const status2 = await disableAutostart();
541
517
  process.stdout.write(`Autostart ${status2.enabled ? "still enabled" : "disabled"} (${status2.method})
542
518
  `);
@@ -560,6 +536,7 @@ Commands:
560
536
  config get Show current configuration
561
537
  config set Set a configuration value
562
538
  autostart Show/enable/disable autostart
539
+ autostart on|off Enable or disable autostart
563
540
  logs Show recent log entries (--gateway for gateway logs)
564
541
  version Print version
565
542
 
@@ -0,0 +1,8 @@
1
+ import {
2
+ LinkHttpError,
3
+ isLinkHttpError
4
+ } from "./chunk-NP3Y2NVF.js";
5
+ export {
6
+ LinkHttpError,
7
+ isLinkHttpError
8
+ };
@@ -1,7 +1,6 @@
1
1
  import Koa from 'koa';
2
2
  import { Server } from 'http';
3
3
  import { z } from 'zod';
4
- import { EventEmitter } from 'events';
5
4
 
6
5
  interface RuntimePaths {
7
6
  homeDir: string;
@@ -23,10 +22,6 @@ type Language = "auto" | "en" | "zh-CN";
23
22
  interface LinkConfig {
24
23
  port: number;
25
24
  lanHost: string | null;
26
- serverBaseUrl: string;
27
- relayBaseUrl: string;
28
- appConnectTokenIssuer: string;
29
- appConnectTokenAudience: string;
30
25
  language: Language;
31
26
  logLevel: LogLevel;
32
27
  }
@@ -55,55 +50,14 @@ declare const linkIdentitySchema: z.ZodObject<{
55
50
  }>;
56
51
  type LinkIdentity = z.infer<typeof linkIdentitySchema>;
57
52
 
58
- type RelayClientState = "disconnected" | "connecting" | "connected" | "closing";
59
- interface RelayClientOptions {
60
- relayBaseUrl: string;
61
- identity: LinkIdentity;
62
- token: string;
63
- paths?: RuntimePaths;
64
- fetchImpl?: typeof fetch;
65
- reconnectDelayMs?: number;
66
- maxReconnectDelayMs?: number;
67
- pingIntervalMs?: number;
68
- }
69
- interface RelayMessage {
70
- type: string;
71
- [key: string]: unknown;
72
- }
73
- declare class RelayClient extends EventEmitter {
74
- private options;
75
- private ws;
76
- private state;
77
- private token;
78
- private logger;
79
- private reconnectTimer;
80
- private pingTimer;
81
- private reconnectDelay;
82
- private closed;
83
- constructor(options: RelayClientOptions);
84
- get currentState(): RelayClientState;
85
- start(): void;
86
- stop(): void;
87
- send(message: RelayMessage): void;
88
- private connect;
89
- private buildWsUrl;
90
- private scheduleReconnect;
91
- private maybeRefreshTokenAndReconnect;
92
- private startPing;
93
- private stopPing;
94
- private clearTimers;
95
- }
96
-
97
53
  interface LinkServiceOptions {
98
54
  config: LinkConfig;
99
55
  identity: LinkIdentity;
100
56
  paths: RuntimePaths;
101
- relayToken: string;
102
57
  }
103
58
  interface LinkService {
104
59
  app: Koa;
105
60
  server: Server;
106
- relayClient: RelayClient;
107
61
  stop(): Promise<void>;
108
62
  }
109
63
  declare function startLinkService(options: LinkServiceOptions): Promise<LinkService>;
package/dist/http/app.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  startLinkService
3
- } from "../chunk-GHAP2EHA.js";
3
+ } from "../chunk-ZO2S4ZIO.js";
4
+ import "../chunk-NP3Y2NVF.js";
4
5
  export {
5
6
  startLinkService
6
7
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bulolo/hermes-link",
3
- "version": "0.2.9",
4
- "description": "Hermes Link companion service and CLI for connecting hermes-agent through zhiji",
3
+ "version": "0.3.1",
4
+ "description": "Local companion service and CLI for Hermes Agent, enabling mobile and LAN access",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -17,9 +17,10 @@
17
17
  "keywords": [
18
18
  "hermes",
19
19
  "hermes-agent",
20
- "relay",
21
20
  "link",
22
- "cli"
21
+ "cli",
22
+ "local",
23
+ "lan"
23
24
  ],
24
25
  "publishConfig": {
25
26
  "access": "public"
@@ -31,13 +32,20 @@
31
32
  "build": "tsup src/cli/index.ts src/http/app.ts --format esm --target node20 --dts --clean",
32
33
  "check": "tsc --noEmit",
33
34
  "dev": "tsx src/cli/index.ts",
35
+ "dev:run": "npm run build && node ./dist/cli/index.js daemon --foreground",
36
+ "dev:watch": "tsup src/cli/index.ts src/http/app.ts --format esm --target node20 --watch",
34
37
  "preinstall": "node ./scripts/check-node-version.mjs",
35
38
  "postinstall": "node ./scripts/postinstall.mjs",
36
39
  "prepack": "npm run build",
37
40
  "start": "node ./dist/cli/index.js",
38
- "test": "vitest",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "test:e2e": "vitest run tests/e2e",
39
44
  "publish:npm": "npm publish --access public --registry https://registry.npmjs.org"
40
45
  },
46
+ "overrides": {
47
+ "generator-function": "1.x"
48
+ },
41
49
  "dependencies": {
42
50
  "@koa/cors": "^5.0.0",
43
51
  "@koa/router": "^15.4.0",
@@ -66,4 +74,4 @@
66
74
  "typescript": "^5.7.2",
67
75
  "vitest": "^2.1.8"
68
76
  }
69
- }
77
+ }