@nordbyte/nordrelay 0.8.1 → 0.8.3

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.
Files changed (179) hide show
  1. package/.env.example +9 -0
  2. package/README.md +84 -1205
  3. package/dist/{access-control.js → access/access-control.js} +1 -1
  4. package/dist/{audit-log.js → access/audit-log.js} +32 -15
  5. package/dist/{session-locks.js → access/session-locks.js} +1 -1
  6. package/dist/{user-management.js → access/user-management.js} +1 -1
  7. package/dist/{claude-code-cli.js → agents/claude-code/claude-code-cli.js} +2 -2
  8. package/dist/{claude-code-session.js → agents/claude-code/claude-code-session.js} +1 -1
  9. package/dist/{codex-cli.js → agents/codex/codex-cli.js} +14 -5
  10. package/dist/{codex-session.js → agents/codex/codex-session.js} +2 -4
  11. package/dist/{hermes-cli.js → agents/hermes/hermes-cli.js} +2 -2
  12. package/dist/{hermes-launch.js → agents/hermes/hermes-launch.js} +1 -1
  13. package/dist/{hermes-session.js → agents/hermes/hermes-session.js} +1 -1
  14. package/dist/{openclaw-cli.js → agents/openclaw/openclaw-cli.js} +2 -2
  15. package/dist/{openclaw-launch.js → agents/openclaw/openclaw-launch.js} +1 -1
  16. package/dist/{openclaw-session.js → agents/openclaw/openclaw-session.js} +1 -1
  17. package/dist/{pi-cli.js → agents/pi/pi-cli.js} +2 -2
  18. package/dist/{pi-launch.js → agents/pi/pi-launch.js} +1 -1
  19. package/dist/{pi-session.js → agents/pi/pi-session.js} +1 -1
  20. package/dist/{adapter-conformance.js → agents/shared/adapter-conformance.js} +2 -2
  21. package/dist/{agent-activity.js → agents/shared/agent-activity.js} +5 -5
  22. package/dist/agents/shared/agent-auth-commands.js +30 -0
  23. package/dist/{agent-factory.js → agents/shared/agent-factory.js} +5 -5
  24. package/dist/{agent-feature-matrix.js → agents/shared/agent-feature-matrix.js} +2 -2
  25. package/dist/{agent-updates.js → agents/shared/agent-updates.js} +7 -7
  26. package/dist/{discord-artifacts.js → channels/discord/discord-artifacts.js} +4 -4
  27. package/dist/{discord-bot.js → channels/discord/discord-bot.js} +176 -451
  28. package/dist/{discord-channel-runtime.js → channels/discord/discord-channel-runtime.js} +2 -2
  29. package/dist/{discord-command-surface.js → channels/discord/discord-command-surface.js} +3 -3
  30. package/dist/{bot-rendering.js → channels/shared/bot-rendering.js} +6 -6
  31. package/dist/{channel-actions.js → channels/shared/channel-actions.js} +4 -4
  32. package/dist/channels/shared/channel-bridge-controller.js +69 -0
  33. package/dist/channels/shared/channel-cli-artifacts.js +51 -0
  34. package/dist/{channel-command-service.js → channels/shared/channel-command-service.js} +51 -28
  35. package/dist/channels/shared/channel-external-mirror-controller.js +193 -0
  36. package/dist/channels/shared/channel-external-monitor.js +52 -0
  37. package/dist/{channel-mirror-registry.js → channels/shared/channel-mirror-registry.js} +14 -6
  38. package/dist/{channel-peer-prompt.js → channels/shared/channel-peer-prompt.js} +3 -3
  39. package/dist/channels/shared/channel-prompt-queue.js +37 -0
  40. package/dist/{channel-turn-service.js → channels/shared/channel-turn-service.js} +25 -11
  41. package/dist/{context-key.js → channels/shared/context-key.js} +1 -1
  42. package/dist/{session-format.js → channels/shared/session-format.js} +2 -2
  43. package/dist/{slack-artifacts.js → channels/slack/slack-artifacts.js} +4 -4
  44. package/dist/{slack-bot.js → channels/slack/slack-bot.js} +171 -309
  45. package/dist/{slack-channel-runtime.js → channels/slack/slack-channel-runtime.js} +2 -2
  46. package/dist/{slack-command-surface.js → channels/slack/slack-command-surface.js} +2 -2
  47. package/dist/{slack-diagnostics.js → channels/slack/slack-diagnostics.js} +2 -2
  48. package/dist/{bot-ui.js → channels/telegram/bot-ui.js} +1 -1
  49. package/dist/{bot.js → channels/telegram/bot.js} +195 -430
  50. package/dist/{telegram-access-commands.js → channels/telegram/telegram-access-commands.js} +3 -3
  51. package/dist/{telegram-access-middleware.js → channels/telegram/telegram-access-middleware.js} +4 -4
  52. package/dist/{telegram-agent-commands.js → channels/telegram/telegram-agent-commands.js} +9 -9
  53. package/dist/{telegram-artifact-commands.js → channels/telegram/telegram-artifact-commands.js} +4 -4
  54. package/dist/{telegram-channel-runtime.js → channels/telegram/telegram-channel-runtime.js} +2 -2
  55. package/dist/{telegram-command-menu.js → channels/telegram/telegram-command-menu.js} +1 -1
  56. package/dist/{telegram-diagnostics-command.js → channels/telegram/telegram-diagnostics-command.js} +7 -7
  57. package/dist/{telegram-general-commands.js → channels/telegram/telegram-general-commands.js} +4 -4
  58. package/dist/{telegram-operational-commands.js → channels/telegram/telegram-operational-commands.js} +5 -5
  59. package/dist/{telegram-output.js → channels/telegram/telegram-output.js} +2 -2
  60. package/dist/{telegram-preference-commands.js → channels/telegram/telegram-preference-commands.js} +3 -3
  61. package/dist/{telegram-queue-commands.js → channels/telegram/telegram-queue-commands.js} +6 -6
  62. package/dist/{telegram-support-command.js → channels/telegram/telegram-support-command.js} +4 -4
  63. package/dist/{telegram-update-commands.js → channels/telegram/telegram-update-commands.js} +5 -5
  64. package/dist/{config-metadata.js → core/config-metadata.js} +8 -0
  65. package/dist/{config.js → core/config.js} +11 -3
  66. package/dist/core/pagination.js +22 -0
  67. package/dist/index.js +27 -23
  68. package/dist/peers/peer-discovery-jobs.js +206 -0
  69. package/dist/peers/peer-discovery.js +223 -0
  70. package/dist/peers/peer-health-monitor.js +49 -0
  71. package/dist/{peer-identity.js → peers/peer-identity.js} +50 -1
  72. package/dist/{peer-runtime-service.js → peers/peer-runtime-service.js} +29 -7
  73. package/dist/{peer-server.js → peers/peer-server.js} +3 -2
  74. package/dist/{peer-store.js → peers/peer-store.js} +96 -9
  75. package/dist/{peer-types.js → peers/peer-types.js} +28 -0
  76. package/dist/peers/peer-web-proxy-contract.js +129 -0
  77. package/dist/{metrics.js → runtime/metrics.js} +5 -3
  78. package/dist/{relay-artifact-service.js → runtime/relay-artifact-service.js} +1 -1
  79. package/dist/runtime/relay-auth-service.js +63 -0
  80. package/dist/runtime/relay-dashboard-service.js +139 -0
  81. package/dist/{relay-external-activity-monitor.js → runtime/relay-external-activity-monitor.js} +155 -53
  82. package/dist/{relay-queue-service.js → runtime/relay-queue-service.js} +1 -0
  83. package/dist/runtime/relay-runtime-active-sessions.js +387 -0
  84. package/dist/runtime/relay-runtime-dashboard.js +204 -0
  85. package/dist/{relay-runtime-helpers.js → runtime/relay-runtime-helpers.js} +3 -0
  86. package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +311 -0
  87. package/dist/runtime/relay-runtime-sessions.js +631 -0
  88. package/dist/runtime/relay-runtime-trace.js +92 -0
  89. package/dist/runtime/relay-runtime-types.js +1 -0
  90. package/dist/runtime/relay-runtime-updates-jobs.js +366 -0
  91. package/dist/runtime/relay-runtime.js +461 -0
  92. package/dist/runtime/runtime-cache.js +117 -0
  93. package/dist/{prompt-store.js → state/prompt-store.js} +13 -1
  94. package/dist/{session-registry.js → state/session-registry.js} +3 -3
  95. package/dist/{operations.js → support/operations.js} +7 -7
  96. package/dist/{support-bundle.js → support/support-bundle.js} +1 -1
  97. package/dist/{web-api-contract.js → web/web-api-contract.js} +19 -3
  98. package/dist/web/web-api-types.js +1 -0
  99. package/dist/{web-dashboard-access-routes.js → web/web-dashboard-access-routes.js} +17 -14
  100. package/dist/{web-dashboard-artifact-routes.js → web/web-dashboard-artifact-routes.js} +6 -2
  101. package/dist/{web-dashboard-assets.js → web/web-dashboard-assets.js} +25 -2
  102. package/dist/{web-dashboard-http.js → web/web-dashboard-http.js} +41 -5
  103. package/dist/{web-dashboard-pages.js → web/web-dashboard-pages.js} +95 -30
  104. package/dist/{web-dashboard-peer-routes.js → web/web-dashboard-peer-routes.js} +121 -7
  105. package/dist/{web-dashboard-runtime-routes.js → web/web-dashboard-runtime-routes.js} +8 -1
  106. package/dist/web/web-dashboard-security.js +14 -0
  107. package/dist/{web-dashboard-session-routes.js → web/web-dashboard-session-routes.js} +29 -13
  108. package/dist/web/web-dashboard-ui.js +56 -0
  109. package/dist/{web-dashboard.js → web/web-dashboard.js} +132 -48
  110. package/dist/web/web-performance.js +62 -0
  111. package/dist/web/web-rate-limit.js +19 -0
  112. package/dist/{web-state.js → web/web-state.js} +107 -9
  113. package/dist/webui-assets/dashboard.css +398 -49
  114. package/dist/webui-assets/dashboard.js +1239 -103
  115. package/dist/webui-assets/favicon.ico +0 -0
  116. package/dist/webui-assets/favicon.png +0 -0
  117. package/dist/webui-assets/logo.png +0 -0
  118. package/package.json +6 -3
  119. package/plugins/nordrelay/scripts/nordrelay.mjs +346 -12
  120. package/plugins/nordrelay/scripts/service-installer.mjs +183 -0
  121. package/{launchd/start.sh → scripts/launchd-start.sh} +1 -1
  122. package/scripts/postinstall.mjs +122 -0
  123. package/dist/relay-runtime.js +0 -1916
  124. package/dist/runtime-cache.js +0 -57
  125. package/dist/web-dashboard-ui.js +0 -20
  126. /package/dist/{user-management-crypto.js → access/user-management-crypto.js} +0 -0
  127. /package/dist/{user-management-normalize.js → access/user-management-normalize.js} +0 -0
  128. /package/dist/{user-management-types.js → access/user-management-types.js} +0 -0
  129. /package/dist/{claude-code-auth.js → agents/claude-code/claude-code-auth.js} +0 -0
  130. /package/dist/{claude-code-launch.js → agents/claude-code/claude-code-launch.js} +0 -0
  131. /package/dist/{claude-code-state.js → agents/claude-code/claude-code-state.js} +0 -0
  132. /package/dist/{codex-auth.js → agents/codex/codex-auth.js} +0 -0
  133. /package/dist/{codex-config.js → agents/codex/codex-config.js} +0 -0
  134. /package/dist/{codex-launch.js → agents/codex/codex-launch.js} +0 -0
  135. /package/dist/{codex-state.js → agents/codex/codex-state.js} +0 -0
  136. /package/dist/{hermes-api.js → agents/hermes/hermes-api.js} +0 -0
  137. /package/dist/{hermes-auth.js → agents/hermes/hermes-auth.js} +0 -0
  138. /package/dist/{hermes-state.js → agents/hermes/hermes-state.js} +0 -0
  139. /package/dist/{openclaw-auth.js → agents/openclaw/openclaw-auth.js} +0 -0
  140. /package/dist/{openclaw-gateway.js → agents/openclaw/openclaw-gateway.js} +0 -0
  141. /package/dist/{openclaw-state.js → agents/openclaw/openclaw-state.js} +0 -0
  142. /package/dist/{pi-auth.js → agents/pi/pi-auth.js} +0 -0
  143. /package/dist/{pi-rpc.js → agents/pi/pi-rpc.js} +0 -0
  144. /package/dist/{pi-state.js → agents/pi/pi-state.js} +0 -0
  145. /package/dist/{agent-adapter.js → agents/shared/agent-adapter.js} +0 -0
  146. /package/dist/{agent.js → agents/shared/agent.js} +0 -0
  147. /package/dist/{artifacts.js → artifacts/artifacts.js} +0 -0
  148. /package/dist/{attachments.js → artifacts/attachments.js} +0 -0
  149. /package/dist/{voice.js → artifacts/voice.js} +0 -0
  150. /package/dist/{discord-rate-limit.js → channels/discord/discord-rate-limit.js} +0 -0
  151. /package/dist/{channel-adapter.js → channels/shared/channel-adapter.js} +0 -0
  152. /package/dist/{relay-runtime-types.js → channels/shared/channel-bridge-state.js} +0 -0
  153. /package/dist/{channel-command-catalog.js → channels/shared/channel-command-catalog.js} +0 -0
  154. /package/dist/{channel-command-core.js → channels/shared/channel-command-core.js} +0 -0
  155. /package/dist/{channel-prompt-engine.js → channels/shared/channel-prompt-engine.js} +0 -0
  156. /package/dist/{channel-runtime.js → channels/shared/channel-runtime.js} +0 -0
  157. /package/dist/{channel-turn-lifecycle.js → channels/shared/channel-turn-lifecycle.js} +0 -0
  158. /package/dist/{slack-rate-limit.js → channels/slack/slack-rate-limit.js} +0 -0
  159. /package/dist/{telegram-command-types.js → channels/telegram/telegram-command-types.js} +0 -0
  160. /package/dist/{telegram-rate-limit.js → channels/telegram/telegram-rate-limit.js} +0 -0
  161. /package/dist/{activity-events.js → core/activity-events.js} +0 -0
  162. /package/dist/{error-messages.js → core/error-messages.js} +0 -0
  163. /package/dist/{format.js → core/format.js} +0 -0
  164. /package/dist/{logger.js → core/logger.js} +0 -0
  165. /package/dist/{redaction.js → core/redaction.js} +0 -0
  166. /package/dist/{settings-service.js → core/settings-service.js} +0 -0
  167. /package/dist/{settings-wizard-test.js → core/settings-wizard-test.js} +0 -0
  168. /package/dist/{workspace-policy.js → core/workspace-policy.js} +0 -0
  169. /package/dist/{peer-auth.js → peers/peer-auth.js} +0 -0
  170. /package/dist/{peer-client.js → peers/peer-client.js} +0 -0
  171. /package/dist/{peer-context.js → peers/peer-context.js} +0 -0
  172. /package/dist/{peer-readiness.js → peers/peer-readiness.js} +0 -0
  173. /package/dist/{web-api-types.js → runtime/relay-runtime-delegate.js} +0 -0
  174. /package/dist/{remote-prompt.js → runtime/remote-prompt.js} +0 -0
  175. /package/dist/{bot-preferences.js → state/bot-preferences.js} +0 -0
  176. /package/dist/{job-store.js → state/job-store.js} +0 -0
  177. /package/dist/{persistence.js → state/persistence.js} +0 -0
  178. /package/dist/{state-backend.js → state/state-backend.js} +0 -0
  179. /package/dist/{zip-writer.js → support/zip-writer.js} +0 -0
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordbyte/nordrelay",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Remote control plane for coding agents across messaging channels.",
5
5
  "type": "module",
