@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
@@ -1,7 +1,7 @@
1
1
  import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, Client, } from "discord.js";
2
- import { DiscordChannelAdapter, } from "./channel-adapter.js";
2
+ import { DiscordChannelAdapter, } from "../shared/channel-adapter.js";
3
3
  import { discordRateLimiter } from "./discord-rate-limit.js";
4
- import { redactText } from "./redaction.js";
4
+ import { redactText } from "../../core/redaction.js";
5
5
  const DISCORD_MESSAGE_LIMIT = 2000;
6
6
  const DISCORD_SAFE_MESSAGE_LIMIT = 1900;
7
7
  export const DISCORD_ACTION_PREFIX = "nr:";
@@ -1,6 +1,6 @@
1
- import { permissionForCommand } from "./access-control.js";
2
- import { discordCommandCatalog } from "./channel-command-catalog.js";
3
- import { normalizeChannelCommandName, parseChannelCommand } from "./channel-runtime.js";
1
+ import { permissionForCommand } from "../../access/access-control.js";
2
+ import { discordCommandCatalog } from "../shared/channel-command-catalog.js";
3
+ import { normalizeChannelCommandName, parseChannelCommand } from "../shared/channel-runtime.js";
4
4
  export function parseDiscordMessageCommand(text) {
5
5
  return parseChannelCommand(text, { allowBotMention: false });
6
6
  }
@@ -1,10 +1,10 @@
1
1
  import { InlineKeyboard } from "grammy";
2
- import { CODEX_AGENT_CAPABILITIES, agentLabel } from "./agent.js";
3
- import { getAgentDiagnostics } from "./agent-activity.js";
4
- import { enabledAgents } from "./agent-factory.js";
5
- import { isTelegramImagePreview } from "./artifacts.js";
6
- import { friendlyErrorText } from "./error-messages.js";
7
- import { escapeHTML } from "./format.js";
2
+ import { CODEX_AGENT_CAPABILITIES, agentLabel } from "../../agents/shared/agent.js";
3
+ import { getAgentDiagnostics } from "../../agents/shared/agent-activity.js";
4
+ import { enabledAgents } from "../../agents/shared/agent-factory.js";
5
+ import { isTelegramImagePreview } from "../../artifacts/artifacts.js";
6
+ import { friendlyErrorText } from "../../core/error-messages.js";
7
+ import { escapeHTML } from "../../core/format.js";
8
8
  const TOOL_OUTPUT_PREVIEW_LIMIT = 500;
9
9
  const STREAMING_PREVIEW_LIMIT = 3800;
