@nordbyte/nordrelay 0.8.1 → 0.8.2

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 (173) hide show
  1. package/.env.example +9 -0
  2. package/README.md +81 -1206
  3. package/dist/{access-control.js → access/access-control.js} +1 -1
  4. package/dist/{audit-log.js → access/audit-log.js} +2 -2
  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} +164 -424
  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/{channel-turn-service.js → channels/shared/channel-turn-service.js} +2 -2
  40. package/dist/{context-key.js → channels/shared/context-key.js} +1 -1
  41. package/dist/{session-format.js → channels/shared/session-format.js} +2 -2
  42. package/dist/{slack-artifacts.js → channels/slack/slack-artifacts.js} +4 -4
  43. package/dist/{slack-bot.js → channels/slack/slack-bot.js} +159 -294
  44. package/dist/{slack-channel-runtime.js → channels/slack/slack-channel-runtime.js} +2 -2
  45. package/dist/{slack-command-surface.js → channels/slack/slack-command-surface.js} +2 -2
  46. package/dist/{slack-diagnostics.js → channels/slack/slack-diagnostics.js} +2 -2
  47. package/dist/{bot-ui.js → channels/telegram/bot-ui.js} +1 -1
  48. package/dist/{bot.js → channels/telegram/bot.js} +178 -427
  49. package/dist/{telegram-access-commands.js → channels/telegram/telegram-access-commands.js} +3 -3
  50. package/dist/{telegram-access-middleware.js → channels/telegram/telegram-access-middleware.js} +4 -4
  51. package/dist/{telegram-agent-commands.js → channels/telegram/telegram-agent-commands.js} +9 -9
  52. package/dist/{telegram-artifact-commands.js → channels/telegram/telegram-artifact-commands.js} +4 -4
  53. package/dist/{telegram-channel-runtime.js → channels/telegram/telegram-channel-runtime.js} +2 -2
  54. package/dist/{telegram-command-menu.js → channels/telegram/telegram-command-menu.js} +1 -1
  55. package/dist/{telegram-diagnostics-command.js → channels/telegram/telegram-diagnostics-command.js} +7 -7
  56. package/dist/{telegram-general-commands.js → channels/telegram/telegram-general-commands.js} +4 -4
  57. package/dist/{telegram-operational-commands.js → channels/telegram/telegram-operational-commands.js} +5 -5
  58. package/dist/{telegram-output.js → channels/telegram/telegram-output.js} +2 -2
  59. package/dist/{telegram-preference-commands.js → channels/telegram/telegram-preference-commands.js} +3 -3
  60. package/dist/{telegram-queue-commands.js → channels/telegram/telegram-queue-commands.js} +6 -6
  61. package/dist/{telegram-support-command.js → channels/telegram/telegram-support-command.js} +4 -4
  62. package/dist/{telegram-update-commands.js → channels/telegram/telegram-update-commands.js} +5 -5
  63. package/dist/{config-metadata.js → core/config-metadata.js} +8 -0
  64. package/dist/{config.js → core/config.js} +11 -3
  65. package/dist/index.js +27 -23
  66. package/dist/peers/peer-discovery-jobs.js +206 -0
  67. package/dist/peers/peer-discovery.js +223 -0
  68. package/dist/peers/peer-health-monitor.js +49 -0
  69. package/dist/{peer-identity.js → peers/peer-identity.js} +50 -1
  70. package/dist/{peer-runtime-service.js → peers/peer-runtime-service.js} +29 -7
  71. package/dist/{peer-server.js → peers/peer-server.js} +3 -2
  72. package/dist/{peer-store.js → peers/peer-store.js} +80 -9
  73. package/dist/{peer-types.js → peers/peer-types.js} +9 -0
  74. package/dist/peers/peer-web-proxy-contract.js +127 -0
  75. package/dist/{metrics.js → runtime/metrics.js} +5 -3
  76. package/dist/{relay-artifact-service.js → runtime/relay-artifact-service.js} +1 -1
  77. package/dist/runtime/relay-auth-service.js +63 -0
  78. package/dist/runtime/relay-dashboard-service.js +139 -0
  79. package/dist/{relay-external-activity-monitor.js → runtime/relay-external-activity-monitor.js} +140 -53
  80. package/dist/runtime/relay-runtime-active-sessions.js +387 -0
  81. package/dist/runtime/relay-runtime-dashboard.js +201 -0
  82. package/dist/runtime/relay-runtime-prompt-queue-artifacts.js +307 -0
  83. package/dist/runtime/relay-runtime-sessions.js +623 -0
  84. package/dist/runtime/relay-runtime-types.js +1 -0
  85. package/dist/runtime/relay-runtime-updates-jobs.js +360 -0
  86. package/dist/runtime/relay-runtime.js +451 -0
  87. package/dist/runtime/runtime-cache.js +117 -0
  88. package/dist/{session-registry.js → state/session-registry.js} +3 -3
  89. package/dist/{operations.js → support/operations.js} +7 -7
  90. package/dist/{support-bundle.js → support/support-bundle.js} +1 -1
  91. package/dist/{web-api-contract.js → web/web-api-contract.js} +17 -3
  92. package/dist/web/web-api-types.js +1 -0
  93. package/dist/{web-dashboard-access-routes.js → web/web-dashboard-access-routes.js} +2 -2
  94. package/dist/{web-dashboard-assets.js → web/web-dashboard-assets.js} +24 -2
  95. package/dist/{web-dashboard-http.js → web/web-dashboard-http.js} +41 -5
  96. package/dist/{web-dashboard-pages.js → web/web-dashboard-pages.js} +37 -10
  97. package/dist/{web-dashboard-peer-routes.js → web/web-dashboard-peer-routes.js} +102 -7
  98. package/dist/web/web-dashboard-security.js +14 -0
  99. package/dist/{web-dashboard-session-routes.js → web/web-dashboard-session-routes.js} +12 -1
  100. package/dist/{web-dashboard.js → web/web-dashboard.js} +132 -48
  101. package/dist/web/web-performance.js +60 -0
  102. package/dist/web/web-rate-limit.js +19 -0
  103. package/dist/{web-state.js → web/web-state.js} +74 -5
  104. package/dist/webui-assets/dashboard.css +171 -10
  105. package/dist/webui-assets/dashboard.js +514 -48
  106. package/dist/webui-assets/favicon.ico +0 -0
  107. package/dist/webui-assets/favicon.png +0 -0
  108. package/dist/webui-assets/logo.png +0 -0
  109. package/package.json +4 -3
  110. package/plugins/nordrelay/scripts/nordrelay.mjs +13 -4
  111. package/{launchd/start.sh → scripts/launchd-start.sh} +1 -1
  112. package/dist/relay-runtime.js +0 -1916
  113. package/dist/runtime-cache.js +0 -57
  114. /package/dist/{user-management-crypto.js → access/user-management-crypto.js} +0 -0
  115. /package/dist/{user-management-normalize.js → access/user-management-normalize.js} +0 -0
  116. /package/dist/{user-management-types.js → access/user-management-types.js} +0 -0
  117. /package/dist/{claude-code-auth.js → agents/claude-code/claude-code-auth.js} +0 -0
  118. /package/dist/{claude-code-launch.js → agents/claude-code/claude-code-launch.js} +0 -0
  119. /package/dist/{claude-code-state.js → agents/claude-code/claude-code-state.js} +0 -0
  120. /package/dist/{codex-auth.js → agents/codex/codex-auth.js} +0 -0
  121. /package/dist/{codex-config.js → agents/codex/codex-config.js} +0 -0
  122. /package/dist/{codex-launch.js → agents/codex/codex-launch.js} +0 -0
  123. /package/dist/{codex-state.js → agents/codex/codex-state.js} +0 -0
  124. /package/dist/{hermes-api.js → agents/hermes/hermes-api.js} +0 -0
  125. /package/dist/{hermes-auth.js → agents/hermes/hermes-auth.js} +0 -0
  126. /package/dist/{hermes-state.js → agents/hermes/hermes-state.js} +0 -0
  127. /package/dist/{openclaw-auth.js → agents/openclaw/openclaw-auth.js} +0 -0
  128. /package/dist/{openclaw-gateway.js → agents/openclaw/openclaw-gateway.js} +0 -0
  129. /package/dist/{openclaw-state.js → agents/openclaw/openclaw-state.js} +0 -0
  130. /package/dist/{pi-auth.js → agents/pi/pi-auth.js} +0 -0
  131. /package/dist/{pi-rpc.js → agents/pi/pi-rpc.js} +0 -0
  132. /package/dist/{pi-state.js → agents/pi/pi-state.js} +0 -0
  133. /package/dist/{agent-adapter.js → agents/shared/agent-adapter.js} +0 -0
  134. /package/dist/{agent.js → agents/shared/agent.js} +0 -0
  135. /package/dist/{artifacts.js → artifacts/artifacts.js} +0 -0
  136. /package/dist/{attachments.js → artifacts/attachments.js} +0 -0
  137. /package/dist/{voice.js → artifacts/voice.js} +0 -0
  138. /package/dist/{discord-rate-limit.js → channels/discord/discord-rate-limit.js} +0 -0
  139. /package/dist/{channel-adapter.js → channels/shared/channel-adapter.js} +0 -0
  140. /package/dist/{relay-runtime-types.js → channels/shared/channel-bridge-state.js} +0 -0
  141. /package/dist/{channel-command-catalog.js → channels/shared/channel-command-catalog.js} +0 -0
  142. /package/dist/{channel-command-core.js → channels/shared/channel-command-core.js} +0 -0
  143. /package/dist/{channel-prompt-engine.js → channels/shared/channel-prompt-engine.js} +0 -0
  144. /package/dist/{channel-runtime.js → channels/shared/channel-runtime.js} +0 -0
  145. /package/dist/{channel-turn-lifecycle.js → channels/shared/channel-turn-lifecycle.js} +0 -0
  146. /package/dist/{slack-rate-limit.js → channels/slack/slack-rate-limit.js} +0 -0
  147. /package/dist/{telegram-command-types.js → channels/telegram/telegram-command-types.js} +0 -0
  148. /package/dist/{telegram-rate-limit.js → channels/telegram/telegram-rate-limit.js} +0 -0
  149. /package/dist/{activity-events.js → core/activity-events.js} +0 -0
  150. /package/dist/{error-messages.js → core/error-messages.js} +0 -0
  151. /package/dist/{format.js → core/format.js} +0 -0
  152. /package/dist/{logger.js → core/logger.js} +0 -0
  153. /package/dist/{redaction.js → core/redaction.js} +0 -0
  154. /package/dist/{settings-service.js → core/settings-service.js} +0 -0
  155. /package/dist/{settings-wizard-test.js → core/settings-wizard-test.js} +0 -0
  156. /package/dist/{workspace-policy.js → core/workspace-policy.js} +0 -0
  157. /package/dist/{peer-auth.js → peers/peer-auth.js} +0 -0
  158. /package/dist/{peer-client.js → peers/peer-client.js} +0 -0
  159. /package/dist/{peer-context.js → peers/peer-context.js} +0 -0
  160. /package/dist/{peer-readiness.js → peers/peer-readiness.js} +0 -0
  161. /package/dist/{relay-queue-service.js → runtime/relay-queue-service.js} +0 -0
  162. /package/dist/{web-api-types.js → runtime/relay-runtime-delegate.js} +0 -0
  163. /package/dist/{relay-runtime-helpers.js → runtime/relay-runtime-helpers.js} +0 -0
  164. /package/dist/{remote-prompt.js → runtime/remote-prompt.js} +0 -0
  165. /package/dist/{bot-preferences.js → state/bot-preferences.js} +0 -0
  166. /package/dist/{job-store.js → state/job-store.js} +0 -0
  167. /package/dist/{persistence.js → state/persistence.js} +0 -0
  168. /package/dist/{prompt-store.js → state/prompt-store.js} +0 -0
  169. /package/dist/{state-backend.js → state/state-backend.js} +0 -0
  170. /package/dist/{zip-writer.js → support/zip-writer.js} +0 -0
  171. /package/dist/{web-dashboard-artifact-routes.js → web/web-dashboard-artifact-routes.js} +0 -0
  172. /package/dist/{web-dashboard-runtime-routes.js → web/web-dashboard-runtime-routes.js} +0 -0
  173. /package/dist/{web-dashboard-ui.js → web/web-dashboard-ui.js} +0 -0