6
6
  "author": "Ricardo",
@@ -34,7 +34,8 @@
34
34
  "files": [
35
35
  "dist/",
36
36
  "plugins/",
37
- "launchd/",
37
+ "scripts/postinstall.mjs",
38
+ "scripts/launchd-start.sh",
38
39
  ".env.example",
39
40
  "Dockerfile",
40
41
  "docker-compose.yml"
@@ -43,14 +44,16 @@
43
44
  "api:check": "node --import tsx scripts/generate-web-api-routes.mjs --check",
44
45
  "api:generate": "node --import tsx scripts/generate-web-api-routes.mjs",
45
46
  "build": "node scripts/clean-dist.mjs && npm run api:generate && tsc && node scripts/build-web-assets.mjs",
46
- "check": "node --check plugins/nordrelay/scripts/nordrelay.mjs && npm run api:check && tsc --noEmit && npm run webui:check && node scripts/build-web-assets.mjs --check && node --import tsx scripts/generate-env-example.mjs --check",
47
+ "check": "node --check plugins/nordrelay/scripts/nordrelay.mjs && node --check plugins/nordrelay/scripts/service-installer.mjs && npm run api:check && tsc --noEmit && npm run webui:check && node scripts/build-web-assets.mjs --check && node --import tsx scripts/generate-env-example.mjs --check && npm run size:check",
47
48
  "dev": "tsx src/index.ts",
48
49
  "env:check": "node --import tsx scripts/generate-env-example.mjs --check",
49
50
  "env:generate": "node --import tsx scripts/generate-env-example.mjs",
50
51
  "foreground": "node plugins/nordrelay/scripts/nordrelay.mjs foreground",
52
+ "postinstall": "node scripts/postinstall.mjs",
51
53
  "prepack": "npm run build",
52
54
  "prepublishOnly": "npm run check && npm test && npm run build",
53
55
  "security:audit": "npm audit --audit-level=high",
56
+ "size:check": "node scripts/check-module-size.mjs",
54
57
  "sbom": "npm sbom --json > sbom.json",
55
58
  "status": "node plugins/nordrelay/scripts/nordrelay.mjs status",
56
59
  "start": "node plugins/nordrelay/scripts/nordrelay.mjs start",
@@ -6,8 +6,15 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import process from "node:process";
8
8
  import readline from "node:readline/promises";
9
- import { spawn } from "node:child_process";
9
+ import { spawn, spawnSync } from "node:child_process";
10
10
  import { fileURLToPath, pathToFileURL } from "node:url";
11
+ import {
12
+ buildLaunchdServiceSpec,
13
+ buildSystemdUserServiceSpec,
14
+ buildWindowsTaskServiceSpec,
15
+ parseServiceFlags,
16
+ serviceInstallSpec,
17
+ } from "./service-installer.mjs";
11
18
 
12
19
  const FALLBACK_VERSION = "0.3.1";
13
20
  const require = createRequire(import.meta.url);
@@ -238,6 +245,7 @@ function formatDashboardUrl(endpoint) {
238
245
  async function commandStart(options, settings = {}) {
239
246
  await mkdirp(options.home);
240
247
  loadEnvFiles(options.home);
248
+ warnIfCliPathMissing();
241
249
  await prepareRuntimeForLaunch(options);
242
250
  const dashboard = resolveDashboardEndpoint(options);
243
251
 
@@ -431,6 +439,46 @@ async function commandStatus(options) {
431
439
  if (state.error) console.log(`Error: ${state.error}`);
432
440
  }
433
441
 
442
+ function cliPathDiagnostics() {
443
+ const resolved = findExecutable(APP_NAME);
444
+ const globalBin = resolveNpmGlobalBinDir();
445
+ const candidate = globalBin ? path.join(globalBin, process.platform === "win32" ? `${APP_NAME}.cmd` : APP_NAME) : null;
446
+ const pathContainsGlobalBin = globalBin ? pathListIncludes(globalBin) : false;
447
+ const expected = [candidate, SCRIPT_PATH].filter(Boolean);
448
+ const resolvedKnown = Boolean(resolved && expected.some((item) => pathsEqualOrLinked(resolved, item)));
449
+ const hint = globalBin
450
+ ? process.platform === "win32"
451
+ ? `Add ${globalBin} to PATH and reopen the terminal.`
452
+ : `Add ${globalBin} to PATH, for example: export PATH="${globalBin}:$PATH"`
453
+ : "Ensure the npm global bin directory is on PATH.";
454
+ return {
455
+ ok: Boolean(resolved),
456
+ resolved,
457
+ globalBin,
458
+ pathContainsGlobalBin,
459
+ expected: candidate,
460
+ resolvedKnown,
461
+ detail: resolved
462
+ ? resolvedKnown
463
+ ? resolved
464
+ : `${resolved} (different command target; current wrapper: ${SCRIPT_PATH})`
465
+ : `not found on PATH${globalBin ? `; npm global bin: ${globalBin}` : ""}`,
466
+ hint,
467
+ };
468
+ }
469
+
470
+ function warnIfCliPathMissing() {
471
+ if (envFlag("NORDRELAY_SUPPRESS_PATH_WARNING")) {
472
+ return;
473
+ }
474
+ const diagnostics = cliPathDiagnostics();
475
+ if (diagnostics.ok) {
476
+ return;
477
+ }
478
+ console.warn(`Warning: \`${APP_NAME}\` is not available on PATH.`);
479
+ console.warn(`Hint: ${diagnostics.hint}`);
480
+ }
481
+
434
482
  async function commandUpdate(options) {
435
483
  await mkdirp(options.home);
436
484
  loadEnvFiles(options.home);
@@ -671,6 +719,7 @@ function quoteWindowsCmdArg(value) {
671
719
 
672
720
  async function commandInit(options) {
673
721
  await mkdirp(options.home);
722
+ warnIfCliPathMissing();
674
723
  const envPath = path.join(options.home, "nordrelay.env");
675
724
  const userStore = await createUserStore(options.home);
676
725
  if (fs.existsSync(envPath) && !options.force) {
@@ -818,7 +867,7 @@ function parsePeerFlags(argv) {
818
867
  const copy = [...argv];
819
868
  const subcommand = copy[0] && !copy[0].startsWith("-") ? copy.shift() : "list";
820
869
  const flags = { subcommand, url: undefined };
821
- if (["add", "test", "check", "revoke"].includes(subcommand) && copy[0] && !copy[0].startsWith("-")) {
870
+ if (["add", "test", "check", "revoke", "trust", "rotate"].includes(subcommand) && copy[0] && !copy[0].startsWith("-")) {
822
871
  flags.url = copy.shift();
823
872
  flags.id = flags.url;
824
873
  }
@@ -881,6 +930,7 @@ async function commandPeer(options) {
881
930
  if (peer.lastSeenAt) console.log(` Last seen: ${peer.lastSeenAt}`);
882
931
  if (peer.lastLatencyMs !== undefined) console.log(` Latency: ${peer.lastLatencyMs}ms`);
883
932
  if (peer.remoteVersion) console.log(` Remote version: ${peer.remoteVersion}`);
933
+ if (peer.trustStatus) console.log(` Trust: ${peer.trustStatus}${peer.trustWarnings?.length ? ` (${peer.trustWarnings.join("; ")})` : ""}`);
884
934
  if (peer.lastError) console.log(` Last error: ${peer.lastError}`);
885
935
  }
886
936
  return;
@@ -957,7 +1007,187 @@ async function commandPeer(options) {
957
1007
  return;
958
1008
  }
959
1009
 
960
- throw new Error("Usage: nordrelay peer [identity|list|invite|add|test|check|revoke]");
1010
+ if (flags.subcommand === "trust") {
1011
+ const id = flags.id || await ask(null, "Peer id", "");
1012
+ const peer = store.get(id);
1013
+ if (!peer?.url) throw new Error("Peer URL is required before TLS trust can be updated.");
1014
+ const probe = await clientMod.checkPeerIdentityEndpoint(peer.url, { timeoutMs: 5000 });
1015
+ if (!probe.ok || !probe.identity) throw new Error(`Peer identity could not be verified: ${probe.detail}`);
1016
+ if (probe.identity.nodeId !== peer.nodeId || probe.identity.publicKey !== peer.publicKey || probe.identity.fingerprint !== peer.fingerprint) {
1017
+ throw new Error("Peer identity changed. Re-pair this peer instead of trusting the TLS fingerprint.");
1018
+ }
1019
+ const updated = store.updatePeerTlsFingerprint(peer.id, probe.tlsFingerprint);
1020
+ console.log(`Trusted TLS fingerprint for ${updated.name}: ${updated.tlsFingerprint || "-"}`);
1021
+ return;
1022
+ }
1023
+
1024
+ if (flags.subcommand === "rotate") {
1025
+ const id = flags.id || await ask(null, "Peer id", "");
1026
+ const url = process.env.NORDRELAY_PEER_PUBLIC_URL || `${process.env.NORDRELAY_PEER_TLS_ENABLED === "false" ? "http" : "https"}://${process.env.NORDRELAY_PEER_HOST || "127.0.0.1"}:${process.env.NORDRELAY_PEER_PORT || "31979"}`;
1027
+ const created = store.createRotationInvitation(id, { expiresInMs: Number.isFinite(flags.expiresMinutes) ? flags.expiresMinutes * 60 * 1000 : undefined });
1028
+ console.log(`Rotation invite for ${created.peer.name} (${created.peer.id}).`);
1029
+ console.log(`Pairing code: ${created.code}`);
1030
+ console.log(`Expires: ${created.invitation.expiresAt}`);
1031
+ console.log(`Command: nordrelay peer add ${url} --code ${created.code}`);
1032
+ return;
1033
+ }
1034
+
1035
+ throw new Error("Usage: nordrelay peer [identity|list|invite|add|test|check|trust|rotate|revoke]");
1036
+ }
1037
+
1038
+ async function commandService(options) {
1039
+ await mkdirp(options.home);
1040
+ loadEnvFiles(options.home);
1041
+ warnIfCliPathMissing();
1042
+ const flags = parseServiceFlags(options.rawFlags);
1043
+ const specOptions = { ...options, scriptPath: SCRIPT_PATH };
1044
+
1045
+ if (flags.subcommand === "install") {
1046
+ if (flags.dryRun) {
1047
+ printServiceInstallDryRun(specOptions, flags);
1048
+ return;
1049
+ }
1050
+ if (flags.platform === "darwin") {
1051
+ await installLaunchdService(specOptions, flags);
1052
+ return;
1053
+ }
1054
+ if (flags.platform === "win32") {
1055
+ await installWindowsTask(specOptions, flags);
1056
+ return;
1057
+ }
1058
+ await installSystemdUserService(specOptions, flags);
1059
+ return;
1060
+ }
1061
+
1062
+ if (flags.subcommand === "uninstall" || flags.subcommand === "remove") {
1063
+ if (flags.platform === "darwin") {
1064
+ await uninstallLaunchdService(flags);
1065
+ return;
1066
+ }
1067
+ if (flags.platform === "win32") {
1068
+ await uninstallWindowsTask(flags);
1069
+ return;
1070
+ }
1071
+ await uninstallSystemdUserService(flags);
1072
+ return;
1073
+ }
1074
+
1075
+ if (flags.subcommand === "status") {
1076
+ await commandServiceStatus(flags);
1077
+ return;
1078
+ }
1079
+
1080
+ throw new Error("Usage: nordrelay service [install|uninstall|status] [--no-start] [--name <name>] [--label <label>]");
1081
+ }
1082
+
1083
+ async function installSystemdUserService(options, flags) {
1084
+ const spec = buildSystemdUserServiceSpec(options, flags);
1085
+ const unitDir = path.dirname(spec.path);
1086
+ const unitPath = spec.path;
1087
+ await mkdirp(unitDir);
1088
+ await fsp.writeFile(unitPath, spec.content);
1089
+ console.log(`Installed systemd user service: ${unitPath}`);
1090
+ for (const command of spec.commands) {
1091
+ runPlatformCommand(command.command, command.args, command.label, command.settings);
1092
+ }
1093
+ console.log(`Status: nordrelay service status`);
1094
+ }
1095
+
1096
+ async function uninstallSystemdUserService(flags) {
1097
+ runPlatformCommand("systemctl", ["--user", "disable", "--now", `${flags.name}.service`], `Disable ${flags.name}.service`);
1098
+ const unitPath = path.join(os.homedir(), ".config", "systemd", "user", `${flags.name}.service`);
1099
+ await fsp.rm(unitPath, { force: true });
1100
+ runPlatformCommand("systemctl", ["--user", "daemon-reload"], "Reload systemd user units");
1101
+ console.log(`Removed systemd user service: ${unitPath}`);
1102
+ }
1103
+
1104
+ async function installLaunchdService(options, flags) {
1105
+ const spec = buildLaunchdServiceSpec(options, flags);
1106
+ const launchAgentsDir = path.dirname(spec.path);
1107
+ const plistPath = spec.path;
1108
+ await mkdirp(launchAgentsDir);
1109
+ await fsp.writeFile(plistPath, spec.content);
1110
+ console.log(`Installed launchd service: ${plistPath}`);
1111
+ for (const command of spec.commands) {
1112
+ runPlatformCommand(command.command, command.args, command.label, command.settings);
1113
+ }
1114
+ if (!flags.start) {
1115
+ const domain = launchdDomain();
1116
+ console.log(`Start later with: launchctl bootstrap ${domain} ${plistPath}`);
1117
+ }
1118
+ }
1119
+
1120
+ async function uninstallLaunchdService(flags) {
1121
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${flags.label}.plist`);
1122
+ const domain = `gui/${process.getuid?.() ?? ""}`;
1123
+ runPlatformCommand("launchctl", ["bootout", domain, plistPath], `Unload ${flags.label}`, { allowFailure: true });
1124
+ await fsp.rm(plistPath, { force: true });
1125
+ console.log(`Removed launchd service: ${plistPath}`);
1126
+ }
1127
+
1128
+ async function installWindowsTask(options, flags) {
1129
+ const spec = buildWindowsTaskServiceSpec(options, flags);
1130
+ for (const command of spec.commands) {
1131
+ runPlatformCommand(command.command, command.args, command.label, command.settings);
1132
+ }
1133
+ console.log(`Installed Windows task: ${flags.name}`);
1134
+ }
1135
+
1136
+ async function uninstallWindowsTask(flags) {
1137
+ runPlatformCommand("schtasks", ["/Delete", "/F", "/TN", flags.name], `Delete Windows task ${flags.name}`, { allowFailure: true });
1138
+ console.log(`Removed Windows task: ${flags.name}`);
1139
+ }
1140
+
1141
+ async function commandServiceStatus(flags) {
1142
+ if (process.platform === "darwin") {
1143
+ const domain = `gui/${process.getuid?.() ?? ""}`;
1144
+ runPlatformCommand("launchctl", ["print", `${domain}/${flags.label}`], `launchd status ${flags.label}`, { allowFailure: true });
1145
+ return;
1146
+ }
1147
+ if (process.platform === "win32") {
1148
+ runPlatformCommand("schtasks", ["/Query", "/TN", flags.name], `Windows task status ${flags.name}`, { allowFailure: true });
1149
+ return;
1150
+ }
1151
+ runPlatformCommand("systemctl", ["--user", "status", `${flags.name}.service`, "--no-pager"], `systemd user status ${flags.name}.service`, { allowFailure: true });
1152
+ }
1153
+
1154
+ function printServiceInstallDryRun(options, flags) {
1155
+ const spec = serviceInstallSpec(options, flags);
1156
+ console.log(`Service install dry-run (${spec.platform})`);
1157
+ console.log(`Target: ${spec.path}`);
1158
+ if (spec.content) {
1159
+ console.log("--- file content ---");
1160
+ console.log(spec.content.trimEnd());
1161
+ }
1162
+ console.log("--- commands ---");
1163
+ for (const command of spec.commands) {
1164
+ console.log(formatCommand(command.command, command.args));
1165
+ }
1166
+ }
1167
+
1168
+ function launchdDomain() {
1169
+ return `gui/${process.getuid?.() ?? ""}`;
1170
+ }
1171
+
1172
+ function runPlatformCommand(command, args, label, settings = {}) {
1173
+ const resolved = findExecutable(command);
1174
+ if (!resolved) {
1175
+ console.log(`${label}: ${command} not found. Run this step manually if this platform service manager is available.`);
1176
+ return false;
1177
+ }
1178
+ const useShell = isWindowsShellScript(resolved);
1179
+ console.log(`${label}: ${formatCommand(resolved, args)}`);
1180
+ const result = spawnSync(useShell ? formatShellCommand(resolved, args) : resolved, useShell ? [] : args, {
1181
+ cwd: RUNTIME_ROOT,
1182
+ env: process.env,
1183
+ shell: useShell,
1184
+ stdio: "inherit",
1185
+ windowsHide: false,
1186
+ });
1187
+ if (result.status !== 0 && !settings.allowFailure) {
1188
+ throw new Error(`${label} failed with exit code ${result.status ?? "unknown"}`);
1189
+ }
1190
+ return result.status === 0;
961
1191
  }
962
1192
 
963
1193
  function parseUserFlags(argv) {
@@ -1092,6 +1322,11 @@ async function commandDoctor(options) {
1092
1322
  const userSnapshot = userStore?.snapshot();
1093
1323
  const checks = [];
1094
1324
  checks.push(check("Node.js >= 22", Number.parseInt(process.versions.node.split(".")[0], 10) >= 22, process.version));
1325
+ const cliPath = cliPathDiagnostics();
1326
+ checks.push(check("NordRelay CLI on PATH", cliPath.ok, cliPath.ok ? cliPath.detail : `${cliPath.detail}; ${cliPath.hint}`, "warn"));
1327
+ if (cliPath.globalBin) {
1328
+ checks.push(check("npm global bin on PATH", cliPath.pathContainsGlobalBin, cliPath.globalBin, "warn"));
1329
+ }
1095
1330
  const telegramRequested = process.env.TELEGRAM_ENABLED !== "false";
1096
1331
  const discordRequested = process.env.DISCORD_ENABLED === "true";
1097
1332
  const slackRequested = process.env.SLACK_ENABLED === "true";
@@ -1245,11 +1480,20 @@ async function checkOpenClawGateway() {
1245
1480
  async function commandWeb(options) {
1246
1481
  await mkdirp(options.home);
1247
1482
  loadEnvFiles(options.home);
1483
+ warnIfCliPathMissing();
1248
1484
  await prepareRuntimeForLaunch(options);
1249
1485
  await ensureConnectorStartedForWeb(options);
1250
1486
  await startWebDashboard(options, { detached: false });
1251
1487
  }
1252
1488
 
1489
+ async function commandServiceRun(options) {
1490
+ await mkdirp(options.home);
1491
+ loadEnvFiles(options.home);
1492
+ await prepareRuntimeForLaunch(options);
1493
+ await ensureConnectorStartedForWeb(options);
1494
+ await startWebDashboard(options, { detached: false, stopConnectorOnExit: true });
1495
+ }
1496
+
1253
1497
  async function startWebDashboard(options, settings = {}) {
1254
1498
  await mkdirp(options.home);
1255
1499
  loadEnvFiles(options.home);
@@ -1335,6 +1579,9 @@ async function startWebDashboard(options, settings = {}) {
1335
1579
  exitCode: exit.code,
1336
1580
  signal: exit.signal,
1337
1581
  });
1582
+ if (settings.stopConnectorOnExit) {
1583
+ await commandStop(options, { keepWeb: true });
1584
+ }
1338
1585
  if (exit.signal) {
1339
1586
  process.kill(process.pid, exit.signal);
1340
1587
  return;
@@ -1486,7 +1733,7 @@ function runtimeBuildStatus() {
1486
1733
  ]);
1487
1734
  const distTargets = [
1488
1735
  path.join(RUNTIME_ROOT, "dist", "index.js"),
1489
- path.join(RUNTIME_ROOT, "dist", "web-dashboard.js"),
1736
+ path.join(RUNTIME_ROOT, "dist", "web", "web-dashboard.js"),
1490
1737
  path.join(RUNTIME_ROOT, "dist", "webui-assets", "dashboard.js"),
1491
1738
  path.join(RUNTIME_ROOT, "dist", "webui-assets", "dashboard.css"),
1492
1739
  ];
@@ -1583,12 +1830,12 @@ async function runInteractiveStep(label, command, args, settings = {}) {
1583
1830
  }
1584
1831
 
1585
1832
  async function resolveWebRuntimeEntry() {
1586
- const distEntry = path.join(RUNTIME_ROOT, "dist", "web-dashboard.js");
1833
+ const distEntry = path.join(RUNTIME_ROOT, "dist", "web", "web-dashboard.js");
1587
1834
  if (fs.existsSync(distEntry)) {
1588
1835
  return { command: process.execPath, args: [distEntry] };
1589
1836
  }
1590
1837
 
1591
- const tsEntry = path.join(RUNTIME_ROOT, "src", "web-dashboard.ts");
1838
+ const tsEntry = path.join(RUNTIME_ROOT, "src", "web", "web-dashboard.ts");
1592
1839
  const tsxBin = path.join(RUNTIME_ROOT, "node_modules", ".bin", process.platform === "win32" ? "tsx.cmd" : "tsx");
1593
1840
  if (fs.existsSync(tsEntry) && fs.existsSync(tsxBin)) {
1594
1841
  return { command: tsxBin, args: [tsEntry] };
@@ -1712,7 +1959,7 @@ function findExecutable(command, pathValue = process.env.PATH, pathextValue = pr
1712
1959
  if (command.includes(path.sep) && fs.existsSync(command)) return command;
1713
1960
  const paths = (pathValue || "").split(path.delimiter);
1714
1961
  const extensions = process.platform === "win32"
1715
- ? ["", ...(pathextValue || ".COM;.EXE;.BAT;.CMD").split(";")]
1962
+ ? windowsExecutableExtensions(pathextValue)
1716
1963
  : [""];
1717
1964
  for (const dir of paths) {
1718
1965
  for (const extension of extensions) {
@@ -1723,6 +1970,79 @@ function findExecutable(command, pathValue = process.env.PATH, pathextValue = pr
1723
1970
  return null;
1724
1971
  }
1725
1972
 
1973
+ function resolveNpmGlobalBinDir(env = process.env) {
1974
+ const prefix = resolveNpmGlobalPrefix(env);
1975
+ if (!prefix) {
1976
+ return null;
1977
+ }
1978
+ return process.platform === "win32" ? prefix : path.join(prefix, "bin");
1979
+ }
1980
+
1981
+ function resolveNpmGlobalPrefix(env = process.env) {
1982
+ if (env.npm_config_prefix) {
1983
+ return path.resolve(env.npm_config_prefix);
1984
+ }
1985
+ const npm = resolveNpmSpawnCommand(env);
1986
+ if (!npm) {
1987
+ return null;
1988
+ }
1989
+ const command = npm.shell
1990
+ ? formatShellCommand(npm.command, [...npm.argsPrefix, "prefix", "-g"])
1991
+ : npm.command;
1992
+ const args = npm.shell ? [] : [...npm.argsPrefix, "prefix", "-g"];
1993
+ const result = spawnSync(command, args, {
1994
+ cwd: os.homedir(),
1995
+ env,
1996
+ shell: npm.shell,
1997
+ encoding: "utf8",
1998
+ windowsHide: true,
1999
+ timeout: 5000,
2000
+ });
2001
+ if (result.status !== 0) {
2002
+ return null;
2003
+ }
2004
+ const prefix = String(result.stdout || "").trim().split(/\r?\n/).at(-1)?.trim();
2005
+ return prefix ? path.resolve(prefix) : null;
2006
+ }
2007
+
2008
+ function pathListIncludes(directory, pathValue = process.env.PATH) {
2009
+ const normalized = normalizePathForCompare(directory);
2010
+ return (pathValue || "")
2011
+ .split(path.delimiter)
2012
+ .filter(Boolean)
2013
+ .some((entry) => normalizePathForCompare(entry) === normalized);
2014
+ }
2015
+
2016
+ function pathsEqualOrLinked(left, right) {
2017
+ if (!left || !right) {
2018
+ return false;
2019
+ }
2020
+ const normalizedLeft = normalizePathForCompare(left);
2021
+ const normalizedRight = normalizePathForCompare(right);
2022
+ if (normalizedLeft === normalizedRight) {
2023
+ return true;
2024
+ }
2025
+ try {
2026
+ return normalizePathForCompare(fs.realpathSync(left)) === normalizePathForCompare(fs.realpathSync(right));
2027
+ } catch {
2028
+ return false;
2029
+ }
2030
+ }
2031
+
2032
+ function normalizePathForCompare(value) {
2033
+ const resolved = path.resolve(value || "");
2034
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
2035
+ }
2036
+
2037
+ function windowsExecutableExtensions(pathextValue) {
2038
+ const pathext = (pathextValue || ".COM;.EXE;.BAT;.CMD")
2039
+ .split(";")
2040
+ .map((extension) => extension.trim())
2041
+ .filter(Boolean)
2042
+ .map((extension) => extension.startsWith(".") ? extension : `.${extension}`);
2043
+ return [...new Set([...pathext, ""])];
2044
+ }
2045
+
1726
2046
  function isWindowsShellScript(filePath) {
1727
2047
  return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(filePath);
1728
2048
  }
@@ -1783,6 +2103,7 @@ function printHelp() {
1783
2103
  console.log(" init Create local config and first admin user");
1784
2104
  console.log(" user Manage users, groups, and channel links");
1785
2105
  console.log(" peer Manage secure NordRelay peer federation");
2106
+ console.log(" service Install, remove, or inspect the OS service");
1786
2107
  console.log(" doctor Validate the local setup");
1787
2108
  console.log(" web, dashboard Start the WebUI and connector");
1788
2109
  console.log(" start Start the connector");
@@ -1797,6 +2118,7 @@ function printHelp() {
1797
2118
  console.log(" --home <path> Runtime home directory");
1798
2119
  console.log(" --host <host> WebUI bind host");
1799
2120
  console.log(" --port <port> WebUI port");
2121
+ console.log(" service install --dry-run [--platform linux|darwin|win32]");
1800
2122
  console.log(" --build Build source runtime before start/web/restart");
1801
2123
  console.log(" --force Overwrite existing config during init");
1802
2124
  console.log(" --help, -h Show this help");
@@ -1815,6 +2137,7 @@ async function main() {
1815
2137
  if (options.command === "init") return commandInit(options);
1816
2138
  if (options.command === "user") return commandUser(options);
1817
2139
  if (options.command === "peer") return commandPeer(options);
2140
+ if (options.command === "service") return commandService(options);
1818
2141
  if (options.command === "doctor") return commandDoctor(options);
1819
2142
  if (options.command === "update") return commandUpdate(options);
1820
2143
  if (options.command === "web" || options.command === "dashboard") return commandWeb(options);
@@ -1831,18 +2154,29 @@ async function main() {
1831
2154
  return;
1832
2155
  }
1833
2156
  if (options.command === "foreground") return commandForeground(options);
2157
+ if (options.command === "service-run") return commandServiceRun(options);
1834
2158
  if (options.command === "--version" || options.command === "version") {
1835
2159
  console.log(`${APP_NAME} ${VERSION}`);
1836
2160
  return;
1837
2161
  }
1838
2162
 
1839
2163
  console.error(`Unknown command: ${options.command}`);
1840
- console.error("Usage: nordrelay [init|user|peer|doctor|web|start|stop|restart|status|update|foreground|version]");
2164
+ console.error("Usage: nordrelay [init|user|peer|service|doctor|web|start|stop|restart|status|update|foreground|version]");
1841
2165
  console.error("Run `nordrelay --help` for details.");
1842
2166
  process.exitCode = 2;
1843
2167
  }
1844
2168
 
1845
- main().catch((error) => {
1846
- console.error(error instanceof Error ? error.message : String(error));
1847
- process.exitCode = 1;
1848
- });
2169
+ export {
2170
+ buildLaunchdServiceSpec,
2171
+ buildSystemdUserServiceSpec,
2172
+ buildWindowsTaskServiceSpec,
2173
+ parseServiceFlags,
2174
+ serviceInstallSpec,
2175
+ };
2176
+
2177
+ if (process.argv[1] && path.resolve(process.argv[1]) === SCRIPT_PATH) {
2178
+ main().catch((error) => {
2179
+ console.error(error instanceof Error ? error.message : String(error));
2180
+ process.exitCode = 1;
2181
+ });
2182
+ }