@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,623 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { ensureOutDir } from "../artifacts/artifacts.js";
3
+ import { buildFileInstructions, outboxPath, stageFile, } from "../artifacts/attachments.js";
4
+ import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, isAgentId, } from "../agents/shared/agent.js";
5
+ import { getExternalSnapshotForSession } from "../agents/shared/agent-activity.js";
6
+ import { listAgentAdapterDescriptors } from "../agents/shared/agent-adapter.js";
7
+ import { AgentUpdateManager } from "../agents/shared/agent-updates.js";
8
+ import { createAgentSessionService, enabledAgents } from "../agents/shared/agent-factory.js";
9
+ import { AuditLogStore } from "../access/audit-log.js";
10
+ import { BotPreferencesStore } from "../state/bot-preferences.js";
11
+ import { ChannelCommandService } from "../channels/shared/channel-command-service.js";
12
+ import { ChannelTurnService } from "../channels/shared/channel-turn-service.js";
13
+ import { activeSessionSourceForContextKey, ChannelMirrorRegistry } from "../channels/shared/channel-mirror-registry.js";
14
+ import { listThreads as listCodexThreads } from "../agents/codex/codex-state.js";
15
+ import { friendlyErrorText } from "../core/error-messages.js";
16
+ import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "../support/operations.js";
17
+ import { PromptStore, toPromptEnvelope } from "../state/prompt-store.js";
18
+ import { UnifiedJobStore } from "../state/job-store.js";
19
+ import { buildRuntimeMetrics } from "./metrics.js";
20
+ import { RelayArtifactService } from "./relay-artifact-service.js";
21
+ import { RelayAuthService } from "./relay-auth-service.js";
22
+ import { RelayExternalActivityMonitor } from "./relay-external-activity-monitor.js";
23
+ import { RelayQueueService } from "./relay-queue-service.js";
24
+ import { RuntimeSnapshotCache } from "./runtime-cache.js";
25
+ import { activeSessionPriority, activityToUnifiedJob, agentUpdateStatusToUnified, dedupeJobs, hostLoginCommand, hostLogoutCommand, isPromptTerminalActivity, normalizeMimeType, promptActivityToUnifiedJob, shouldRefreshActiveSessions, taskToUnifiedJob, uploadFileDtos, } from "./relay-runtime-helpers.js";
26
+ import { RelayDashboardService } from "./relay-dashboard-service.js";
27
+ import { capabilitiesOf } from "../channels/shared/bot-rendering.js";
28
+ import { renderSessionInfoPlain, renderSessionUsageRows } from "../channels/shared/session-format.js";
29
+ import { SessionLockStore } from "../access/session-locks.js";
30
+ import { SessionRegistry } from "../state/session-registry.js";
31
+ import { createSupportBundle } from "../support/support-bundle.js";
32
+ import { transcribeAudio } from "../artifacts/voice.js";
33
+ import { WebActivityStore, WebChatStore, } from "../web/web-state.js";
34
+ import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "../core/workspace-policy.js";
35
+ export const WEB_CONTEXT_KEY = "web:dashboard";
36
+ const ACTIVE_CODEX_DISCOVERY_LIMIT = 200;
37
+ const ACTIVE_ACTIVITY_TTL_MS = 6 * 60 * 60 * 1000;
38
+ const MAX_WEB_SESSION_PAGE_SIZE = 50;
39
+ const MAX_CHAT_HISTORY = 250;
40
+ export function relayRuntimeLocks(runtime) {
41
+ return runtime.lockStore.list();
42
+ }
43
+ export function relayRuntimeLockWebSession(runtime, ownerName = "Web dashboard", actor) {
44
+ const label = ownerName || actor?.label || "Web dashboard";
45
+ const lock = runtime.lockStore.set(runtime.contextKey, {
46
+ userId: actor?.id ?? "web",
47
+ label,
48
+ channel: "web",
49
+ }, runtime.config.sessionLockTtlMs);
50
+ runtime.appendActivity({
51
+ source: "web",
52
+ status: "info",
53
+ type: "lock_created",
54
+ threadId: null,
55
+ workspace: runtime.config.workspace,
56
+ actor,
57
+ detail: `locked by ${label}`,
58
+ });
59
+ runtime.appendAudit({
60
+ action: "lock_updated",
61
+ status: "ok",
62
+ contextKey: runtime.contextKey,
63
+ actor,
64
+ description: "lock",
65
+ detail: `locked by ${label}`,
66
+ });
67
+ return lock;
68
+ }
69
+ export function relayRuntimeUnlockWebSession(runtime, actor) {
70
+ const removed = runtime.lockStore.clear(runtime.contextKey);
71
+ runtime.appendActivity({
72
+ source: "web",
73
+ status: "info",
74
+ type: "lock_removed",
75
+ threadId: null,
76
+ workspace: runtime.config.workspace,
77
+ actor,
78
+ detail: removed ? "unlocked" : "no lock",
79
+ });
80
+ runtime.appendAudit({
81
+ action: "lock_updated",
82
+ status: "ok",
83
+ contextKey: runtime.contextKey,
84
+ actor,
85
+ description: "unlock",
86
+ detail: removed ? "unlocked" : "no lock",
87
+ });
88
+ return { removed, locks: runtime.locks() };
89
+ }
90
+ export async function relayRuntimeControlOptions(runtime, agentId) {
91
+ const { session, dispose } = await runtime.getControlSession(agentId);
92
+ try {
93
+ const info = runtime.publicInfo(session);
94
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
95
+ if (capabilities.modelSelection) {
96
+ await session.refreshModels().catch((error) => {
97
+ console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
98
+ });
99
+ }
100
+ return {
101
+ models: capabilities.modelSelection ? session.listModels() : [],
102
+ reasoningLabel: agentReasoningLabel(info.agentId),
103
+ reasoningOptions: agentReasoningOptions(info.agentId),
104
+ launchProfiles: capabilities.launchProfiles ? session.listLaunchProfiles() : [],
105
+ workspaces: filterAllowedWorkspaces(session.listWorkspaces(), runtime.config),
106
+ capabilities,
107
+ };
108
+ }
109
+ finally {
110
+ if (dispose) {
111
+ session.dispose();
112
+ }
113
+ }
114
+ }
115
+ export async function relayRuntimeAuthStatus(runtime, agentId) {
116
+ const { session, dispose } = await runtime.getControlSession(agentId);
117
+ try {
118
+ const info = runtime.publicInfo(session);
119
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
120
+ if (!capabilities.auth) {
121
+ return {
122
+ agentId: info.agentId,
123
+ agentLabel: info.agentLabel,
124
+ supported: false,
125
+ authenticated: null,
126
+ detail: `${info.agentLabel} authentication is managed outside NordRelay.`,
127
+ loginSupported: false,
128
+ logoutSupported: false,
129
+ hostLoginCommand: hostLoginCommand(info, runtime.config),
130
+ hostLogoutCommand: hostLogoutCommand(info, runtime.config),
131
+ };
132
+ }
133
+ const status = await runtime.authService.check(info);
134
+ return {
135
+ agentId: info.agentId,
136
+ agentLabel: info.agentLabel,
137
+ supported: true,
138
+ authenticated: status.authenticated,
139
+ method: status.method,
140
+ detail: status.detail,
141
+ loginSupported: capabilities.login,
142
+ logoutSupported: capabilities.logout,
143
+ hostLoginCommand: hostLoginCommand(info, runtime.config),
144
+ hostLogoutCommand: hostLogoutCommand(info, runtime.config),
145
+ };
146
+ }
147
+ finally {
148
+ if (dispose) {
149
+ session.dispose();
150
+ }
151
+ }
152
+ }
153
+ export async function relayRuntimeLogin(runtime, agentId, actor) {
154
+ const { session, dispose } = await runtime.getControlSession(agentId);
155
+ try {
156
+ const info = runtime.publicInfo(session);
157
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
158
+ if (!capabilities.login) {
159
+ return {
160
+ ...(await runtime.authStatus(info.agentId)),
161
+ result: {
162
+ success: false,
163
+ message: `${info.agentLabel} login is not managed by NordRelay. Run ${hostLoginCommand(info, runtime.config)} on the host.`,
164
+ },
165
+ };
166
+ }
167
+ if (!runtime.config.enableTelegramLogin) {
168
+ return {
169
+ ...(await runtime.authStatus(info.agentId)),
170
+ result: {
171
+ success: false,
172
+ message: `Remote login is disabled. Run ${hostLoginCommand(info, runtime.config)} on the host.`,
173
+ },
174
+ };
175
+ }
176
+ const result = await runtime.authService.startLogin(info);
177
+ runtime.appendActivity({
178
+ source: "web",
179
+ status: result.success ? "info" : "failed",
180
+ type: result.success ? "login_started" : "login_failed",
181
+ threadId: info.threadId,
182
+ workspace: info.workspace,
183
+ agentId: info.agentId,
184
+ actor,
185
+ detail: result.message,
186
+ });
187
+ runtime.appendAudit({
188
+ action: "command",
189
+ status: result.success ? "ok" : "failed",
190
+ contextKey: runtime.contextKey,
191
+ agentId: info.agentId,
192
+ threadId: info.threadId,
193
+ workspace: info.workspace,
194
+ actor,
195
+ description: "login",
196
+ detail: result.message,
197
+ });
198
+ return { ...(await runtime.authStatus(info.agentId)), result };
199
+ }
200
+ finally {
201
+ if (dispose) {
202
+ session.dispose();
203
+ }
204
+ }
205
+ }
206
+ export async function relayRuntimeLogout(runtime, agentId, actor) {
207
+ const { session, dispose } = await runtime.getControlSession(agentId);
208
+ try {
209
+ const info = runtime.publicInfo(session);
210
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
211
+ if (!capabilities.logout) {
212
+ return {
213
+ ...(await runtime.authStatus(info.agentId)),
214
+ result: {
215
+ success: false,
216
+ message: `${info.agentLabel} logout is not managed by NordRelay. Run ${hostLogoutCommand(info, runtime.config)} on the host.`,
217
+ },
218
+ };
219
+ }
220
+ if (!runtime.config.enableTelegramLogin) {
221
+ return {
222
+ ...(await runtime.authStatus(info.agentId)),
223
+ result: {
224
+ success: false,
225
+ message: `Remote auth management is disabled. Run ${hostLogoutCommand(info, runtime.config)} on the host.`,
226
+ },
227
+ };
228
+ }
229
+ const current = await runtime.authService.check(info);
230
+ if (current.method === "api-key") {
231
+ return {
232
+ ...(await runtime.authStatus(info.agentId)),
233
+ result: {
234
+ success: false,
235
+ message: "Cannot logout while API-key authentication is configured. Remove the API key from .env to use CLI auth.",
236
+ },
237
+ };
238
+ }
239
+ const result = await runtime.authService.startLogout(info);
240
+ runtime.appendActivity({
241
+ source: "web",
242
+ status: result.success ? "info" : "failed",
243
+ type: result.success ? "logout_completed" : "logout_failed",
244
+ threadId: info.threadId,
245
+ workspace: info.workspace,
246
+ agentId: info.agentId,
247
+ actor,
248
+ detail: result.message,
249
+ });
250
+ runtime.appendAudit({
251
+ action: "command",
252
+ status: result.success ? "ok" : "failed",
253
+ contextKey: runtime.contextKey,
254
+ agentId: info.agentId,
255
+ threadId: info.threadId,
256
+ workspace: info.workspace,
257
+ actor,
258
+ description: "logout",
259
+ detail: result.message,
260
+ });
261
+ return { ...(await runtime.authStatus(info.agentId)), result };
262
+ }
263
+ finally {
264
+ if (dispose) {
265
+ session.dispose();
266
+ }
267
+ }
268
+ }
269
+ export async function relayRuntimeChatHistory(runtime, limit = 200) {
270
+ const session = await runtime.getSession(true);
271
+ return runtime.chatStore.list(runtime.publicInfo(session).threadId, limit);
272
+ }
273
+ export async function relayRuntimeWebMirrorPreference(runtime, argument = "", actor) {
274
+ const session = await runtime.getSession(true);
275
+ runtime.registry.updateMetadata(runtime.contextKey, session);
276
+ const info = runtime.publicInfo(session);
277
+ const response = new ChannelCommandService(runtime.config).renderMirrorPreference({
278
+ source: "web",
279
+ contextKey: runtime.contextKey,
280
+ argument,
281
+ preferencesStore: runtime.preferencesStore,
282
+ cliMirrorSupported: capabilitiesOf(info).cliMirror,
283
+ agentLabel: info.agentLabel,
284
+ });
285
+ const mode = runtime.preferencesStore.get(runtime.contextKey).mirrorMode ?? runtime.config.webMirrorMode;
286
+ const changed = argument.trim() && response.plain.startsWith("CLI mirroring:");
287
+ if (changed) {
288
+ runtime.appendActivity({
289
+ source: "web",
290
+ status: "info",
291
+ type: "mirror_mode_changed",
292
+ threadId: info.threadId,
293
+ workspace: info.workspace,
294
+ agentId: info.agentId,
295
+ actor,
296
+ detail: mode,
297
+ });
298
+ runtime.appendAudit({
299
+ action: "command",
300
+ status: "ok",
301
+ contextKey: runtime.contextKey,
302
+ agentId: info.agentId,
303
+ threadId: info.threadId,
304
+ workspace: info.workspace,
305
+ actor,
306
+ description: `mirror ${mode}`,
307
+ });
308
+ runtime.externalActivityMonitor.reset();
309
+ void runtime.externalActivityMonitor.monitorSafe();
310
+ }
311
+ return {
312
+ mode,
313
+ minInterval: runtime.config.webMirrorMinUpdateMs,
314
+ response,
315
+ };
316
+ }
317
+ export async function relayRuntimeSessionDetail(runtime, threadId) {
318
+ const session = await runtime.getSession(true);
319
+ const record = session.getSessionRecord(threadId);
320
+ const active = runtime.publicInfo(session);
321
+ return {
322
+ record,
323
+ active,
324
+ usageRows: active.threadId === threadId ? renderSessionUsageRows(active) : [],
325
+ messages: runtime.chatStore.list(threadId, 100),
326
+ activity: runtime.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
327
+ };
328
+ }
329
+ export async function relayRuntimeClearChatHistory(runtime, actor) {
330
+ const session = await runtime.getSession(true);
331
+ const info = runtime.publicInfo(session);
332
+ const removed = runtime.chatStore.clear(info.threadId);
333
+ const messages = await runtime.chatHistory();
334
+ runtime.broadcast({ type: "chat_history", messages });
335
+ runtime.appendActivity({
336
+ source: "web",
337
+ status: "info",
338
+ type: "chat_history_cleared",
339
+ threadId: info.threadId,
340
+ workspace: info.workspace,
341
+ agentId: info.agentId,
342
+ actor,
343
+ detail: `${removed} messages removed.`,
344
+ });
345
+ return { removed, messages };
346
+ }
347
+ export function relayRuntimeActivity(runtime, options = {}) {
348
+ const currentInfo = runtime.registry.get(runtime.contextKey)?.getInfo();
349
+ return runtime.activityStore.list(options).map((event) => runtime.enrichActivityEvent(event, currentInfo));
350
+ }
351
+ export async function relayRuntimeRetry(runtime, actor) {
352
+ const cached = runtime.queueService.getLastPrompt();
353
+ if (!cached) {
354
+ throw new Error("Nothing to retry. Send a message first.");
355
+ }
356
+ runtime.appendAudit({
357
+ action: "command",
358
+ status: "ok",
359
+ contextKey: runtime.contextKey,
360
+ actor,
361
+ description: "retry",
362
+ detail: cached.description,
363
+ });
364
+ return runtime.sendEnvelope({ ...cached, activityActor: cached.activityActor ?? actor }, actor);
365
+ }
366
+ export async function relayRuntimeSync(runtime, actor) {
367
+ const session = await runtime.getSession(true);
368
+ const info = runtime.publicInfo(session);
369
+ if (!(info.capabilities ?? CODEX_AGENT_CAPABILITIES).externalActivity) {
370
+ throw new Error(`${info.agentLabel} has no external state watcher to sync.`);
371
+ }
372
+ const result = session.syncFromAgentState({ reattach: true });
373
+ if (result.changed) {
374
+ runtime.updateSession(session);
375
+ }
376
+ runtime.appendActivity({
377
+ source: "web",
378
+ status: "info",
379
+ type: "session_sync",
380
+ threadId: result.info.threadId,
381
+ workspace: result.info.workspace,
382
+ agentId: result.info.agentId,
383
+ actor,
384
+ detail: result.changedFields.length > 0 ? result.changedFields.join(", ") : "already in sync",
385
+ });
386
+ runtime.appendAudit({
387
+ action: "command",
388
+ status: "ok",
389
+ contextKey: runtime.contextKey,
390
+ agentId: result.info.agentId,
391
+ threadId: result.info.threadId,
392
+ workspace: result.info.workspace,
393
+ actor,
394
+ description: "sync",
395
+ detail: result.changedFields.join(", ") || "none",
396
+ });
397
+ return result;
398
+ }
399
+ export async function relayRuntimeListSessions(runtime, limit = 80, query = "", agentId) {
400
+ const { session, dispose } = await runtime.getControlSession(agentId);
401
+ try {
402
+ return runtime.filteredSessions(session, query, Math.max(1, limit * 3)).slice(0, limit);
403
+ }
404
+ finally {
405
+ if (dispose) {
406
+ session.dispose();
407
+ }
408
+ }
409
+ }
410
+ export async function relayRuntimeListSessionsPage(runtime, page = 1, pageSize = MAX_WEB_SESSION_PAGE_SIZE, query = "", agentId) {
411
+ const { session, dispose } = await runtime.getControlSession(agentId);
412
+ try {
413
+ const effectivePage = Math.max(1, Math.floor(page));
414
+ const effectivePageSize = Math.min(MAX_WEB_SESSION_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
415
+ const offset = (effectivePage - 1) * effectivePageSize;
416
+ const requested = Math.min(5_000, Math.max(100, (offset + effectivePageSize + 1) * 3));
417
+ const records = runtime.filteredSessions(session, query, requested);
418
+ return {
419
+ sessions: records.slice(offset, offset + effectivePageSize),
420
+ pagination: {
421
+ page: effectivePage,
422
+ pageSize: effectivePageSize,
423
+ hasPrevious: effectivePage > 1,
424
+ hasNext: records.length > offset + effectivePageSize,
425
+ },
426
+ };
427
+ }
428
+ finally {
429
+ if (dispose) {
430
+ session.dispose();
431
+ }
432
+ }
433
+ }
434
+ export function relayRuntimeFilteredSessions(runtime, session, query, limit) {
435
+ const normalized = query.trim().toLowerCase();
436
+ return session.listAllSessions(limit)
437
+ .filter((record) => evaluateWorkspacePolicy(record.cwd, runtime.config).allowed)
438
+ .filter((record) => {
439
+ if (!normalized) {
440
+ return true;
441
+ }
442
+ return [
443
+ record.id,
444
+ record.title,
445
+ record.cwd,
446
+ record.model,
447
+ record.reasoningEffort,
448
+ record.firstUserMessage,
449
+ ].some((value) => value?.toLowerCase().includes(normalized));
450
+ });
451
+ }
452
+ export async function relayRuntimeListModels(runtime) {
453
+ const session = await runtime.getSession(true);
454
+ const info = runtime.publicInfo(session);
455
+ await session.refreshModels({ force: true }).catch((error) => {
456
+ console.warn(`Failed to refresh ${agentLabel(info.agentId)} models: ${error instanceof Error ? error.message : String(error)}`);
457
+ });
458
+ return session.listModels();
459
+ }
460
+ export async function relayRuntimeSetAgent(runtime, agentId, actor) {
461
+ if (!enabledAgents(runtime.config).includes(agentId)) {
462
+ throw new Error(`Agent is not enabled: ${agentId}`);
463
+ }
464
+ const session = await runtime.registry.switchAgent(runtime.contextKey, agentId);
465
+ runtime.updateSession(session);
466
+ const info = runtime.publicInfo(session);
467
+ runtime.appendActivity({
468
+ source: "web",
469
+ status: "info",
470
+ type: "agent_switch",
471
+ threadId: info.threadId,
472
+ workspace: info.workspace,
473
+ agentId: info.agentId,
474
+ actor,
475
+ detail: `Dashboard switched agent to ${info.agentLabel}.`,
476
+ });
477
+ return runtime.publicInfo(session);
478
+ }
479
+ export async function relayRuntimeNewSession(runtime, options = {}, actor) {
480
+ const session = options.agentId ? await runtime.registry.switchAgent(runtime.contextKey, options.agentId) : await runtime.getSession(true);
481
+ runtime.ensureIdle(session);
482
+ if (options.reasoningEffort) {
483
+ const reasoningOptions = agentReasoningOptions(session.getInfo().agentId);
484
+ if (!reasoningOptions.includes(options.reasoningEffort)) {
485
+ throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${options.reasoningEffort}`);
486
+ }
487
+ session.setReasoningEffort(options.reasoningEffort);
488
+ }
489
+ if (options.launchProfileId && (session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).launchProfiles) {
490
+ session.setLaunchProfile(options.launchProfileId);
491
+ }
492
+ if (typeof options.fastMode === "boolean" && (session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).fastMode) {
493
+ session.setFastMode(options.fastMode);
494
+ }
495
+ const info = await session.newThread(options.workspace, options.model);
496
+ runtime.updateSession(session);
497
+ runtime.appendActivity({
498
+ source: "web",
499
+ status: "info",
500
+ type: "session_new",
501
+ threadId: info.threadId,
502
+ workspace: info.workspace,
503
+ agentId: info.agentId,
504
+ actor,
505
+ detail: "New dashboard session created.",
506
+ });
507
+ return runtime.publicInfo(session);
508
+ }
509
+ export async function relayRuntimeSwitchSession(runtime, threadId, actor) {
510
+ const session = await runtime.getSession(true);
511
+ runtime.ensureIdle(session);
512
+ const info = await session.switchSession(threadId);
513
+ runtime.updateSession(session);
514
+ runtime.broadcast({ type: "chat_history", messages: await runtime.chatHistory() });
515
+ runtime.appendActivity({
516
+ source: "web",
517
+ status: "info",
518
+ type: "session_switch",
519
+ threadId: info.threadId,
520
+ workspace: info.workspace,
521
+ agentId: info.agentId,
522
+ actor,
523
+ detail: "Dashboard switched session.",
524
+ });
525
+ return runtime.publicInfo(session);
526
+ }
527
+ export async function relayRuntimeAttachSession(runtime, threadId, actor) {
528
+ return runtime.switchSession(threadId, actor);
529
+ }
530
+ export async function relayRuntimeSetModel(runtime, model, actor) {
531
+ const session = await runtime.getSession(true);
532
+ runtime.ensureIdle(session);
533
+ await session.setModelForCurrentSession(model);
534
+ runtime.updateSession(session);
535
+ const info = runtime.publicInfo(session);
536
+ runtime.appendActivity({ source: "web", status: "info", type: "model_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: model });
537
+ return info;
538
+ }
539
+ export async function relayRuntimeSetReasoningEffort(runtime, effort, actor) {
540
+ const session = await runtime.getSession(true);
541
+ runtime.ensureIdle(session);
542
+ const options = agentReasoningOptions(session.getInfo().agentId);
543
+ if (!options.includes(effort)) {
544
+ throw new Error(`Invalid ${agentReasoningLabel(session.getInfo().agentId)} value: ${effort}`);
545
+ }
546
+ await session.setReasoningEffortForCurrentSession(effort);
547
+ runtime.updateSession(session);
548
+ const info = runtime.publicInfo(session);
549
+ runtime.appendActivity({ source: "web", status: "info", type: "reasoning_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: effort });
550
+ return info;
551
+ }
552
+ export async function relayRuntimeSetFastMode(runtime, enabled, actor) {
553
+ const session = await runtime.getSession(true);
554
+ runtime.ensureIdle(session);
555
+ if (!(session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).fastMode) {
556
+ throw new Error(`Fast mode is not supported for ${agentLabel(session.getInfo().agentId)}.`);
557
+ }
558
+ session.setFastMode(enabled);
559
+ runtime.updateSession(session);
560
+ const info = runtime.publicInfo(session);
561
+ runtime.appendActivity({ source: "web", status: "info", type: "fast_mode_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: enabled ? "on" : "off" });
562
+ return info;
563
+ }
564
+ export async function relayRuntimeSetLaunchProfile(runtime, profileId, actor) {
565
+ const session = await runtime.getSession(true);
566
+ runtime.ensureIdle(session);
567
+ session.setLaunchProfile(profileId);
568
+ runtime.updateSession(session);
569
+ const info = runtime.publicInfo(session);
570
+ runtime.appendActivity({ source: "web", status: "info", type: "launch_profile_changed", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: info.launchProfileLabel ?? profileId });
571
+ return info;
572
+ }
573
+ export async function relayRuntimeHandback(runtime, actor) {
574
+ const session = await runtime.getSession(true);
575
+ runtime.ensureIdle(session);
576
+ const result = session.handback();
577
+ runtime.updateSession(session);
578
+ const info = runtime.publicInfo(session);
579
+ runtime.appendActivity({ source: "web", status: "info", type: "handback", threadId: result.threadId, workspace: result.workspace, agentId: info.agentId, actor, detail: result.command ?? "Thread handed back." });
580
+ return result;
581
+ }
582
+ export async function relayRuntimeAbort(runtime, actor) {
583
+ const session = await runtime.getSession(true);
584
+ const snapshot = getExternalSnapshotForSession(session, runtime.config, { maxEvents: 0 });
585
+ if (snapshot?.activity.active && !session.isProcessing()) {
586
+ runtime.broadcast({
587
+ type: "status",
588
+ level: "warn",
589
+ message: `Cannot abort the external ${snapshot.agentLabel} CLI task from NordRelay. Stop it in the terminal where it is running.`,
590
+ at: new Date().toISOString(),
591
+ });
592
+ const info = runtime.publicInfo(session);
593
+ runtime.appendActivity({ source: "web", status: "aborted", type: "prompt_abort_rejected", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: `External ${snapshot.agentLabel} CLI task is active.` });
594
+ return;
595
+ }
596
+ await session.abort();
597
+ const info = runtime.publicInfo(session);
598
+ runtime.appendActivity({ source: "web", status: "aborted", type: "prompt_aborted", threadId: info.threadId, workspace: info.workspace, agentId: info.agentId, actor, detail: "Current operation aborted." });
599
+ runtime.broadcast({ type: "status", level: "warn", message: "Current operation aborted.", at: new Date().toISOString() });
600
+ }
601
+ export async function relayRuntimeGetControlSession(runtime, agentId) {
602
+ const active = await runtime.getSession(true);
603
+ const activeInfo = runtime.publicInfo(active);
604
+ if (!agentId || agentId === activeInfo.agentId) {
605
+ return { session: active, dispose: false };
606
+ }
607
+ if (!enabledAgents(runtime.config).includes(agentId)) {
608
+ throw new Error(`Agent is not enabled: ${agentId}`);
609
+ }
610
+ const session = await createAgentSessionService(runtime.config, agentId, {
611
+ deferThreadStart: true,
612
+ workspace: activeInfo.workspace,
613
+ });
614
+ return { session, dispose: true };
615
+ }
616
+ export function relayRuntimeCliPathOptions(runtime) {
617
+ return {
618
+ piCliPath: runtime.config.piCliPath,
619
+ hermesCliPath: runtime.config.hermesCliPath,
620
+ openClawCliPath: runtime.config.openClawCliPath,
621
+ claudeCodeCliPath: runtime.config.claudeCodeCliPath,
622
+ };
623
+ }
@@ -0,0 +1 @@
1
+ export {};