@@ -0,0 +1,127 @@
1
+ import { WEB_API_ROUTE_DEFINITIONS } from "../web/web-api-contract.js";
2
+ const LOCAL_ONLY_ROUTE_PATHS = new Set([
3
+ "/api/auth/me",
4
+ "/api/dashboard/logout",
5
+ "/api/permissions",
6
+ "/api/settings",
7
+ "/api/settings/wizard/test",
8
+ "/api/peers",
9
+ "/api/peers/invite",
10
+ "/api/peers/pair",
11
+ "/api/peers/probe",
12
+ "/api/peers/discover",
13
+ "/api/peers/discovery-jobs",
14
+ "/api/peers/discovery-jobs/:id",
15
+ "/api/peers/discovery-jobs/:id/cancel",
16
+ "/api/peers/discovery-jobs/:id/log",
17
+ "/api/peers/identity/backup",
18
+ "/api/peers/identity/restore",
19
+ "/api/peers/invitations/:id",
20
+ "/api/peers/:id",
21
+ "/api/peers/:id/repin",
22
+ "/api/peers/:id/health",
23
+ "/api/peers/:id/proxy",
24
+ "/api/peers/:id/events",
25
+ "/api/peers/global-sessions",
26
+ "/api/users",
27
+ "/api/users/:id",
28
+ "/api/users/:id/password",
29
+ "/api/users/:id/sessions",
30
+ "/api/users/:id/sessions/:sessionId",
31
+ "/api/users/:id/telegram",
32
+ "/api/users/:id/telegram/:identityId",
33
+ "/api/users/:id/discord",
34
+ "/api/users/:id/discord/:identityId",
35
+ "/api/users/:id/slack",
36
+ "/api/users/:id/slack/:identityId",
37
+ "/api/groups",
38
+ "/api/groups/:id",
39
+ "/api/telegram-chats",
40
+ "/api/telegram-chats/:id",
41
+ "/api/discord-channels",
42
+ "/api/discord-channels/:id",
43
+ "/api/slack-channels",
44
+ "/api/slack-channels/:id",
45
+ "/api/audit",
46
+ ]);
47
+ const IMPLEMENTED_ROUTE_PATHS = new Set([
48
+ "/api/bootstrap",
49
+ "/api/health",
50
+ "/api/snapshot",
51
+ "/api/tasks",
52
+ "/api/progress",
53
+ "/api/metrics",
54
+ "/api/jobs",
55
+ "/api/jobs/:id/log",
56
+ "/api/jobs/:id/action",
57
+ "/api/active-sessions",
58
+ "/api/version",
59
+ "/api/update",
60
+ "/api/agent-updates",
61
+ "/api/agent-update",
62
+ "/api/agent-update/:id/log",
63
+ "/api/agent-update/:id/input",
64
+ "/api/agent-update/:id/cancel",
65
+ "/api/adapters/health",
66
+ "/api/adapters/conformance",
67
+ "/api/locks",
68
+ "/api/auth/status",
69
+ "/api/auth/login",
70
+ "/api/auth/logout",
71
+ "/api/control-options",
72
+ "/api/sessions",
73
+ "/api/sessions/new",
74
+ "/api/sessions/switch",
75
+ "/api/sessions/attach",
76
+ "/api/sessions/detail",
77
+ "/api/agent",
78
+ "/api/models",
79
+ "/api/session/model",
80
+ "/api/session/reasoning",
81
+ "/api/session/fast",
82
+ "/api/session/launch",
83
+ "/api/prompt",
84
+ "/api/prompt/upload",
85
+ "/api/abort",
86
+ "/api/stop",
87
+ "/api/handback",
88
+ "/api/retry",
89
+ "/api/sync",
90
+ "/api/queue",
91
+ "/api/chat/history",
92
+ "/api/chat/mirror",
93
+ "/api/activity",
94
+ "/api/artifacts",
95
+ "/api/artifacts/bulk",
96
+ "/api/artifacts/zip",
97
+ "/api/artifacts/file",
98
+ "/api/artifacts/preview",
99
+ "/api/logs",
100
+ "/api/logs/clear",
101
+ "/api/diagnostics",
102
+ "/api/diagnostics/bundle",
103
+ "/api/runtime/restart",
104
+ ]);
105
+ export function peerProxyCoverage() {
106
+ const implemented = [];
107
+ const localOnly = [];
108
+ const missing = [];
109
+ for (const route of WEB_API_ROUTE_DEFINITIONS) {
110
+ for (const method of route.methods) {
111
+ const key = { method, path: route.path };
112
+ if (IMPLEMENTED_ROUTE_PATHS.has(route.path)) {
113
+ implemented.push(key);
114
+ }
115
+ else if (LOCAL_ONLY_ROUTE_PATHS.has(route.path)) {
116
+ localOnly.push(key);
117
+ }
118
+ else {
119
+ missing.push(key);
120
+ }
121
+ }
122
+ }
123
+ return { implemented, localOnly, missing };
124
+ }
125
+ export function isPeerProxyLocalOnlyPath(path) {
126
+ return LOCAL_ONLY_ROUTE_PATHS.has(path);
127
+ }
@@ -1,7 +1,8 @@
1
1
  import { monitorEventLoopDelay } from "node:perf_hooks";
