@hermespilot/link 0.6.0 → 0.6.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
@@ -13,9 +13,12 @@ import {
13
13
  detectRuntimeEnvironment,
14
14
  ensureHermesApiServerAvailable,
15
15
  ensureHermesApiServerConfig,
16
+ ensureHermesLinkSkillInstalledBestEffort,
16
17
  ensureIdentity,
17
18
  fetchRelayStreamBatchPolicy,
19
+ flushLogFiles,
18
20
  getDaemonStatus,
21
+ getGatewayLogFiles,
19
22
  getIdentityStatus,
20
23
  getLinkLogFile,
21
24
  hasActiveDevices,
@@ -28,6 +31,10 @@ import {
28
31
  readHermesApiServerConfig,
29
32
  readHermesVersion,
30
33
  readPairingClaim,
34
+ readRecentGatewayLogEntries,
35
+ readRecentLogEntries,
36
+ readRecentTextLogEntries,
37
+ readRelayStatusSnapshot,
31
38
  reportLinkStatusToServer,
32
39
  resolveHermesConfigPath,
33
40
  resolveHermesProfileDir,
@@ -37,11 +44,12 @@ import {
37
44
  startDaemonProcess,
38
45
  startLinkService,
39
46
  stopDaemonProcess
40
- } from "../chunk-UHYO4EJD.js";
47
+ } from "../chunk-RBMFF32Z.js";
41
48
 
42
49
  // src/cli/index.ts
43
50
  import { Command } from "commander";
44
- import { realpathSync as realpathSync2 } from "fs";
51
+ import { realpathSync as realpathSync2, unwatchFile, watchFile } from "fs";
52
+ import { open, readFile as readFile2, stat as stat2 } from "fs/promises";
45
53
  import path4 from "path";
46
54
  import { createInterface } from "readline/promises";
47
55
  import { pathToFileURL } from "url";
@@ -298,12 +306,41 @@ var messages = {
298
306
  "status.json": "print machine-readable status",
299
307
  "status.runtime": "Runtime: {value}",
300
308
  "status.mode": "Mode: {value}",
309
+ "status.daemon": "Service: {value}",
310
+ "status.daemon.runningReachable": "running (PID {pid}, local API reachable)",
311
+ "status.daemon.runningUnreachable": "running (PID {pid}, local API is not responding)",
312
+ "status.daemon.notRunning": "not running",
301
313
  "status.port": "Local port: {value}",
302
- "status.lanHost": "Configured LAN host: {value}",
314
+ "status.lanHost": "LAN access address (manual override): {value}",
303
315
  "status.notSet": "not set",
316
+ "status.unknown": "unknown",
304
317
  "status.environmentWarning": "Network note: {message}",
305
318
  "status.linkId": "Link ID: {value}",
306
319
  "status.notPaired": "not paired",
320
+ "status.relay": "Relay: {value}",
321
+ "status.relay.notPaired": "not connected (not paired yet; Relay is disabled until pairing)",
322
+ "status.relay.notConfigured": "not configured",
323
+ "status.relay.serviceStopped": "not connected (background service is not running)",
324
+ "status.relay.serviceUnreachable": "unknown (background service is running but local API is not responding)",
325
+ "status.relay.unknown": "unknown (background service has not reported Relay state yet)",
326
+ "status.relay.state.connecting": "connecting",
327
+ "status.relay.state.connected": "connected",
328
+ "status.relay.state.disconnected": "disconnected",
329
+ "status.relay.state.retrying": "retrying",
330
+ "status.relay.state.cooldown": "cooling down",
331
+ "status.relay.state.failed": "failed",
332
+ "status.relay.stateWithAttempt": "{state}, attempt {attempt}",
333
+ "status.relay.updatedAt": "updated {time}",
334
+ "status.relay.withMessage": "{state} - {message}",
335
+ "status.relay.withUpdatedAt": "{state}; {updatedAt}",
336
+ "status.relay.withMessageAndUpdatedAt": "{state} - {message}; {updatedAt}",
337
+ "status.recentError": "Latest error: {value}",
338
+ "status.recentError.none": "none",
339
+ "status.recentError.format": "{time} [{source}] {message}",
340
+ "status.logSource.service": "service",
341
+ "status.logSource.daemon": "daemon",
342
+ "status.logSource.gateway": "gateway",
343
+ "status.timeUnknown": "time unknown",
307
344
  "start.description": "Start Hermes Link daemon",
308
345
  "start.backgroundStarted": "Hermes Link is running in the background. PID: {pid}",
309
346
  "start.alreadyRunning": "Hermes Link is already running. PID: {pid}",
@@ -323,18 +360,36 @@ var messages = {
323
360
  "config.unset.description": "Unset a configuration value",
324
361
  "config.unknownKey": "Unknown config key: {key}",
325
362
  "config.lanHostInvalid": "lan-host must be a private LAN IPv4 address, such as 192.168.1.23.",
326
- "config.lanHostSet": "Configured LAN host: {value}",
327
- "config.lanHostUnset": "Configured LAN host cleared.",
363
+ "config.lanHostSet": "LAN access address override set: {value}",
364
+ "config.lanHostUnset": "LAN access address override cleared.",
328
365
  "config.logLevelInvalid": "log-level must be one of: debug, info, warn, error.",
329
366
  "config.logLevelSet": "Configured log level: {value}",
367
+ "config.logLevelRestartRequired": "The running daemon keeps its current logger until restart. Run `hermeslink restart` to apply this write level immediately.",
330
368
  "config.logLevelUnset": "Configured log level reset to the default: {value}.",
331
369
  "config.reported": "Updated HermesPilot Server with the latest LAN address.",
332
370
  "config.reportSkippedUnpaired": "Hermes Link is not paired yet. The LAN address will be reported after pairing.",
333
371
  "daemon.description": "Run Hermes Link in the foreground",
334
372
  "daemon.foreground": "Hermes Link foreground daemon is running. Press Ctrl+C to stop.",
335
- "logs.description": "Show Hermes Link log paths",
373
+ "logs.description": "Show or follow Hermes Link logs",
374
+ "logs.follow": "follow log output",
375
+ "logs.tail": "print the last N lines before exiting or following",
376
+ "logs.serviceOnly": "only read the service JSONL log",
377
+ "logs.daemonOnly": "only read the daemon stdout/stderr log",
378
+ "logs.gatewayOnly": "only read Hermes Gateway logs",
379
+ "logs.all": "read service, daemon, and Gateway logs",
380
+ "logs.level": "minimum level to display: debug, info, warn, or error",
381
+ "logs.debugLevel": "show debug and above",
382
+ "logs.infoLevel": "show info and above",
383
+ "logs.warnLevel": "show warn and above",
384
+ "logs.errorLevel": "show errors only",
336
385
  "logs.servicePath": "Service log: {path}",
337
386
  "logs.daemonPath": "Daemon stdout/stderr log: {path}",
387
+ "logs.gatewayPath": "Gateway log: {path}",
388
+ "logs.followHint": "Use `hermeslink logs -f` to follow logs, or `hermeslink logs -n 100` to print recent lines.",
389
+ "logs.following": "Following logs. Press Ctrl+C to stop.",
390
+ "logs.flush.description": "Clear Hermes Link log files",
391
+ "logs.flush.all": "clear service, daemon, and Gateway logs",
392
+ "logs.flush.flushed": "Cleared {truncated} active log file(s) and removed {removed} rotated log file(s).",
338
393
  "autostart.description": "Manage boot autostart",
339
394
  "autostart.on.description": "Enable boot autostart",
340
395
  "autostart.off.description": "Disable boot autostart",
@@ -390,7 +445,7 @@ var messages = {
390
445
  "doctor.installId": "Install ID: {value}",
391
446
  "doctor.linkId": "Link ID: {value}",
392
447
  "doctor.notAssigned": "not assigned",
393
- "doctor.lanHost": "Configured LAN host: {value}",
448
+ "doctor.lanHost": "LAN access address (manual override): {value}",
394
449
  "doctor.networkWarning": "Network note: {message}",
395
450
  "doctor.hermesCli": "Hermes CLI: {value}",
396
451
  "doctor.hermesCliUnavailable": "Hermes CLI is unavailable. Please make sure the `hermes` command can run in this system.",
@@ -415,12 +470,41 @@ var messages = {
415
470
  "status.json": "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB\u7684\u72B6\u6001 JSON",
416
471
  "status.runtime": "\u8FD0\u884C\u76EE\u5F55\uFF1A{value}",
417
472
  "status.mode": "\u6A21\u5F0F\uFF1A{value}",
473
+ "status.daemon": "\u540E\u53F0\u670D\u52A1\uFF1A{value}",
474
+ "status.daemon.runningReachable": "\u8FD0\u884C\u4E2D\uFF08PID\uFF1A{pid}\uFF0C\u672C\u5730 API \u53EF\u8BBF\u95EE\uFF09",
475
+ "status.daemon.runningUnreachable": "\u8FD0\u884C\u4E2D\uFF08PID\uFF1A{pid}\uFF0C\u4F46\u672C\u5730 API \u6682\u4E0D\u53EF\u8BBF\u95EE\uFF09",
476
+ "status.daemon.notRunning": "\u672A\u8FD0\u884C",
418
477
  "status.port": "\u672C\u5730\u7AEF\u53E3\uFF1A{value}",
419
- "status.lanHost": "\u5DF2\u914D\u7F6E\u5C40\u57DF\u7F51\u4E3B\u673A\uFF1A{value}",
478
+ "status.lanHost": "\u5C40\u57DF\u7F51\u8BBF\u95EE\u5730\u5740\uFF08\u624B\u52A8\u6307\u5B9A\uFF09\uFF1A{value}",
420
479
  "status.notSet": "\u672A\u8BBE\u7F6E",
480
+ "status.unknown": "\u672A\u77E5",
421
481
  "status.environmentWarning": "\u7F51\u7EDC\u63D0\u793A\uFF1A{message}",
422
482
  "status.linkId": "Link ID\uFF1A{value}",
423
483
  "status.notPaired": "\u5C1A\u672A\u914D\u5BF9",
484
+ "status.relay": "Relay\uFF1A{value}",
485
+ "status.relay.notPaired": "\u672A\u8FDE\u63A5\uFF08\u5C1A\u672A\u914D\u5BF9\uFF0CRelay \u4F1A\u4FDD\u6301\u5173\u95ED\uFF09",
486
+ "status.relay.notConfigured": "\u672A\u914D\u7F6E",
487
+ "status.relay.serviceStopped": "\u672A\u8FDE\u63A5\uFF08\u540E\u53F0\u670D\u52A1\u672A\u8FD0\u884C\uFF09",
488
+ "status.relay.serviceUnreachable": "\u672A\u77E5\uFF08\u540E\u53F0\u670D\u52A1\u6B63\u5728\u8FD0\u884C\uFF0C\u4F46\u672C\u5730 API \u6682\u4E0D\u53EF\u8BBF\u95EE\uFF09",
489
+ "status.relay.unknown": "\u672A\u77E5\uFF08\u540E\u53F0\u670D\u52A1\u8FD8\u6CA1\u6709\u4E0A\u62A5 Relay \u72B6\u6001\uFF09",
490
+ "status.relay.state.connecting": "\u8FDE\u63A5\u4E2D",
491
+ "status.relay.state.connected": "\u5DF2\u8FDE\u63A5",
492
+ "status.relay.state.disconnected": "\u672A\u8FDE\u63A5",
493
+ "status.relay.state.retrying": "\u91CD\u8BD5\u4E2D",
494
+ "status.relay.state.cooldown": "\u51B7\u5374\u4E2D",
495
+ "status.relay.state.failed": "\u8FDE\u63A5\u5931\u8D25",
496
+ "status.relay.stateWithAttempt": "{state}\uFF08\u7B2C {attempt} \u6B21\uFF09",
497
+ "status.relay.updatedAt": "\u66F4\u65B0\u4E8E {time}",
498
+ "status.relay.withMessage": "{state} - {message}",
499
+ "status.relay.withUpdatedAt": "{state}\uFF1B{updatedAt}",
500
+ "status.relay.withMessageAndUpdatedAt": "{state} - {message}\uFF1B{updatedAt}",
501
+ "status.recentError": "\u6700\u8FD1\u9519\u8BEF\uFF1A{value}",
502
+ "status.recentError.none": "\u65E0",
503
+ "status.recentError.format": "{time} [{source}] {message}",
504
+ "status.logSource.service": "\u670D\u52A1",
505
+ "status.logSource.daemon": "\u5B88\u62A4\u8FDB\u7A0B",
506
+ "status.logSource.gateway": "Gateway",
507
+ "status.timeUnknown": "\u65F6\u95F4\u672A\u77E5",
424
508
  "start.description": "\u542F\u52A8 Hermes Link \u670D\u52A1",
425
509
  "start.backgroundStarted": "Hermes Link \u5DF2\u5728\u540E\u53F0\u8FD0\u884C\u3002PID\uFF1A{pid}",
426
510
  "start.alreadyRunning": "Hermes Link \u5DF2\u7ECF\u5728\u8FD0\u884C\u3002PID\uFF1A{pid}",
@@ -440,18 +524,36 @@ var messages = {
440
524
  "config.unset.description": "\u6E05\u9664\u914D\u7F6E\u9879",
441
525
  "config.unknownKey": "\u672A\u77E5\u914D\u7F6E\u9879\uFF1A{key}",
442
526
  "config.lanHostInvalid": "lan-host \u5FC5\u987B\u662F\u5C40\u57DF\u7F51 IPv4 \u5730\u5740\uFF0C\u4F8B\u5982 192.168.1.23\u3002",
443
- "config.lanHostSet": "\u5DF2\u914D\u7F6E\u5C40\u57DF\u7F51\u4E3B\u673A\uFF1A{value}",
444
- "config.lanHostUnset": "\u5DF2\u6E05\u9664\u5C40\u57DF\u7F51\u4E3B\u673A\u914D\u7F6E\u3002",
527
+ "config.lanHostSet": "\u5DF2\u8BBE\u7F6E\u5C40\u57DF\u7F51\u8BBF\u95EE\u5730\u5740\uFF08\u624B\u52A8\u6307\u5B9A\uFF09\uFF1A{value}",
528
+ "config.lanHostUnset": "\u5DF2\u6E05\u9664\u5C40\u57DF\u7F51\u8BBF\u95EE\u5730\u5740\u624B\u52A8\u914D\u7F6E\u3002",
445
529
  "config.logLevelInvalid": "log-level \u53EA\u80FD\u662F\u4EE5\u4E0B\u503C\u4E4B\u4E00\uFF1Adebug\u3001info\u3001warn\u3001error\u3002",
446
530
  "config.logLevelSet": "\u5DF2\u914D\u7F6E\u65E5\u5FD7\u7EA7\u522B\uFF1A{value}",
531
+ "config.logLevelRestartRequired": "\u6B63\u5728\u8FD0\u884C\u7684 daemon \u4F1A\u7EE7\u7EED\u4F7F\u7528\u542F\u52A8\u65F6\u7684\u65E5\u5FD7\u5199\u5165\u7EA7\u522B\u3002\u8FD0\u884C `hermeslink restart` \u53EF\u7ACB\u5373\u5E94\u7528\u65B0\u7684\u5199\u5165\u7EA7\u522B\u3002",
447
532
  "config.logLevelUnset": "\u5DF2\u5C06\u65E5\u5FD7\u7EA7\u522B\u6062\u590D\u4E3A\u9ED8\u8BA4\u503C\uFF1A{value}\u3002",
448
533
  "config.reported": "\u5DF2\u628A\u6700\u65B0\u5C40\u57DF\u7F51\u5730\u5740\u66F4\u65B0\u5230 HermesPilot Server\u3002",
449
534
  "config.reportSkippedUnpaired": "Hermes Link \u8FD8\u6CA1\u6709\u914D\u5BF9\uFF0C\u5C40\u57DF\u7F51\u5730\u5740\u4F1A\u5728\u914D\u5BF9\u540E\u4E0A\u62A5\u3002",
450
535
  "daemon.description": "\u4EE5\u524D\u53F0\u65B9\u5F0F\u8FD0\u884C Hermes Link",
451
536
  "daemon.foreground": "Hermes Link \u524D\u53F0\u670D\u52A1\u6B63\u5728\u8FD0\u884C\u3002\u6309 Ctrl+C \u505C\u6B62\u3002",
452
- "logs.description": "\u663E\u793A Hermes Link \u65E5\u5FD7\u8DEF\u5F84",
537
+ "logs.description": "\u663E\u793A\u6216\u8DDF\u8E2A Hermes Link \u65E5\u5FD7",
538
+ "logs.follow": "\u6301\u7EED\u8DDF\u8E2A\u65E5\u5FD7\u8F93\u51FA",
539
+ "logs.tail": "\u9000\u51FA\u6216\u6301\u7EED\u8DDF\u8E2A\u524D\u5148\u8F93\u51FA\u6700\u8FD1 N \u884C",
540
+ "logs.serviceOnly": "\u53EA\u8BFB\u53D6\u670D\u52A1 JSONL \u65E5\u5FD7",
541
+ "logs.daemonOnly": "\u53EA\u8BFB\u53D6 Daemon \u6807\u51C6\u8F93\u51FA/\u9519\u8BEF\u65E5\u5FD7",
542
+ "logs.gatewayOnly": "\u53EA\u8BFB\u53D6 Hermes Gateway \u65E5\u5FD7",
543
+ "logs.all": "\u8BFB\u53D6\u670D\u52A1\u3001Daemon \u4E0E Gateway \u65E5\u5FD7",
544
+ "logs.level": "\u6700\u4F4E\u663E\u793A\u7EA7\u522B\uFF1Adebug\u3001info\u3001warn \u6216 error",
545
+ "logs.debugLevel": "\u663E\u793A debug \u53CA\u4EE5\u4E0A\u7EA7\u522B",
546
+ "logs.infoLevel": "\u663E\u793A info \u53CA\u4EE5\u4E0A\u7EA7\u522B",
547
+ "logs.warnLevel": "\u663E\u793A warn \u53CA\u4EE5\u4E0A\u7EA7\u522B",
548
+ "logs.errorLevel": "\u53EA\u663E\u793A error \u7EA7\u522B",
453
549
  "logs.servicePath": "\u670D\u52A1\u65E5\u5FD7\uFF1A{path}",
454
550
  "logs.daemonPath": "Daemon \u6807\u51C6\u8F93\u51FA/\u9519\u8BEF\u65E5\u5FD7\uFF1A{path}",
551
+ "logs.gatewayPath": "Gateway \u65E5\u5FD7\uFF1A{path}",
552
+ "logs.followHint": "\u4F7F\u7528 `hermeslink logs -f` \u6301\u7EED\u8DDF\u8E2A\u65E5\u5FD7\uFF0C\u6216 `hermeslink logs -n 100` \u8F93\u51FA\u6700\u8FD1\u65E5\u5FD7\u3002",
553
+ "logs.following": "\u6B63\u5728\u6301\u7EED\u8DDF\u8E2A\u65E5\u5FD7\u3002\u6309 Ctrl+C \u505C\u6B62\u3002",
554
+ "logs.flush.description": "\u6E05\u7A7A Hermes Link \u65E5\u5FD7\u6587\u4EF6",
555
+ "logs.flush.all": "\u6E05\u7A7A\u670D\u52A1\u3001Daemon \u4E0E Gateway \u65E5\u5FD7",
556
+ "logs.flush.flushed": "\u5DF2\u6E05\u7A7A {truncated} \u4E2A\u5F53\u524D\u65E5\u5FD7\u6587\u4EF6\uFF0C\u5E76\u5220\u9664 {removed} \u4E2A\u8F6E\u8F6C\u65E5\u5FD7\u6587\u4EF6\u3002",
455
557
  "autostart.description": "\u7BA1\u7406\u5F00\u673A\u81EA\u542F",
456
558
  "autostart.on.description": "\u542F\u7528\u5F00\u673A\u81EA\u542F",
457
559
  "autostart.off.description": "\u5173\u95ED\u5F00\u673A\u81EA\u542F",
@@ -507,7 +609,7 @@ var messages = {
507
609
  "doctor.installId": "Install ID\uFF1A{value}",
508
610
  "doctor.linkId": "Link ID\uFF1A{value}",
509
611
  "doctor.notAssigned": "\u5C1A\u672A\u5206\u914D",
510
- "doctor.lanHost": "\u5DF2\u914D\u7F6E\u5C40\u57DF\u7F51\u4E3B\u673A\uFF1A{value}",
612
+ "doctor.lanHost": "\u5C40\u57DF\u7F51\u8BBF\u95EE\u5730\u5740\uFF08\u624B\u52A8\u6307\u5B9A\uFF09\uFF1A{value}",
511
613
  "doctor.networkWarning": "\u7F51\u7EDC\u63D0\u793A\uFF1A{message}",
512
614
  "doctor.hermesCli": "Hermes CLI\uFF1A{value}",
513
615
  "doctor.hermesCliUnavailable": "Hermes CLI\uFF1A\u4E0D\u53EF\u7528\u3002\u8BF7\u786E\u8BA4\u5F53\u524D\u7CFB\u7EDF\u53EF\u4EE5\u76F4\u63A5\u8FD0\u884C `hermes` \u547D\u4EE4\u3002",
@@ -942,6 +1044,23 @@ program.command("status").option("--json", helpText("status.json")).description(
942
1044
  const [identity, config] = await Promise.all([loadIdentity(paths), loadConfig(paths)]);
943
1045
  const language = resolveLanguage(config.language);
944
1046
  const t = translate.bind(null, language);
1047
+ const [daemonStatus, relayStatus, recentError] = await Promise.all([
1048
+ getDaemonStatus(paths),
1049
+ readRelayStatusSnapshot(paths).catch(() => null),
1050
+ readMostRecentError(paths).catch(() => null)
1051
+ ]);
1052
+ const serviceProbe = daemonStatus.running ? await probeLocalLinkService({
1053
+ port: config.port,
1054
+ linkId: identity?.link_id ?? void 0,
1055
+ timeoutMs: 500
1056
+ }).catch(() => null) : null;
1057
+ const relay = buildRelayStatusPayload({
1058
+ paired: Boolean(identity?.link_id),
1059
+ relayConfigured: Boolean(config.relayBaseUrl),
1060
+ daemonRunning: daemonStatus.running,
1061
+ localApiReachable: serviceProbe?.reachable ?? false,
1062
+ relayStatus
1063
+ });
945
1064
  const payload = {
946
1065
  version: LINK_VERSION,
947
1066
  runtimeHome: paths.homeDir,
@@ -949,12 +1068,18 @@ program.command("status").option("--json", helpText("status.json")).description(
949
1068
  mode: identity?.link_id ? "paired" : "local-only",
950
1069
  port: config.port,
951
1070
  lanHost: config.lanHost,
1071
+ lanHostMeaning: config.lanHost ? "Manual LAN address override. It is only needed when the phone cannot reach the automatically discovered LAN address." : null,
1072
+ daemon: {
1073
+ running: daemonStatus.running,
1074
+ pid: daemonStatus.pid,
1075
+ localApiReachable: serviceProbe?.reachable ?? false,
1076
+ localApiVersion: serviceProbe?.version ?? null,
1077
+ logFile: daemonStatus.logFile
1078
+ },
952
1079
  environment: detectRuntimeEnvironment(),
953
1080
  identity: identity ? getIdentityStatus(identity) : null,
954
- relay: {
955
- configured: Boolean(config.relayBaseUrl),
956
- connected: false
957
- }
1081
+ relay,
1082
+ recentError
958
1083
  };
959
1084
  if (options.json) {
960
1085
  console.log(JSON.stringify(payload, null, 2));
@@ -963,12 +1088,17 @@ program.command("status").option("--json", helpText("status.json")).description(
963
1088
  console.log(`Hermes Link ${payload.version}`);
964
1089
  console.log(t("status.runtime", { value: payload.runtimeHome }));
965
1090
  console.log(t("status.mode", { value: payload.mode }));
1091
+ console.log(t("status.daemon", { value: formatDaemonStatus(payload.daemon, t) }));
966
1092
  console.log(t("status.port", { value: payload.port }));
967
- console.log(t("status.lanHost", { value: payload.lanHost ?? t("status.notSet") }));
1093
+ if (payload.lanHost) {
1094
+ console.log(t("status.lanHost", { value: payload.lanHost }));
1095
+ }
968
1096
  if (payload.environment.warning) {
969
1097
  console.log(t("status.environmentWarning", { message: payload.environment.warning }));
970
1098
  }
971
1099
  console.log(t("status.linkId", { value: payload.identity?.linkId ?? t("status.notPaired") }));
1100
+ console.log(t("status.relay", { value: formatRelayStatus(payload.relay, t, language) }));
1101
+ console.log(t("status.recentError", { value: formatRecentError(payload.recentError, t, language) }));
972
1102
  });
973
1103
  var configCommand = program.command("config").description(helpText("config.description"));
974
1104
  configCommand.command("set").argument("<key>").argument("<value>").description(helpText("config.set.description")).action(async (key, value) => {
@@ -994,6 +1124,10 @@ configCommand.command("set").argument("<key>").argument("<value>").description(h
994
1124
  }
995
1125
  const next = await saveConfig({ logLevel }, paths);
996
1126
  console.log(t("config.logLevelSet", { value: next.logLevel }));
1127
+ const status = await getDaemonStatus(paths).catch(() => null);
1128
+ if (status?.running) {
1129
+ console.log(t("config.logLevelRestartRequired"));
1130
+ }
997
1131
  return;
998
1132
  }
999
1133
  {
@@ -1024,15 +1158,24 @@ configCommand.command("unset").argument("<key>").description(helpText("config.un
1024
1158
  }
1025
1159
  });
1026
1160
  program.command("start").description(helpText("start.description")).action(async () => {
1027
- const [config, status] = await Promise.all([loadConfig(), getDaemonStatus()]);
1161
+ const paths = resolveRuntimePaths();
1162
+ const [config, status] = await Promise.all([loadConfig(paths), getDaemonStatus(paths)]);
1028
1163
  const language = resolveLanguage(config.language);
1029
1164
  const t = translate.bind(null, language);
1030
1165
  if (status.running && status.pid) {
1031
1166
  console.log(t("start.alreadyRunning", { pid: status.pid }));
1167
+ await ensureHermesLinkSkillInstalledBestEffort({
1168
+ paths,
1169
+ source: "cli_start_already_running"
1170
+ });
1032
1171
  return;
1033
1172
  }
1034
- const nextStatus = await startDaemonProcess();
1173
+ const nextStatus = await startDaemonProcess(paths);
1035
1174
  console.log(t("start.backgroundStarted", { pid: nextStatus.pid ?? "unknown" }));
1175
+ await ensureHermesLinkSkillInstalledBestEffort({
1176
+ paths,
1177
+ source: "cli_start"
1178
+ });
1036
1179
  });
1037
1180
  program.command("stop").description(helpText("stop.description")).action(async () => {
1038
1181
  const config = await loadConfig();
@@ -1047,12 +1190,17 @@ program.command("stop").description(helpText("stop.description")).action(async (
1047
1190
  console.log(t("stop.stopped"));
1048
1191
  });
1049
1192
  program.command("restart").description(helpText("restart.description")).action(async () => {
1050
- const config = await loadConfig();
1193
+ const paths = resolveRuntimePaths();
1194
+ const config = await loadConfig(paths);
1051
1195
  const language = resolveLanguage(config.language);
1052
1196
  const t = translate.bind(null, language);
1053
- await stopDaemonProcess();
1054
- const status = await startDaemonProcess();
1197
+ await stopDaemonProcess(paths);
1198
+ const status = await startDaemonProcess(paths);
1055
1199
  console.log(t("start.backgroundStarted", { pid: status.pid ?? "unknown" }));
1200
+ await ensureHermesLinkSkillInstalledBestEffort({
1201
+ paths,
1202
+ source: "cli_restart"
1203
+ });
1056
1204
  });
1057
1205
  program.command("deliver").argument("<staging-dir>").description(helpText("deliver.description")).action(async (stagingDir) => {
1058
1206
  const paths = resolveRuntimePaths();
@@ -1125,6 +1273,7 @@ program.command("pair").description(helpText("pair.description")).action(async (
1125
1273
  relayBaseUrl: prepared.relayBaseUrl,
1126
1274
  linkId: prepared.linkId,
1127
1275
  localPort: config.port,
1276
+ paths,
1128
1277
  initialStreamBatchPolicy: streamBatchPolicy
1129
1278
  });
1130
1279
  pairingRelayBridge.publishNetworkRoutes(prepared.routes);
@@ -1151,6 +1300,10 @@ program.command("pair").description(helpText("pair.description")).action(async (
1151
1300
  await clearPairingClaim(prepared.sessionId, paths);
1152
1301
  console.log(t(reusedRunningService ? "pair.claimedRunning" : "pair.claimed"));
1153
1302
  printPostPairingNetworkNotice(prepared.routes.environment, config, t);
1303
+ await ensureHermesLinkSkillInstalledBestEffort({
1304
+ paths,
1305
+ source: "cli_pair_claimed"
1306
+ });
1154
1307
  try {
1155
1308
  if (hadActiveDevices) {
1156
1309
  console.log(t("pair.autostartUnchanged"));
@@ -1224,13 +1377,44 @@ autostart.command("status").description(helpText("autostart.status.description")
1224
1377
  })
1225
1378
  );
1226
1379
  });
1227
- program.command("logs").description(helpText("logs.description")).action(async () => {
1380
+ var logsCommand = program.command("logs").option("-f, --follow", helpText("logs.follow")).option("-n, --tail <lines>", helpText("logs.tail")).option("--service", helpText("logs.serviceOnly")).option("--daemon", helpText("logs.daemonOnly")).option("--gateway", helpText("logs.gatewayOnly")).option("--all", helpText("logs.all")).option("--level <level>", helpText("logs.level")).option("--debug", helpText("logs.debugLevel")).option("-i, --info", helpText("logs.infoLevel")).option("-w, --warn", helpText("logs.warnLevel")).option("-e, --error", helpText("logs.errorLevel")).description(helpText("logs.description")).action(async (options) => {
1228
1381
  const paths = resolveRuntimePaths();
1229
1382
  const config = await loadConfig(paths);
1230
1383
  const language = resolveLanguage(config.language);
1231
1384
  const t = translate.bind(null, language);
1232
- console.log(t("logs.servicePath", { path: getLinkLogFile(paths) }));
1233
- console.log(t("logs.daemonPath", { path: daemonLogFile(paths) }));
1385
+ const sources = resolveLogSources(paths, options);
1386
+ const tailLines = parseLogTailLines(options.tail, options.follow ? 80 : null);
1387
+ const displayLevel = parseLogDisplayLevel(options, config.logLevel);
1388
+ if (!options.follow && tailLines === null) {
1389
+ console.log(t("logs.servicePath", { path: getLinkLogFile(paths) }));
1390
+ console.log(t("logs.daemonPath", { path: daemonLogFile(paths) }));
1391
+ for (const filePath of getGatewayLogFiles(paths)) {
1392
+ console.log(t("logs.gatewayPath", { path: filePath }));
1393
+ }
1394
+ console.log(t("logs.followHint"));
1395
+ return;
1396
+ }
1397
+ await printRecentLogLines(sources, tailLines ?? 80, displayLevel);
1398
+ if (options.follow) {
1399
+ console.log(t("logs.following"));
1400
+ await followLogSources(sources, displayLevel);
1401
+ }
1402
+ });
1403
+ logsCommand.command("flush").description(helpText("logs.flush.description")).option("--service", helpText("logs.serviceOnly")).option("--daemon", helpText("logs.daemonOnly")).option("--gateway", helpText("logs.gatewayOnly")).option("--all", helpText("logs.flush.all")).action(async (options) => {
1404
+ const paths = resolveRuntimePaths();
1405
+ const config = await loadConfig(paths);
1406
+ const language = resolveLanguage(config.language);
1407
+ const t = translate.bind(null, language);
1408
+ const sources = resolveLogSources(paths, options);
1409
+ const result = await flushLogFiles({
1410
+ filePaths: sources.map((source) => source.filePath)
1411
+ });
1412
+ console.log(
1413
+ t("logs.flush.flushed", {
1414
+ truncated: result.truncated.length,
1415
+ removed: result.removed.length
1416
+ })
1417
+ );
1234
1418
  });
1235
1419
  program.command("doctor").option("--install", helpText("doctor.installOnly")).description(helpText("doctor.description")).action(async (options) => {
1236
1420
  const installInfo = readInstallPathInfo();
@@ -1285,6 +1469,193 @@ async function loadCliLanguage() {
1285
1469
  const config = await loadConfig();
1286
1470
  return resolveLanguage(config.language);
1287
1471
  }
1472
+ function buildRelayStatusPayload(input) {
1473
+ if (!input.paired) {
1474
+ return emptyRelayStatus(input.relayConfigured, "not_paired");
1475
+ }
1476
+ if (!input.relayConfigured) {
1477
+ return emptyRelayStatus(false, "not_configured");
1478
+ }
1479
+ if (!input.daemonRunning) {
1480
+ return emptyRelayStatus(true, "service_stopped");
1481
+ }
1482
+ if (!input.localApiReachable) {
1483
+ return emptyRelayStatus(true, "service_unreachable");
1484
+ }
1485
+ if (!input.relayStatus) {
1486
+ return emptyRelayStatus(true, "unknown");
1487
+ }
1488
+ return {
1489
+ configured: true,
1490
+ connected: input.relayStatus.state === "connected",
1491
+ state: input.relayStatus.state,
1492
+ attempt: input.relayStatus.attempt,
1493
+ message: input.relayStatus.message,
1494
+ updatedAt: input.relayStatus.updatedAt
1495
+ };
1496
+ }
1497
+ function emptyRelayStatus(configured, state) {
1498
+ return {
1499
+ configured,
1500
+ connected: false,
1501
+ state,
1502
+ attempt: null,
1503
+ message: null,
1504
+ updatedAt: null
1505
+ };
1506
+ }
1507
+ function formatDaemonStatus(daemon, t) {
1508
+ if (!daemon.running) {
1509
+ return t("status.daemon.notRunning");
1510
+ }
1511
+ const pid = daemon.pid ?? t("status.unknown");
1512
+ return t(
1513
+ daemon.localApiReachable ? "status.daemon.runningReachable" : "status.daemon.runningUnreachable",
1514
+ { pid }
1515
+ );
1516
+ }
1517
+ function formatRelayStatus(relay, t, language) {
1518
+ if (relay.state === "not_paired") {
1519
+ return t("status.relay.notPaired");
1520
+ }
1521
+ if (relay.state === "not_configured") {
1522
+ return t("status.relay.notConfigured");
1523
+ }
1524
+ if (relay.state === "service_stopped") {
1525
+ return t("status.relay.serviceStopped");
1526
+ }
1527
+ if (relay.state === "service_unreachable") {
1528
+ return t("status.relay.serviceUnreachable");
1529
+ }
1530
+ if (relay.state === "unknown") {
1531
+ return t("status.relay.unknown");
1532
+ }
1533
+ let state = formatRelayState(relay.state, t);
1534
+ if (relay.attempt !== null && relay.attempt > 0 && relay.state !== "connected") {
1535
+ state = t("status.relay.stateWithAttempt", {
1536
+ state,
1537
+ attempt: relay.attempt
1538
+ });
1539
+ }
1540
+ const updatedAt = relay.updatedAt ? t("status.relay.updatedAt", {
1541
+ time: formatTimestamp(relay.updatedAt, language)
1542
+ }) : null;
1543
+ if (relay.message && updatedAt) {
1544
+ return t("status.relay.withMessageAndUpdatedAt", {
1545
+ state,
1546
+ message: relay.message,
1547
+ updatedAt
1548
+ });
1549
+ }
1550
+ if (relay.message) {
1551
+ return t("status.relay.withMessage", {
1552
+ state,
1553
+ message: relay.message
1554
+ });
1555
+ }
1556
+ if (updatedAt) {
1557
+ return t("status.relay.withUpdatedAt", {
1558
+ state,
1559
+ updatedAt
1560
+ });
1561
+ }
1562
+ return state;
1563
+ }
1564
+ function formatRelayState(state, t) {
1565
+ switch (state) {
1566
+ case "connecting":
1567
+ return t("status.relay.state.connecting");
1568
+ case "connected":
1569
+ return t("status.relay.state.connected");
1570
+ case "disconnected":
1571
+ return t("status.relay.state.disconnected");
1572
+ case "retrying":
1573
+ return t("status.relay.state.retrying");
1574
+ case "cooldown":
1575
+ return t("status.relay.state.cooldown");
1576
+ case "failed":
1577
+ return t("status.relay.state.failed");
1578
+ }
1579
+ }
1580
+ async function readMostRecentError(paths) {
1581
+ const [service, daemon, gateway] = await Promise.all([
1582
+ readRecentLogEntries({ paths, limit: 120 }).catch(() => []),
1583
+ readRecentTextLogEntries({
1584
+ paths,
1585
+ filePaths: [daemonLogFile(paths)],
1586
+ limit: 120
1587
+ }).catch(() => []),
1588
+ readRecentGatewayLogEntries({ paths, limit: 120 }).catch(() => [])
1589
+ ]);
1590
+ const errors = [
1591
+ ...service.map((entry) => toStatusRecentError("service", entry)),
1592
+ ...daemon.map((entry) => toStatusRecentError("daemon", entry)),
1593
+ ...gateway.map((entry) => toStatusRecentError("gateway", entry))
1594
+ ].filter((item) => item !== null);
1595
+ errors.sort((left, right) => readTimestampMs(right.ts) - readTimestampMs(left.ts));
1596
+ return errors[0] ?? null;
1597
+ }
1598
+ function toStatusRecentError(source, entry) {
1599
+ if (!isActionableStatusError(source, entry)) {
1600
+ return null;
1601
+ }
1602
+ const errorDetail = typeof entry.fields?.error === "string" ? entry.fields.error : null;
1603
+ const message = errorDetail && errorDetail !== entry.message ? `${entry.message}: ${errorDetail}` : entry.message;
1604
+ return {
1605
+ source,
1606
+ ts: entry.ts,
1607
+ message
1608
+ };
1609
+ }
1610
+ function isActionableStatusError(source, entry) {
1611
+ if (source === "service") {
1612
+ return entry.level === "error";
1613
+ }
1614
+ if (entry.level !== "error") {
1615
+ return false;
1616
+ }
1617
+ return /(^|[\s[])(error|fatal)([\]\s:]|$)|\b(traceback|exception|failed|failure|unhandledRejection|uncaughtException)\b/iu.test(
1618
+ entry.message
1619
+ );
1620
+ }
1621
+ function formatRecentError(error, t, language) {
1622
+ if (!error) {
1623
+ return t("status.recentError.none");
1624
+ }
1625
+ return t("status.recentError.format", {
1626
+ time: error.ts ? formatTimestamp(error.ts, language) : t("status.timeUnknown"),
1627
+ source: formatLogSourceLabel(error.source, t),
1628
+ message: error.message
1629
+ });
1630
+ }
1631
+ function formatLogSourceLabel(source, t) {
1632
+ switch (source) {
1633
+ case "service":
1634
+ return t("status.logSource.service");
1635
+ case "daemon":
1636
+ return t("status.logSource.daemon");
1637
+ case "gateway":
1638
+ return t("status.logSource.gateway");
1639
+ }
1640
+ }
1641
+ function formatTimestamp(value, language) {
1642
+ const timestamp = Date.parse(value);
1643
+ if (!Number.isFinite(timestamp)) {
1644
+ return value;
1645
+ }
1646
+ return new Intl.DateTimeFormat(language === "zh-CN" ? "zh-CN" : "en-US", {
1647
+ dateStyle: "short",
1648
+ timeStyle: "medium",
1649
+ hour12: false
1650
+ }).format(new Date(timestamp));
1651
+ }
1652
+ function readTimestampMs(value) {
1653
+ if (!value) {
1654
+ return 0;
1655
+ }
1656
+ const timestamp = Date.parse(value);
1657
+ return Number.isFinite(timestamp) ? timestamp : 0;
1658
+ }
1288
1659
  function formatHermesVersion(version) {
1289
1660
  return version.version ?? version.raw;
1290
1661
  }
@@ -1335,6 +1706,225 @@ function printInstallDiagnostics(info, t, verbose) {
1335
1706
  }
1336
1707
  console.log(t("doctor.installNpxFallback"));
1337
1708
  }
1709
+ function resolveLogSources(paths, options) {
1710
+ const hasExplicitSelection = Boolean(
1711
+ options.service || options.daemon || options.gateway || options.all
1712
+ );
1713
+ const includeService = options.all || options.service || !hasExplicitSelection;
1714
+ const includeDaemon = options.all || options.daemon || !hasExplicitSelection;
1715
+ const includeGateway = options.all || options.gateway;
1716
+ const sources = [];
1717
+ if (includeService) {
1718
+ sources.push({ label: "service", filePath: getLinkLogFile(paths), kind: "jsonl" });
1719
+ }
1720
+ if (includeDaemon) {
1721
+ sources.push({ label: "daemon", filePath: daemonLogFile(paths), kind: "text" });
1722
+ }
1723
+ if (includeGateway) {
1724
+ for (const filePath of getGatewayLogFiles(paths)) {
1725
+ sources.push({ label: "gateway", filePath, kind: "text" });
1726
+ }
1727
+ }
1728
+ return sources;
1729
+ }
1730
+ function parseLogDisplayLevel(options, fallback) {
1731
+ const flagLevels = [];
1732
+ if (options.debug) {
1733
+ flagLevels.push("debug");
1734
+ }
1735
+ if (options.info) {
1736
+ flagLevels.push("info");
1737
+ }
1738
+ if (options.warn) {
1739
+ flagLevels.push("warn");
1740
+ }
1741
+ if (options.error) {
1742
+ flagLevels.push("error");
1743
+ }
1744
+ if (flagLevels.length > 1) {
1745
+ throw new Error("Choose only one log level filter");
1746
+ }
1747
+ if (options.level !== void 0 && flagLevels.length > 0) {
1748
+ throw new Error("Use either --level or a level shortcut, not both");
1749
+ }
1750
+ if (flagLevels.length === 1) {
1751
+ return flagLevels[0];
1752
+ }
1753
+ if (options.level === void 0) {
1754
+ return fallback;
1755
+ }
1756
+ const level = parseLogLevel(options.level.trim().toLowerCase());
1757
+ if (!level) {
1758
+ throw new Error("--level must be one of: debug, info, warn, error");
1759
+ }
1760
+ return level;
1761
+ }
1762
+ function parseLogTailLines(raw, defaultValue) {
1763
+ if (raw === void 0) {
1764
+ return defaultValue;
1765
+ }
1766
+ const value = Number.parseInt(raw, 10);
1767
+ if (!Number.isInteger(value) || value < 0) {
1768
+ throw new Error("--tail must be a non-negative integer");
1769
+ }
1770
+ return Math.min(value, 2e3);
1771
+ }
1772
+ async function printRecentLogLines(sources, limit, minLevel) {
1773
+ for (const source of sources) {
1774
+ const lines = await readLastLogLines(source.filePath, limit);
1775
+ for (const line of lines) {
1776
+ printLogLine(source, line, minLevel);
1777
+ }
1778
+ }
1779
+ }
1780
+ async function readLastLogLines(filePath, limit) {
1781
+ if (limit <= 0) {
1782
+ return [];
1783
+ }
1784
+ const raw = await readFile2(filePath, "utf8").catch(() => null);
1785
+ if (!raw) {
1786
+ return [];
1787
+ }
1788
+ return raw.split(/\r?\n/u).filter(Boolean).slice(-limit);
1789
+ }
1790
+ async function followLogSources(sources, minLevel) {
1791
+ const followers = await Promise.all(
1792
+ sources.map((source) => createLogFollower(source, minLevel))
1793
+ );
1794
+ await new Promise((resolve) => {
1795
+ const stop = () => {
1796
+ for (const follower of followers) {
1797
+ follower.close();
1798
+ }
1799
+ process.off("SIGINT", stop);
1800
+ process.off("SIGTERM", stop);
1801
+ resolve();
1802
+ };
1803
+ process.once("SIGINT", stop);
1804
+ process.once("SIGTERM", stop);
1805
+ });
1806
+ }
1807
+ async function createLogFollower(source, minLevel) {
1808
+ let offset = await readFileSize(source.filePath);
1809
+ let pending = "";
1810
+ let closed = false;
1811
+ const poll = async () => {
1812
+ if (closed) {
1813
+ return;
1814
+ }
1815
+ const size = await readFileSize(source.filePath);
1816
+ if (size === null) {
1817
+ offset = 0;
1818
+ pending = "";
1819
+ return;
1820
+ }
1821
+ if (offset === null || size < offset) {
1822
+ offset = 0;
1823
+ pending = "";
1824
+ }
1825
+ if (size <= offset) {
1826
+ return;
1827
+ }
1828
+ const chunk = await readFileRange(source.filePath, offset, size);
1829
+ offset = size;
1830
+ const parts = `${pending}${chunk}`.split(/\r?\n/u);
1831
+ pending = parts.pop() ?? "";
1832
+ for (const line of parts) {
1833
+ if (line) {
1834
+ printLogLine(source, line, minLevel);
1835
+ }
1836
+ }
1837
+ };
1838
+ watchFile(source.filePath, { interval: 1e3 }, () => {
1839
+ void poll().catch(() => void 0);
1840
+ });
1841
+ return {
1842
+ close() {
1843
+ closed = true;
1844
+ unwatchFile(source.filePath);
1845
+ if (pending) {
1846
+ printLogLine(source, pending, minLevel);
1847
+ pending = "";
1848
+ }
1849
+ }
1850
+ };
1851
+ }
1852
+ async function readFileSize(filePath) {
1853
+ const info = await stat2(filePath).catch(() => null);
1854
+ return info?.isFile() ? info.size : null;
1855
+ }
1856
+ async function readFileRange(filePath, start, end) {
1857
+ const length = Math.max(0, end - start);
1858
+ if (length === 0) {
1859
+ return "";
1860
+ }
1861
+ const handle = await open(filePath, "r");
1862
+ try {
1863
+ const buffer = Buffer.alloc(length);
1864
+ const result = await handle.read(buffer, 0, length, start);
1865
+ return buffer.subarray(0, result.bytesRead).toString("utf8");
1866
+ } finally {
1867
+ await handle.close();
1868
+ }
1869
+ }
1870
+ function printLogLine(source, line, minLevel) {
1871
+ if (!logLineMatchesLevel(source, line, minLevel)) {
1872
+ return;
1873
+ }
1874
+ console.log(`[${source.label}] ${formatLogLine(source, line)}`);
1875
+ }
1876
+ function logLineMatchesLevel(source, line, minLevel) {
1877
+ return LOG_LEVEL_PRIORITY[readLogLineLevel(source, line)] >= LOG_LEVEL_PRIORITY[minLevel];
1878
+ }
1879
+ function readLogLineLevel(source, line) {
1880
+ if (source.kind === "jsonl") {
1881
+ try {
1882
+ const entry = JSON.parse(line);
1883
+ if (isLogLevel(entry.level)) {
1884
+ return entry.level;
1885
+ }
1886
+ } catch {
1887
+ return "info";
1888
+ }
1889
+ }
1890
+ return inferTextLogLevel(line);
1891
+ }
1892
+ function formatLogLine(source, line) {
1893
+ if (source.kind !== "jsonl") {
1894
+ return line;
1895
+ }
1896
+ try {
1897
+ const entry = JSON.parse(line);
1898
+ const ts = typeof entry.ts === "string" ? entry.ts : "";
1899
+ const level = typeof entry.level === "string" ? entry.level.toUpperCase() : "INFO";
1900
+ const message = typeof entry.message === "string" ? entry.message : line;
1901
+ const fields = entry.fields && typeof entry.fields === "object" ? ` ${JSON.stringify(entry.fields)}` : "";
1902
+ return `${ts} ${level} ${message}${fields}`.trim();
1903
+ } catch {
1904
+ return line;
1905
+ }
1906
+ }
1907
+ var LOG_LEVEL_PRIORITY = {
1908
+ debug: 10,
1909
+ info: 20,
1910
+ warn: 30,
1911
+ error: 40
1912
+ };
1913
+ function isLogLevel(value) {
1914
+ return value === "debug" || value === "info" || value === "warn" || value === "error";
1915
+ }
1916
+ function inferTextLogLevel(line) {
1917
+ if (/\b(error|fatal|traceback|failed|failure)\b/iu.test(line)) {
1918
+ return "error";
1919
+ }
1920
+ if (/\b(warn|warning)\b/iu.test(line)) {
1921
+ return "warn";
1922
+ }
1923
+ if (/\b(debug|trace)\b/iu.test(line)) {
1924
+ return "debug";
1925
+ }
1926
+ return "info";
1927
+ }
1338
1928
  function pairingPreflightProgressKey(stage) {
1339
1929
  switch (stage) {
1340
1930
  case "hermes_files":