10
10
  export function renderVersionCheckPlain(check) {
@@ -1,7 +1,7 @@
1
- import { formatAgentFeatureSummaryHTML, formatAgentFeatureSummaryPlain } from "./agent-feature-matrix.js";
2
- import { totalArtifactSize } from "./artifacts.js";
3
- import { escapeHTML } from "./format.js";
4
- import { getAgentUpdateLogPath, getUpdateLogPath } from "./operations.js";
1
+ import { formatAgentFeatureSummaryHTML, formatAgentFeatureSummaryPlain } from "../../agents/shared/agent-feature-matrix.js";
2
+ import { totalArtifactSize } from "../../artifacts/artifacts.js";
3
+ import { escapeHTML } from "../../core/format.js";
4
+ import { getAgentUpdateLogPath, getUpdateLogPath } from "../../support/operations.js";
5
5
  import { formatFileSize } from "./session-format.js";
6
6
  export function renderChannelsAction(descriptors) {
7
7
  const plain = [
@@ -0,0 +1,69 @@
1
+ export function createChannelBusyStore(defaults = () => ({ processing: false, switching: false })) {
2
+ const states = new Map();
3
+ return {
4
+ get(contextKey) {
5
+ let state = states.get(contextKey);
6
+ if (!state) {
7
+ state = defaults();
8
+ states.set(contextKey, state);
9
+ }
10
+ return state;
11
+ },
12
+ peek(contextKey) {
13
+ return states.get(contextKey);
14
+ },
15
+ delete(contextKey) {
16
+ states.delete(contextKey);
17
+ },
18
+ };
19
+ }
20
+ export function createChannelQueueStatusController(options) {
21
+ const states = new Map();
22
+ return {
23
+ async update(contextKey, context, text) {
24
+ const state = states.get(contextKey) ?? {};
25
+ if (state.lastText === text && state.messageId) {
26
+ return;
27
+ }
28
+ if (!state.messageId) {
29
+ state.messageId = await options.send(contextKey, context, text);
30
+ state.lastText = text;
31
+ states.set(contextKey, state);
32
+ return;
33
+ }
34
+ await options.edit(contextKey, context, state.messageId, text);
35
+ state.lastText = text;
36
+ states.set(contextKey, state);
37
+ },
38
+ delete(contextKey) {
39
+ states.delete(contextKey);
40
+ },
41
+ };
42
+ }
43
+ export function createChannelActivityRecorder(options) {
44
+ return (request, input) => {
45
+ return options.activityStore.append({
46
+ source: options.source,
47
+ contextKey: request.contextKey,
48
+ actor: input.actor ?? options.actorFor(request),
49
+ workspace: input.workspace ?? options.workspace,
50
+ threadId: input.threadId ?? null,
51
+ ...input,
52
+ });
53
+ };
54
+ }
55
+ export function createChannelAuditRecorder(options) {
56
+ return (request, input) => {
57
+ options.auditLog.append({
58
+ channelId: options.channelId,
59
+ contextKey: input.contextKey ?? request.contextKey,
60
+ actor: input.actor ?? options.actorFor(request),
61
+ actorId: request.authUser?.user.id ?? options.actorIdFor(request),
62
+ actorRole: request.authUser?.groups.map((group) => group.name).join(", ") ?? "unauthenticated",
63
+ ...input,
64
+ });
65
+ };
66
+ }
67
+ export function createChannelPermissionChecker(userStore) {
68
+ return (request, permission) => userStore.hasPermission(request.authUser, permission);
69
+ }
@@ -0,0 +1,51 @@
1
+ import { collectRecentWorkspaceArtifacts, formatArtifactSummary, persistWorkspaceArtifactReport, } from "../../artifacts/artifacts.js";
2
+ import { isEmptyArtifactReport } from "./bot-rendering.js";
3
+ export async function deliverChannelCliArtifacts(options) {
4
+ if (!options.startedAt || !options.turnId) {
5
+ return;
6
+ }
7
+ if (options.state?.artifactsDeliveredForTurnId === options.turnId) {
8
+ return;
9
+ }
10
+ const workspace = options.session.getInfo().workspace;
11
+ const report = await collectRecentWorkspaceArtifacts(workspace, {
12
+ since: options.startedAt,
13
+ until: new Date(),
14
+ maxFileSize: options.config.maxFileSize,
15
+ limit: 5,
16
+ ignoreDirs: options.config.artifactIgnoreDirs,
17
+ ignoreGlobs: options.config.artifactIgnoreGlobs,
18
+ });
19
+ if (isEmptyArtifactReport(report)) {
20
+ if (options.state)
21
+ options.state.artifactsDeliveredForTurnId = options.turnId;
22
+ return;
23
+ }
24
+ const persistedReport = await persistWorkspaceArtifactReport(workspace, options.turnId, report).catch((error) => {
25
+ console.error(`Failed to persist ${options.logPrefix} CLI artifact report:`, error);
26
+ return null;
27
+ });
28
+ const summary = formatArtifactSummary(report.artifacts, report.skippedCount, report.omittedCount);
29
+ if (options.autoSend || options.sendSummaryWhenAutoSendDisabled) {
30
+ await options.sendSummary(summary);
31
+ }
32
+ if (options.autoSend) {
33
+ for (const artifact of (persistedReport?.artifacts ?? report.artifacts).slice(0, 5)) {
34
+ await options.sendArtifact(artifact);
35
+ }
36
+ }
37
+ const info = options.session.getInfo();
38
+ options.appendActivity({
39
+ source: "cli",
40
+ status: "info",
41
+ type: options.autoSend ? "artifacts_sent" : "artifacts_detected",
42
+ contextKey: options.contextKey,
43
+ threadId: info.threadId,
44
+ workspace: info.workspace,
45
+ agentId: info.agentId,
46
+ actor: { channel: "cli", label: `${info.agentLabel} CLI` },
47
+ detail: summary,
48
+ });
49
+ if (options.state)
50
+ options.state.artifactsDeliveredForTurnId = options.turnId;
51
+ }
@@ -1,15 +1,15 @@
1
- import { listAgentAdapterDescriptors } from "./agent-adapter.js";
2
- import { enabledAgents } from "./agent-factory.js";
3
- import { formatQuietHours, isQuietNow, parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
1
+ import { listAgentAdapterDescriptors } from "../../agents/shared/agent-adapter.js";
2
+ import { enabledAgents } from "../../agents/shared/agent-factory.js";
3
+ import { formatQuietHours, isQuietNow, parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "../../state/bot-preferences.js";
4
4
  import { logTailRequests, parseLogsCommand, renderAgentsAction, renderChannelsAction, renderLogTailsAction, } from "./channel-actions.js";
5
5
  import { listChannelDescriptors } from "./channel-adapter.js";
6
- import { friendlyErrorText } from "./error-messages.js";
7
- import { escapeHTML } from "./format.js";
8
- import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, } from "./operations.js";
9
- import { PeerStore } from "./peer-store.js";
6
+ import { friendlyErrorText } from "../../core/error-messages.js";
7
+ import { escapeHTML } from "../../core/format.js";
8
+ import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, } from "../../support/operations.js";
9
+ import { PeerStore } from "../../peers/peer-store.js";
10
10
  import { formatCliPathHTML, formatCliPathPlain, renderActivityTimeline, renderAuditEvents, renderProgressHTML, renderProgressPlain, parseToggle, renderVersionCheckHTML, renderVersionCheckPlain, } from "./bot-rendering.js";
11
11
  import { renderSessionInfoHTML, renderSessionInfoPlain } from "./session-format.js";
12
- import { getAvailableBackends } from "./voice.js";
12
+ import { getAvailableBackends } from "../../artifacts/voice.js";
13
13
  export class ChannelCommandService {
14
14
  config;
15
15
  constructor(config) {
@@ -176,11 +176,7 @@ export class ChannelCommandService {
176
176
  });
177
177
  }
178
178
  const mode = this.effectiveMirrorMode(options.source, options.contextKey, options.preferencesStore);
179
- const minInterval = options.source === "telegram"
180
- ? this.config.telegramMirrorMinUpdateMs
181
- : options.source === "discord"
182
- ? this.config.discordMirrorMinUpdateMs
183
- : this.config.slackMirrorMinUpdateMs;
179
+ const minInterval = this.mirrorMinUpdateMs(options.source);
184
180
  return {
185
181
  plain: [
186
182
  `CLI mirroring: ${mode}`,
@@ -338,25 +334,52 @@ export class ChannelCommandService {
338
334
  };
339
335
  }
340
336
  defaultMirrorMode(source) {
341
- return source === "telegram"
342
- ? this.config.telegramMirrorMode
343
- : source === "discord"
344
- ? this.config.discordMirrorMode
345
- : this.config.slackMirrorMode;
337
+ if (source === "telegram") {
338
+ return this.config.telegramMirrorMode;
339
+ }
340
+ if (source === "discord") {
341
+ return this.config.discordMirrorMode;
342
+ }
343
+ if (source === "slack") {
344
+ return this.config.slackMirrorMode;
345
+ }
346
+ return this.config.webMirrorMode;
347
+ }
348
+ mirrorMinUpdateMs(source) {
349
+ if (source === "telegram") {
350
+ return this.config.telegramMirrorMinUpdateMs;
351
+ }
352
+ if (source === "discord") {
353
+ return this.config.discordMirrorMinUpdateMs;
354
+ }
355
+ if (source === "slack") {
356
+ return this.config.slackMirrorMinUpdateMs;
357
+ }
358
+ return this.config.webMirrorMinUpdateMs;
346
359
  }
347
360
  defaultNotifyMode(source) {
348
- return source === "telegram"
349
- ? this.config.telegramNotifyMode
350
- : source === "discord"
351
- ? this.config.discordNotifyMode
352
- : this.config.slackNotifyMode;
361
+ if (source === "telegram") {
362
+ return this.config.telegramNotifyMode;
363
+ }
364
+ if (source === "discord") {
365
+ return this.config.discordNotifyMode;
366
+ }
367
+ if (source === "slack") {
368
+ return this.config.slackNotifyMode;
369
+ }
370
+ return this.config.notifyMode;
353
371
  }
354
372
  defaultQuietHours(source) {
355
- return source === "telegram"
356
- ? this.config.telegramQuietHours
357
- : source === "discord"
358
- ? this.config.discordQuietHours
359
- : this.config.slackQuietHours;
373
+ if (source === "telegram") {
374
+ return this.config.telegramQuietHours;
375
+ }
376
+ if (source === "discord") {
377
+ return this.config.discordQuietHours;
378
+ }
379
+ if (source === "slack") {
380
+ return this.config.slackQuietHours;
381
+ }
382
+ return this.config.quietHours;
360
383
  }
361
384
  effectiveMirrorMode(source, contextKey, preferencesStore) {
362
385
  return preferencesStore.get(contextKey).mirrorMode ?? this.defaultMirrorMode(source);
@@ -0,0 +1,193 @@
1
+ import { renderExternalMirrorEvent, renderExternalMirrorStatus, trimLine } from "./bot-rendering.js";
2
+ export function createChannelExternalMirrorController(options) {
3
+ const fullEventFilter = options.fullEventFilter ?? ((event) => event.kind === "tool" || event.kind === "task");
4
+ const fullEventLimit = options.fullEventLimit ?? 4;
5
+ const requirePreviousForTerminal = options.requirePreviousForTerminal ?? true;
6
+ const ensureState = (contextKey, snapshot) => {
7
+ const previous = options.states.get(contextKey);
8
+ if (previous && previous.threadId === snapshot.threadId && previous.rolloutPath === snapshot.sourcePath) {
9
+ return { state: previous, previous };
10
+ }
11
+ const state = {
12
+ threadId: snapshot.threadId,
13
+ rolloutPath: snapshot.sourcePath,
14
+ lastLine: snapshot.lineCount,
15
+ turnId: snapshot.activity.turnId,
16
+ startedAt: snapshot.activity.startedAt,
17
+ };
18
+ options.states.set(contextKey, state);
19
+ return { state, previous: undefined };
20
+ };
21
+ const maybeSendTyping = async (contextKey, context, state) => {
22
+ const now = Date.now();
23
+ if (state.lastTypingAt && now - state.lastTypingAt < options.typingIntervalMs) {
24
+ return;
25
+ }
26
+ state.lastTypingAt = now;
27
+ await options.sendTyping(contextKey, context, state);
28
+ };
29
+ const recordTurnStart = (contextKey, session, state, snapshot) => {
30
+ const turnKey = snapshot.activity.turnId ?? snapshot.activity.startedAt?.toISOString() ?? "unknown";
31
+ if (state.activityStartedTurnKey === turnKey) {
32
+ return;
33
+ }
34
+ const info = session.getInfo();
35
+ options.appendActivity({
36
+ source: "cli",
37
+ status: "running",
38
+ type: "cli_turn_started",
39
+ contextKey,
40
+ threadId: snapshot.threadId,
41
+ workspace: info.workspace,
42
+ agentId: info.agentId,
43
+ actor: options.activityActor(snapshot),
44
+ prompt: snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`,
45
+ detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
46
+ });
47
+ state.activityStartedTurnKey = turnKey;
48
+ state.activityFinishedTurnKey = undefined;
49
+ state.activityToolStartLines = [];
50
+ state.activityToolEndLines = [];
51
+ };
52
+ const recordToolEvents = (contextKey, session, state, snapshot) => {
53
+ const info = session.getInfo();
54
+ const loggedStartLines = new Set(state.activityToolStartLines ?? []);
55
+ const loggedEndLines = new Set(state.activityToolEndLines ?? []);
56
+ for (const event of snapshot.events.filter((event) => event.lineNumber > state.lastLine && event.kind === "tool")) {
57
+ if (event.status === "started" && !loggedStartLines.has(event.lineNumber)) {
58
+ options.appendActivity({
59
+ source: "cli",
60
+ status: "running",
61
+ type: "cli_tool_started",
62
+ contextKey,
63
+ threadId: snapshot.threadId,
64
+ workspace: info.workspace,
65
+ agentId: info.agentId,
66
+ actor: options.activityActor(snapshot),
67
+ prompt: snapshot.latestUserMessage ?? undefined,
68
+ detail: event.toolName ?? "tool",
69
+ });
70
+ loggedStartLines.add(event.lineNumber);
71
+ }
72
+ if ((event.status === "finished" || event.status === "failed") && !loggedEndLines.has(event.lineNumber)) {
73
+ options.appendActivity({
74
+ source: "cli",
75
+ status: event.status === "failed" ? "failed" : "completed",
76
+ type: event.status === "failed" ? "cli_tool_failed" : "cli_tool_completed",
77
+ contextKey,
78
+ threadId: snapshot.threadId,
79
+ workspace: info.workspace,
80
+ agentId: info.agentId,
81
+ actor: options.activityActor(snapshot),
82
+ prompt: snapshot.latestUserMessage ?? undefined,
83
+ detail: event.toolName ?? "tool",
84
+ });
85
+ loggedEndLines.add(event.lineNumber);
86
+ }
87
+ }
88
+ state.activityToolStartLines = [...loggedStartLines].slice(-200);
89
+ state.activityToolEndLines = [...loggedEndLines].slice(-200);
90
+ };
91
+ const recordTurnFinished = (contextKey, session, state, snapshot, terminalEvent) => {
92
+ const turnKey = terminalEvent.turnId ?? snapshot.activity.turnId ?? state.startedAt?.toString() ?? "unknown";
93
+ if (state.activityFinishedTurnKey === turnKey) {
94
+ return;
95
+ }
96
+ const info = session.getInfo();
97
+ const startedAt = state.startedAt instanceof Date ? state.startedAt : state.startedAt ? new Date(state.startedAt) : snapshot.activity.startedAt;
98
+ options.appendActivity({
99
+ source: "cli",
100
+ status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
101
+ type: "cli_turn_finished",
102
+ contextKey,
103
+ threadId: snapshot.threadId,
104
+ workspace: info.workspace,
105
+ agentId: info.agentId,
106
+ actor: options.activityActor(snapshot),
107
+ prompt: snapshot.latestUserMessage ?? undefined,
108
+ detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
109
+ durationMs: startedAt && terminalEvent.timestamp ? Math.max(0, terminalEvent.timestamp.getTime() - startedAt.getTime()) : undefined,
110
+ });
111
+ state.activityFinishedTurnKey = turnKey;
112
+ };
113
+ return {
114
+ async mirror(contextKey, context, session, snapshot) {
115
+ const { state, previous } = ensureState(contextKey, snapshot);
116
+ const mirrorMode = options.mirrorMode(contextKey);
117
+ if (snapshot.activity.active) {
118
+ state.turnId = snapshot.activity.turnId;
119
+ state.startedAt = snapshot.activity.startedAt;
120
+ recordTurnStart(contextKey, session, state, snapshot);
121
+ if (mirrorMode !== "off") {
122
+ await maybeSendTyping(contextKey, context, state);
123
+ }
124
+ if (mirrorMode === "final") {
125
+ await options.sendWorkingNotice(contextKey, context, state, snapshot, trimLine(snapshot.latestUserMessage ?? "", 250));
126
+ state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
127
+ return;
128
+ }
129
+ if (mirrorMode === "off") {
130
+ state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
131
+ return;
132
+ }
133
+ const status = renderExternalMirrorStatus(snapshot, options.queueLength(contextKey));
134
+ const now = Date.now();
135
+ const canUpdateStatus = !state.latestStatusAt || now - state.latestStatusAt >= options.minUpdateMs(contextKey);
136
+ if (!state.statusMessageId) {
137
+ state.statusMessageId = await options.sendStatus(contextKey, context, state, status);
138
+ state.latestStatusAt = now;
139
+ }
140
+ else if (state.latestStatus !== status.plain && canUpdateStatus) {
141
+ await options.editStatus(contextKey, context, state, state.statusMessageId, status);
142
+ state.latestStatusAt = now;
143
+ }
144
+ state.latestStatus = status.plain;
145
+ if (mirrorMode === "full") {
146
+ const newEvents = snapshot.events
147
+ .filter((event) => event.lineNumber > (state.latestMirroredEventLine ?? state.lastLine))
148
+ .filter(fullEventFilter)
149
+ .slice(-fullEventLimit);
150
+ for (const event of newEvents) {
151
+ const rendered = renderExternalMirrorEvent(event);
152
+ if (!rendered) {
153
+ continue;
154
+ }
155
+ await options.sendEvent(contextKey, context, state, rendered);
156
+ state.latestMirroredEventLine = event.lineNumber;
157
+ }
158
+ }
159
+ recordToolEvents(contextKey, session, state, snapshot);
160
+ state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
161
+ return;
162
+ }
163
+ if (requirePreviousForTerminal && !previous) {
164
+ state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
165
+ return;
166
+ }
167
+ const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
168
+ if (terminalEvent) {
169
+ recordTurnFinished(contextKey, session, state, snapshot, terminalEvent);
170
+ if (mirrorMode !== "off") {
171
+ const doneText = `${snapshot.agentLabel} CLI task ${terminalEvent.status}.`;
172
+ if (state.statusMessageId || options.shouldSendDone?.(contextKey) !== false) {
173
+ await options.sendDone(contextKey, context, state, doneText);
174
+ }
175
+ }
176
+ const finalAgent = snapshot.events.filter((event) => event.kind === "agent" && event.text).at(-1);
177
+ if (mirrorMode !== "off" && mirrorMode !== "status" && finalAgent?.text && finalAgent.lineNumber !== state.latestAgentLine) {
178
+ await options.sendFinalAnswer(contextKey, context, state, snapshot, finalAgent.text);
179
+ state.latestAgentLine = finalAgent.lineNumber;
180
+ }
181
+ await options.deliverArtifacts(contextKey, context, session, state, terminalEvent.turnId);
182
+ }
183
+ state.workingNoticeTurnKey = undefined;
184
+ state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
185
+ },
186
+ get(contextKey) {
187
+ return options.states.get(contextKey);
188
+ },
189
+ delete(contextKey) {
190
+ options.states.delete(contextKey);
191
+ },
192
+ };
193
+ }
@@ -0,0 +1,52 @@
1
+ import { getExternalSnapshotForSession } from "../../agents/shared/agent-activity.js";
2
+ import { capabilitiesOf } from "./bot-rendering.js";
3
+ export async function monitorChannelExternalContexts(options) {
4
+ const contextKeys = new Set([
5
+ ...options.registry.listContexts().map((context) => context.contextKey),
6
+ ...options.promptStore.listContextKeys(),
7
+ ].filter(options.isContextKey));
8
+ for (const contextKey of contextKeys) {
9
+ await monitorChannelExternalContext(options, contextKey);
10
+ }
11
+ }
12
+ async function monitorChannelExternalContext(options, contextKey) {
13
+ if (!options.canSendSystemMessages(contextKey) || options.isAllowed?.(contextKey) === false) {
14
+ return;
15
+ }
16
+ const session = await options.registry.getOrCreate(contextKey, { deferThreadStart: true }).catch(() => null);
17
+ const context = options.contextForKey(contextKey);
18
+ if (!session || !context) {
19
+ return;
20
+ }
21
+ const queueLength = options.promptStore.list(contextKey).length;
22
+ const paused = options.promptStore.isPaused(contextKey);
23
+ const shouldDrain = queueLength > 0 && !paused && !session.isProcessing();
24
+ if (!capabilitiesOf(session.getInfo()).externalActivity || !session.getActiveThreadId()) {
25
+ if (shouldDrain) {
26
+ await options.drainQueue(contextKey, context, session);
27
+ }
28
+ return;
29
+ }
30
+ const snapshot = getExternalSnapshotForSession(session, options.config, {
31
+ afterLine: options.previousLastLine(contextKey) ?? Number.MAX_SAFE_INTEGER,
32
+ }) ?? getExternalSnapshotForSession(session, options.config, { maxEvents: 1 });
33
+ if (!snapshot) {
34
+ if (shouldDrain) {
35
+ await options.drainQueue(contextKey, context, session);
36
+ }
37
+ return;
38
+ }
39
+ if (!session.isProcessing()) {
40
+ await options.mirrorSnapshot(contextKey, context, session, snapshot);
41
+ }
42
+ if (snapshot.activity.active) {
43
+ if (queueLength > 0) {
44
+ await options.updateQueueStatus(contextKey, context, `Waiting for ${snapshot.agentLabel} CLI task... ${queueLength} queued${paused ? " (paused)" : ""}.`).catch(() => { });
45
+ }
46
+ return;
47
+ }
48
+ if (shouldDrain) {
49
+ await options.updateQueueStatus(contextKey, context, `CLI task finished, running queued prompt 1/${queueLength}.`).catch(() => { });
50
+ await options.drainQueue(contextKey, context, session);
51
+ }
52
+ }
@@ -49,11 +49,7 @@ export class ChannelMirrorRegistry {
49
49
  return mirrors.some((mirror) => mirror.queuePaused) || this.promptStore.isPaused(sourceContextKey);
50
50
  }
51
51
  effectiveMirrorMode(contextKey, source, preferences) {
52
- const configured = source === "telegram"
53
- ? this.config.telegramMirrorMode
54
- : source === "discord"
55
- ? this.config.discordMirrorMode
56
- : this.config.slackMirrorMode;
52
+ const configured = configuredMirrorMode(this.config, source);
57
53
  return preferences.get(contextKey).mirrorMode ?? configured;
58
54
  }
59
55
  snapshot() {
@@ -80,5 +76,17 @@ export function activeSessionSourceForContextKey(contextKey) {
80
76
  return "cli";
81
77
  }
82
78
  export function isMirrorChannelSource(source) {
83
- return source === "telegram" || source === "discord" || source === "slack";
79
+ return source === "telegram" || source === "discord" || source === "slack" || source === "web";
80
+ }
81
+ function configuredMirrorMode(config, source) {
82
+ if (source === "telegram") {
83
+ return config.telegramMirrorMode;
84
+ }
85
+ if (source === "discord") {
86
+ return config.discordMirrorMode;
87
+ }
88
+ if (source === "slack") {
89
+ return config.slackMirrorMode;
90
+ }
91
+ return config.webMirrorMode;
84
92
  }
@@ -1,6 +1,6 @@
1
- import { friendlyErrorText } from "./error-messages.js";
2
- import { RemoteRelayClient } from "./peer-client.js";
3
- import { peerPromptProxyPayload } from "./remote-prompt.js";
1
+ import { friendlyErrorText } from "../../core/error-messages.js";
2
+ import { RemoteRelayClient } from "../../peers/peer-client.js";
3
+ import { peerPromptProxyPayload } from "../../runtime/remote-prompt.js";
4
4
  export async function runChannelPeerPrompt(options) {
5
5
  if (!options.targetPeerId) {
6
6
  return false;
@@ -0,0 +1,37 @@
1
+ export async function queueChannelPromptIfBusy(options) {
2
+ if (!options.busy.busy) {
3
+ return false;
4
+ }
5
+ const item = options.fromQueue && isQueuedPrompt(options.envelope)
6
+ ? options.envelope
7
+ : options.promptStore.enqueue(options.request.contextKey, options.envelope);
8
+ const position = options.promptStore.list(options.request.contextKey).findIndex((queued) => queued.id === item.id) + 1;
9
+ const text = options.busy.kind === "external"
10
+ ? `Queued prompt ${item.id} at position ${position}. The ${options.busy.agentLabel} session is still active and is processing a previous task.`
11
+ : `Queued prompt ${item.id} at position ${position}.`;
12
+ await options.reply(options.request, text, {
13
+ buttons: [[{ label: "Cancel queued message", action: `${options.actionPrefix}_queue_cancel:${options.request.contextKey}:${item.id}` }]],
14
+ });
15
+ options.appendActivity(options.request, {
16
+ status: "queued",
17
+ type: "prompt_queued",
18
+ prompt: item.description,
19
+ detail: text,
20
+ correlationId: item.correlationId,
21
+ });
22
+ options.audit(options.request, {
23
+ action: "prompt_queued",
24
+ status: "ok",
25
+ promptId: item.id,
26
+ correlationId: item.correlationId,
27
+ description: item.description,
28
+ });
29
+ return true;
30
+ }
31
+ function isQueuedPrompt(value) {
32
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
33
+ return false;
34
+ }
35
+ const candidate = value;
36
+ return typeof candidate.id === "string" && typeof candidate.createdAt === "number" && typeof candidate.description === "string";
37
+ }