2
- import { getDiscordRateLimitMetrics } from "./discord-rate-limit.js";
3
- import { getSlackRateLimitMetrics } from "./slack-rate-limit.js";
4
- import { getTelegramRateLimitMetrics } from "./telegram-rate-limit.js";
2
+ import { getDiscordRateLimitMetrics } from "../channels/discord/discord-rate-limit.js";
3
+ import { getSlackRateLimitMetrics } from "../channels/slack/slack-rate-limit.js";
4
+ import { getTelegramRateLimitMetrics } from "../channels/telegram/telegram-rate-limit.js";
5
+ import { getWebApiPerformanceMetrics } from "../web/web-performance.js";
5
6
  const startedAt = Date.now();
6
7
  const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
7
8
  eventLoopDelay.enable();
@@ -38,6 +39,7 @@ export function buildRuntimeMetrics(input) {
38
39
  discord: getDiscordRateLimitMetrics(),
39
40
  slack: getSlackRateLimitMetrics(),
40
41
  },
42
+ web: getWebApiPerformanceMetrics(),
41
43
  };
42
44
  }
43
45
  function processMetrics() {
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { collectRecentWorkspaceArtifacts, createArtifactZipBundle, getArtifactTurnReport, listRecentArtifactReports, persistWorkspaceArtifactReport, removeArtifactTurn, totalArtifactSize, } from "./artifacts.js";
3
+ import { collectRecentWorkspaceArtifacts, createArtifactZipBundle, getArtifactTurnReport, listRecentArtifactReports, persistWorkspaceArtifactReport, removeArtifactTurn, totalArtifactSize, } from "../artifacts/artifacts.js";
4
4
  const MAX_TEXT_PREVIEW_BYTES = 256 * 1024;
5
5
  export class RelayArtifactService {
6
6
  config;
@@ -0,0 +1,63 @@
1
+ import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "../agents/claude-code/claude-code-auth.js";
2
+ import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "../agents/codex/codex-auth.js";
3
+ import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "../agents/hermes/hermes-auth.js";
4
+ import { checkOpenClawAuthStatus } from "../agents/openclaw/openclaw-auth.js";
5
+ import { checkPiAuthStatus } from "../agents/pi/pi-auth.js";
6
+ export class RelayAuthService {
7
+ config;
8
+ constructor(config) {
9
+ this.config = config;
10
+ }
11
+ async check(info) {
12
+ if (info.agentId === "pi") {
13
+ return checkPiAuthStatus(info.model);
14
+ }
15
+ if (info.agentId === "hermes") {
16
+ return checkHermesAuthStatus({
17
+ baseUrl: this.config.hermesApiBaseUrl,
18
+ apiKey: this.config.hermesApiKey,
19
+ });
20
+ }
21
+ if (info.agentId === "openclaw") {
22
+ return checkOpenClawAuthStatus({
23
+ gatewayUrl: this.config.openClawGatewayUrl,
24
+ token: this.config.openClawGatewayToken,
25
+ password: this.config.openClawGatewayPassword,
26
+ });
27
+ }
28
+ if (info.agentId === "claude-code") {
29
+ return checkClaudeCodeAuthStatus(this.config.claudeCodeCliPath);
30
+ }
31
+ return checkAuthStatus(this.config.codexApiKey);
32
+ }
33
+ async startLogin(info) {
34
+ if (info.agentId === "hermes") {
35
+ return startHermesLogin(this.config.hermesCliPath);
36
+ }
37
+ if (info.agentId === "claude-code") {
38
+ return startClaudeCodeLogin(this.config.claudeCodeCliPath);
39
+ }
40
+ if (info.agentId === "codex") {
41
+ return startCodexLogin();
42
+ }
43
+ return {
44
+ success: false,
45
+ message: `${info.agentLabel} login is not managed by NordRelay. Run the agent login flow on the host.`,
46
+ };
47
+ }
48
+ async startLogout(info) {
49
+ if (info.agentId === "hermes") {
50
+ return startHermesLogout(this.config.hermesCliPath);
51
+ }
52
+ if (info.agentId === "claude-code") {
53
+ return startClaudeCodeLogout(this.config.claudeCodeCliPath);
54
+ }
55
+ if (info.agentId === "codex") {
56
+ return startCodexLogout();
57
+ }
58
+ return {
59
+ success: false,
60
+ message: `${info.agentLabel} logout is not managed by NordRelay. Run the agent logout flow on the host.`,
61
+ };
62
+ }
63
+ }
@@ -0,0 +1,139 @@
1
+ import { enabledAgents } from "../agents/shared/agent-factory.js";
2
+ import { listAgentAdapterDescriptors } from "../agents/shared/agent-adapter.js";
3
+ import { friendlyErrorText } from "../core/error-messages.js";
4
+ import { getAgentDiagnostics } from "../agents/shared/agent-activity.js";
5
+ import { getConnectorHealth, getVersionChecks, readConnectorState } from "../support/operations.js";
6
+ import { collectSlackDiagnostics } from "../channels/slack/slack-diagnostics.js";
7
+ import { getSlackRateLimitMetrics } from "../channels/slack/slack-rate-limit.js";
8
+ import { cliHealthForAgent, versionCheckForAgent } from "./relay-runtime-helpers.js";
9
+ export class RelayDashboardService {
10
+ options;
11
+ keys = ["version", "adapterHealth", "diagnostics"];
12
+ warmTimer;
13
+ constructor(options) {
14
+ this.options = options;
15
+ options.cache.register("version", () => this.produceVersion());
16
+ options.cache.register("adapterHealth", () => this.produceAdapterHealth());
17
+ options.cache.register("diagnostics", () => this.produceDiagnostics());
18
+ }
19
+ startBackgroundRefresh() {
20
+ this.options.cache.warm(this.keys);
21
+ const ttlMs = this.options.config.dashboardCacheTtlMs;
22
+ if (ttlMs <= 0 || this.warmTimer) {
23
+ return;
24
+ }
25
+ const intervalMs = Math.max(5_000, ttlMs);
26
+ this.warmTimer = setInterval(() => this.options.cache.warm(this.keys), intervalMs);
27
+ this.warmTimer.unref?.();
28
+ }
29
+ stopBackgroundRefresh() {
30
+ if (!this.warmTimer) {
31
+ return;
32
+ }
33
+ clearInterval(this.warmTimer);
34
+ this.warmTimer = undefined;
35
+ }
36
+ async version() {
37
+ return this.cached("version");
38
+ }
39
+ async diagnostics() {
40
+ return this.cached("diagnostics");
41
+ }
42
+ async adapterHealth() {
43
+ return this.cached("adapterHealth");
44
+ }
45
+ invalidate(key) {
46
+ this.options.cache.invalidate(key);
47
+ if (key) {
48
+ this.options.cache.warm([key]);
49
+ return;
50
+ }
51
+ this.options.cache.warm(this.keys);
52
+ }
53
+ async cached(key) {
54
+ return (await this.options.cache.get(key, this.options.config.dashboardCacheTtlMs)).value;
55
+ }
56
+ async produceVersion() {
57
+ const cliOptions = this.options.cliPathOptions();
58
+ const [health, state, versionChecks] = await Promise.all([
59
+ getConnectorHealth(cliOptions),
60
+ readConnectorState(),
61
+ getVersionChecks(cliOptions),
62
+ ]);
63
+ return {
64
+ health,
65
+ state,
66
+ versionChecks,
67
+ };
68
+ }
69
+ async produceDiagnostics() {
70
+ const cliOptions = this.options.cliPathOptions();
71
+ const [health, versionChecks, snapshot, session] = await Promise.all([
72
+ getConnectorHealth(cliOptions),
73
+ getVersionChecks(cliOptions),
74
+ this.options.snapshot(),
75
+ this.options.getSession(),
76
+ ]);
77
+ return {
78
+ health,
79
+ versionChecks,
80
+ snapshot,
81
+ runtime: {
82
+ stateBackend: this.options.config.stateBackend,
83
+ sourceWorkspace: this.options.config.workspace,
84
+ queuePaused: this.options.queuePaused(),
85
+ externalMirror: this.options.externalMirror(),
86
+ agentDiagnostics: getAgentDiagnostics(session, this.options.config),
87
+ slackDiagnostics: await collectSlackDiagnostics({
88
+ config: this.options.config,
89
+ timeoutMs: 2_500,
90
+ rateLimit: getSlackRateLimitMetrics(),
91
+ }),
92
+ },
93
+ };
94
+ }
95
+ async produceAdapterHealth() {
96
+ const cliOptions = this.options.cliPathOptions();
97
+ const [health, versions] = await Promise.all([
98
+ getConnectorHealth(cliOptions),
99
+ getVersionChecks(cliOptions),
100
+ ]);
101
+ return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
102
+ const enabled = enabledAgents(this.options.config).includes(descriptor.id);
103
+ const auth = descriptor.capabilities.auth && enabled
104
+ ? await this.options.authStatus(descriptor.id).catch((error) => ({
105
+ agentId: descriptor.id,
106
+ agentLabel: descriptor.label,
107
+ supported: descriptor.capabilities.auth,
108
+ authenticated: false,
109
+ detail: friendlyErrorText(error),
110
+ loginSupported: descriptor.capabilities.login,
111
+ logoutSupported: descriptor.capabilities.logout,
112
+ }))
113
+ : null;
114
+ const cli = cliHealthForAgent(descriptor.id, health);
115
+ const version = versionCheckForAgent(descriptor.id, versions);
116
+ return {
117
+ id: descriptor.id,
118
+ label: descriptor.label,
119
+ enabled,
120
+ status: descriptor.status === "available" ? (enabled ? "enabled" : "disabled") : "planned",
121
+ auth: {
122
+ supported: descriptor.capabilities.auth,
123
+ authenticated: auth ? auth.authenticated : null,
124
+ method: auth?.method,
125
+ detail: auth?.detail,
126
+ },
127
+ cli,
128
+ version: {
129
+ installed: version.installedLabel,
130
+ latest: version.latestVersion,
131
+ status: version.status,
132
+ detail: version.detail,
133
+ },
134
+ capabilities: descriptor.capabilities,
135
+ notes: descriptor.notes,
136
+ };
137
+ }));
138
+ }
139
+ }
@@ -1,7 +1,8 @@
1
- import {} from "./agent.js";
2
- import { getExternalSnapshotForSession } from "./agent-activity.js";
3
- import { friendlyErrorText } from "./error-messages.js";
4
- import {} from "./web-state.js";
1
+ import {} from "../agents/shared/agent.js";
2
+ import { getExternalSnapshotForSession } from "../agents/shared/agent-activity.js";
3
+ import { renderExternalMirrorEvent, renderExternalMirrorStatus, trimLine, } from "../channels/shared/bot-rendering.js";
4
+ import { friendlyErrorText } from "../core/error-messages.js";
5
+ import {} from "../web/web-state.js";
5
6
  const CLI_ACTIVITY_ACTOR = {
6
7
  channel: "cli",
7
8
  label: "CLI",
@@ -16,6 +17,9 @@ export class RelayExternalActivityMonitor {
16
17
  snapshot() {
17
18
  return this.mirror ? { ...this.mirror } : null;
18
19
  }
20
+ reset() {
21
+ this.mirror = null;
22
+ }
19
23
  task() {
20
24
  if (!this.mirror) {
21
25
  return null;
@@ -81,7 +85,7 @@ export class RelayExternalActivityMonitor {
81
85
  startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
82
86
  };
83
87
  if (snapshot.activity.active) {
84
- this.startExternalTurn(snapshot, info);
88
+ await this.startExternalTurn(snapshot, info);
85
89
  }
86
90
  return;
87
91
  }
@@ -91,36 +95,51 @@ export class RelayExternalActivityMonitor {
91
95
  mirror.turnId = snapshot.activity.turnId;
92
96
  mirror.startedAt = snapshot.activity.startedAt?.toISOString() ?? null;
93
97
  mirror.latestAgentLine = undefined;
94
- this.startExternalTurn(snapshot, info);
98
+ mirror.latestStatusAt = undefined;
99
+ mirror.latestMirroredEventLine = undefined;
100
+ await this.startExternalTurn(snapshot, info);
101
+ }
102
+ const mirrorMode = this.options.mirrorMode();
103
+ const newEvents = snapshot.events.filter((event) => event.lineNumber > mirror.lastLine);
104
+ this.broadcastExternalEvents(snapshot, newEvents, info, mirrorMode === "full");
105
+ if (mirrorMode === "full") {
106
+ await this.appendExternalEventMessages(snapshot, newEvents, mirror);
95
107
  }
96
- this.broadcastExternalEvents(snapshot, snapshot.events.filter((event) => event.lineNumber > mirror.lastLine), info);
97
108
  mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
98
109
  mirror.latestStatus = externalStatusLine(snapshot, this.options.queueLength());
99
- this.options.broadcastStatus(mirror.latestStatus, "info");
110
+ if (mirrorMode === "status" || mirrorMode === "full") {
111
+ await this.updateExternalStatusMessage(snapshot, mirror);
112
+ }
113
+ if (mirrorMode !== "off") {
114
+ this.options.broadcastStatus(mirror.latestStatus, "info");
115
+ }
100
116
  return;
101
117
  }
102
118
  const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
103
119
  if (terminalEvent && terminalEvent.lineNumber > mirror.lastLine) {
120
+ const mirrorMode = this.options.mirrorMode();
104
121
  const finalAgent = snapshot.events.filter((event) => event.kind === "agent" && event.text).at(-1);
105
122
  const finalText = finalAgent?.text ?? snapshot.latestAgentMessage;
106
123
  const finalLine = finalAgent?.lineNumber ?? snapshot.lineCount;
107
- if (finalText && finalLine !== mirror.latestAgentLine) {
108
- this.options.chatStore.append({
124
+ if ((mirrorMode === "final" || mirrorMode === "full") && finalText && finalLine !== mirror.latestAgentLine) {
125
+ this.options.chatStore.appendWithResult({
109
126
  threadId: snapshot.threadId,
110
127
  role: "agent",
111
128
  text: finalText,
112
129
  source: "cli",
113
130
  turnId: terminalEvent.turnId ?? undefined,
131
+ key: externalMessageKey("final", snapshot, terminalEvent.lineNumber),
114
132
  });
115
- this.options.broadcast({ type: "text_delta", id: terminalEvent.turnId ?? "cli", delta: finalText });
116
133
  mirror.latestAgentLine = finalLine;
117
134
  }
118
135
  const externalStartedAt = mirror.startedAt ? new Date(mirror.startedAt) : snapshot.activity.startedAt;
119
- this.options.broadcast({
120
- type: "turn_complete",
121
- id: terminalEvent.turnId ?? "cli",
122
- at: terminalEvent.timestamp?.toISOString() ?? new Date().toISOString(),
123
- });
136
+ if (mirrorMode !== "off") {
137
+ this.options.broadcast({
138
+ type: "turn_complete",
139
+ id: terminalEvent.turnId ?? "cli",
140
+ at: terminalEvent.timestamp?.toISOString() ?? new Date().toISOString(),
141
+ });
142
+ }
124
143
  this.options.appendActivity({
125
144
  source: "cli",
126
145
  status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
@@ -137,29 +156,35 @@ export class RelayExternalActivityMonitor {
137
156
  await this.options.persistWorkspaceArtifactsForTurn(info.workspace, terminalEvent.turnId, externalStartedAt);
138
157
  }
139
158
  mirror.latestStatus = `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`;
140
- this.options.broadcastStatus(`${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`, terminalEvent.status === "failed" ? "error" : terminalEvent.status === "aborted" ? "warn" : "info");
141
- this.options.broadcast({ type: "chat_history", messages: await this.options.chatHistory() });
159
+ if (mirrorMode === "status" || mirrorMode === "full") {
160
+ await this.updateExternalStatusMessage(snapshot, mirror, mirror.latestStatus);
161
+ }
162
+ if (mirrorMode !== "off") {
163
+ this.options.broadcastStatus(`${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`, terminalEvent.status === "failed" ? "error" : terminalEvent.status === "aborted" ? "warn" : "info");
164
+ await this.broadcastChatHistory();
165
+ }
142
166
  await this.options.drainQueue();
143
167
  }
144
168
  mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
145
169
  }
146
- startExternalTurn(snapshot, info) {
170
+ async startExternalTurn(snapshot, info) {
147
171
  const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
148
- this.options.chatStore.append({
149
- threadId: snapshot.threadId,
150
- role: "user",
151
- text: prompt,
152
- source: "cli",
153
- turnId: snapshot.activity.turnId ?? undefined,
154
- timestamp: snapshot.activity.startedAt?.toISOString(),
155
- });
156
- this.options.broadcast({
157
- type: "turn_start",
158
- id: snapshot.activity.turnId ?? "cli",
159
- prompt,
160
- at: snapshot.activity.startedAt?.toISOString() ?? new Date().toISOString(),
161
- source: "cli",
162
- });
172
+ const mode = this.options.mirrorMode();
173
+ if (mode === "final" || mode === "full") {
174
+ this.options.chatStore.appendWithResult({
175
+ threadId: snapshot.threadId,
176
+ role: "system",
177
+ text: `Working on ${trimLine(prompt, 500)}`,
178
+ source: "cli",
179
+ turnId: snapshot.activity.turnId ?? undefined,
180
+ timestamp: snapshot.activity.startedAt?.toISOString(),
181
+ key: externalMessageKey("working", snapshot),
182
+ });
183
+ await this.broadcastChatHistory();
184
+ }
185
+ if ((mode === "status" || mode === "full") && this.mirror) {
186
+ await this.updateExternalStatusMessage(snapshot, this.mirror);
187
+ }
163
188
  this.options.appendActivity({
164
189
  source: "cli",
165
190
  status: "running",
@@ -172,15 +197,17 @@ export class RelayExternalActivityMonitor {
172
197
  detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
173
198
  });
174
199
  }
175
- broadcastExternalEvents(snapshot, events, info) {
200
+ broadcastExternalEvents(snapshot, events, info, broadcastTools) {
176
201
  for (const event of events) {
177
202
  if (event.kind === "tool" && event.status === "started") {
178
- this.options.broadcast({
179
- type: "tool_start",
180
- id: snapshot.activity.turnId ?? "cli",
181
- toolCallId: `cli-${event.lineNumber}`,
182
- toolName: event.toolName ?? "tool",
183
- });
203
+ if (broadcastTools) {
204
+ this.options.broadcast({
205
+ type: "tool_start",
206
+ id: snapshot.activity.turnId ?? "cli",
207
+ toolCallId: `cli-${event.lineNumber}`,
208
+ toolName: event.toolName ?? "tool",
209
+ });
210
+ }
184
211
  this.options.appendActivity({
185
212
  source: "cli",
186
213
  status: "running",
@@ -193,12 +220,14 @@ export class RelayExternalActivityMonitor {
193
220
  });
194
221
  }
195
222
  if (event.kind === "tool" && event.status === "finished") {
196
- this.options.broadcast({
197
- type: "tool_end",
198
- id: snapshot.activity.turnId ?? "cli",
199
- toolCallId: `cli-${event.lineNumber}`,
200
- isError: false,
201
- });
223
+ if (broadcastTools) {
224
+ this.options.broadcast({
225
+ type: "tool_end",
226
+ id: snapshot.activity.turnId ?? "cli",
227
+ toolCallId: `cli-${event.lineNumber}`,
228
+ isError: false,
229
+ });
230
+ }
202
231
  this.options.appendActivity({
203
232
  source: "cli",
204
233
  status: "completed",
@@ -211,12 +240,14 @@ export class RelayExternalActivityMonitor {
211
240
  });
212
241
  }
213
242
  if (event.kind === "tool" && event.status === "failed") {
214
- this.options.broadcast({
215
- type: "tool_end",
216
- id: snapshot.activity.turnId ?? "cli",
217
- toolCallId: `cli-${event.lineNumber}`,
218
- isError: true,
219
- });
243
+ if (broadcastTools) {
244
+ this.options.broadcast({
245
+ type: "tool_end",
246
+ id: snapshot.activity.turnId ?? "cli",
247
+ toolCallId: `cli-${event.lineNumber}`,
248
+ isError: true,
249
+ });
250
+ }
220
251
  this.options.appendActivity({
221
252
  source: "cli",
222
253
  status: "failed",
@@ -230,6 +261,62 @@ export class RelayExternalActivityMonitor {
230
261
  }
231
262
  }
232
263
  }
264
+ async appendExternalEventMessages(snapshot, events, mirror) {
265
+ let changed = false;
266
+ for (const event of events) {
267
+ if (event.lineNumber <= (mirror.latestMirroredEventLine ?? mirror.lastLine)) {
268
+ continue;
269
+ }
270
+ const rendered = renderExternalMirrorEvent(event);
271
+ if (!rendered) {
272
+ continue;
273
+ }
274
+ const stored = this.options.chatStore.appendWithResult({
275
+ threadId: snapshot.threadId,
276
+ role: event.kind === "tool" ? "tool" : "system",
277
+ text: rendered.plain,
278
+ source: "cli",
279
+ turnId: event.turnId ?? snapshot.activity.turnId ?? undefined,
280
+ timestamp: event.timestamp?.toISOString(),
281
+ key: externalMessageKey("event", snapshot, event.lineNumber),
282
+ });
283
+ changed = changed || stored.inserted;
284
+ mirror.latestMirroredEventLine = event.lineNumber;
285
+ }
286
+ if (changed) {
287
+ await this.broadcastChatHistory();
288
+ }
289
+ }
290
+ async updateExternalStatusMessage(snapshot, mirror, text) {
291
+ const now = Date.now();
292
+ const minInterval = this.options.mirrorMinUpdateMs();
293
+ if (!text && mirror.latestStatusAt && now - mirror.latestStatusAt < minInterval) {
294
+ return;
295
+ }
296
+ this.options.chatStore.upsertByKey({
297
+ threadId: snapshot.threadId,
298
+ role: "system",
299
+ text: text ?? renderExternalMirrorStatus(snapshot, this.options.queueLength()).plain,
300
+ source: "cli",
301
+ turnId: snapshot.activity.turnId ?? undefined,
302
+ key: externalMessageKey("status", snapshot),
303
+ });
304
+ mirror.latestStatusAt = now;
305
+ await this.broadcastChatHistory();
306
+ }
307
+ async broadcastChatHistory() {
308
+ this.options.broadcast({ type: "chat_history", messages: await this.options.chatHistory() });
309
+ }
310
+ }
311
+ function externalMessageKey(kind, snapshot, lineNumber) {
312
+ return [
313
+ "external",
314
+ kind,
315
+ snapshot.agentId,
316
+ snapshot.threadId,
317
+ snapshot.activity.turnId ?? "turn",
318
+ lineNumber ?? "",
319
+ ].join(":");
233
320
  }
234
321
  function externalStatusLine(snapshot, queueLength) {
235
322
  const elapsed = snapshot.activity.startedAt