@nordbyte/nordrelay 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +80 -11
- package/README.md +154 -22
- package/dist/access-control.js +7 -1
- package/dist/activity-events.js +44 -0
- package/dist/audit-log.js +40 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +535 -11
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +40 -7
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +369 -0
- package/dist/channel-mirror-registry.js +77 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-service.js +237 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +93 -13
- package/dist/config.js +103 -8
- package/dist/context-key.js +87 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2073 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +57 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +36 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +87 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +256 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-runtime-service.js +636 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +294 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +897 -394
- package/dist/remote-prompt.js +98 -0
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/support-bundle.js +1 -0
- package/dist/telegram-access-commands.js +15 -2
- package/dist/telegram-access-middleware.js +16 -3
- package/dist/telegram-agent-commands.js +25 -0
- package/dist/telegram-artifact-commands.js +46 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +16 -6
- package/dist/telegram-operational-commands.js +14 -6
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/telegram-queue-commands.js +74 -4
- package/dist/telegram-support-command.js +7 -0
- package/dist/telegram-update-commands.js +27 -0
- package/dist/user-management.js +208 -0
- package/dist/web-api-contract.js +17 -0
- package/dist/web-dashboard-access-routes.js +74 -1
- package/dist/web-dashboard-artifact-routes.js +3 -3
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-pages.js +109 -13
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +2 -0
- package/dist/web-dashboard.js +160 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +779 -55
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
package/dist/channel-actions.js
CHANGED
|
@@ -7,7 +7,9 @@ export function renderChannelsAction(descriptors) {
|
|
|
7
7
|
const plain = [
|
|
8
8
|
"Channel adapters:",
|
|
9
9
|
...descriptors.map((descriptor) => {
|
|
10
|
-
const status = descriptor.status === "available"
|
|
10
|
+
const status = descriptor.status === "available"
|
|
11
|
+
? descriptor.enabled === false ? "available / disabled" : "available / enabled"
|
|
12
|
+
: "planned";
|
|
11
13
|
return `${descriptor.label}: ${status} · ${descriptor.capabilities.join(", ")}`;
|
|
12
14
|
}),
|
|
13
15
|
].join("\n");
|
|
@@ -15,8 +17,11 @@ export function renderChannelsAction(descriptors) {
|
|
|
15
17
|
"<b>Channel adapters:</b>",
|
|
16
18
|
...descriptors.map((descriptor) => {
|
|
17
19
|
const statusIcon = descriptor.status === "available" ? "✅" : "🟡";
|
|
20
|
+
const status = descriptor.status === "available"
|
|
21
|
+
? descriptor.enabled === false ? "available / disabled" : "available / enabled"
|
|
22
|
+
: descriptor.status;
|
|
18
23
|
const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
|
|
19
|
-
return `${statusIcon} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(
|
|
24
|
+
return `${statusIcon} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(status)}</code>\n <code>${escapeHTML(descriptor.capabilities.join(", "))}</code>${notes}`;
|
|
20
25
|
}),
|
|
21
26
|
].join("\n");
|
|
22
27
|
return { plain, html };
|
package/dist/channel-adapter.js
CHANGED
|
@@ -9,14 +9,17 @@ const TELEGRAM_CAPABILITIES = [
|
|
|
9
9
|
"topics",
|
|
10
10
|
"webhooks",
|
|
11
11
|
];
|
|
12
|
+
const DISCORD_CAPABILITIES = [
|
|
13
|
+
"text",
|
|
14
|
+
"streaming-edits",
|
|
15
|
+
"typing",
|
|
16
|
+
"inline-buttons",
|
|
17
|
+
"files",
|
|
18
|
+
"photos",
|
|
19
|
+
"voice",
|
|
20
|
+
"topics",
|
|
21
|
+
];
|
|
12
22
|
const PLANNED_CHANNELS = [
|
|
13
|
-
{
|
|
14
|
-
id: "discord",
|
|
15
|
-
label: "Discord",
|
|
16
|
-
capabilities: ["text", "streaming-edits", "typing", "inline-buttons", "files", "photos", "voice"],
|
|
17
|
-
status: "planned",
|
|
18
|
-
notes: "Adapter boundary is ready; runtime integration still needs bot credentials and event mapping.",
|
|
19
|
-
},
|
|
20
23
|
{
|
|
21
24
|
id: "whatsapp",
|
|
22
25
|
label: "WhatsApp",
|
|
@@ -42,17 +45,47 @@ export class TelegramChannelAdapter {
|
|
|
42
45
|
label = "Telegram";
|
|
43
46
|
capabilities = new Set(TELEGRAM_CAPABILITIES);
|
|
44
47
|
describe() {
|
|
48
|
+
const requested = process.env.TELEGRAM_ENABLED !== "false";
|
|
49
|
+
const enabled = requested && Boolean(process.env.TELEGRAM_BOT_TOKEN);
|
|
50
|
+
return {
|
|
51
|
+
id: this.id,
|
|
52
|
+
label: this.label,
|
|
53
|
+
capabilities: [...this.capabilities],
|
|
54
|
+
status: "available",
|
|
55
|
+
enabled,
|
|
56
|
+
notes: enabled
|
|
57
|
+
? "Telegram bot runtime is enabled."
|
|
58
|
+
: requested
|
|
59
|
+
? "Telegram bot runtime is disabled because TELEGRAM_BOT_TOKEN is missing."
|
|
60
|
+
: "Telegram bot runtime is disabled.",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export class DiscordChannelAdapter {
|
|
65
|
+
id = "discord";
|
|
66
|
+
label = "Discord";
|
|
67
|
+
capabilities = new Set(DISCORD_CAPABILITIES);
|
|
68
|
+
describe() {
|
|
69
|
+
const requested = process.env.DISCORD_ENABLED === "true";
|
|
70
|
+
const enabled = requested && Boolean(process.env.DISCORD_BOT_TOKEN);
|
|
45
71
|
return {
|
|
46
72
|
id: this.id,
|
|
47
73
|
label: this.label,
|
|
48
74
|
capabilities: [...this.capabilities],
|
|
49
75
|
status: "available",
|
|
76
|
+
enabled,
|
|
77
|
+
notes: enabled
|
|
78
|
+
? "Discord bot runtime is enabled."
|
|
79
|
+
: requested
|
|
80
|
+
? "Discord bot runtime is disabled because DISCORD_BOT_TOKEN is missing."
|
|
81
|
+
: "Enable with DISCORD_ENABLED=true and DISCORD_BOT_TOKEN.",
|
|
50
82
|
};
|
|
51
83
|
}
|
|
52
84
|
}
|
|
53
85
|
export function listChannelDescriptors() {
|
|
54
86
|
return [
|
|
55
87
|
new TelegramChannelAdapter().describe(),
|
|
88
|
+
new DiscordChannelAdapter().describe(),
|
|
56
89
|
...PLANNED_CHANNELS,
|
|
57
90
|
];
|
|
58
91
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const textOption = (name = "value", description = "Value", required = false) => ({
|
|
2
|
+
type: 3,
|
|
3
|
+
name,
|
|
4
|
+
description,
|
|
5
|
+
required,
|
|
6
|
+
});
|
|
7
|
+
export const CHANNEL_COMMANDS = [
|
|
8
|
+
{ name: "start", description: "Welcome and status", discordDescription: "Start or inspect the current NordRelay context" },
|
|
9
|
+
{ name: "help", description: "Command reference", discordDescription: "Show Discord adapter help" },
|
|
10
|
+
{ name: "prompt", description: "Send a prompt to the selected agent", telegram: false, discordOptions: [textOption("text", "Prompt text", true)] },
|
|
11
|
+
{ name: "link", description: "Link account to NordRelay user", telegramDescription: "Link Telegram to NordRelay user", discordDescription: "Link this Discord account with a NordRelay code", discordOptions: [textOption("value", "Link code", true)] },
|
|
12
|
+
{ name: "whoami", description: "Show your NordRelay user", discordDescription: "Show linked NordRelay user" },
|
|
13
|
+
{ name: "register_chat", description: "Admin: enable this group chat", discord: false },
|
|
14
|
+
{ name: "register_channel", description: "Enable this Discord channel for NordRelay", telegram: false },
|
|
15
|
+
{ name: "channels", description: "Messaging adapter status", discordDescription: "Show channel adapters" },
|
|
16
|
+
{ name: "peers", description: "NordRelay peer status", discordDescription: "Show paired NordRelay instances" },
|
|
17
|
+
{ name: "target", description: "Select local or peer target", discordDescription: "Select local or peer target", discordOptions: [textOption("value", "local or peer id")] },
|
|
18
|
+
{ name: "agents", description: "Agent adapter status", discordDescription: "Show agent adapters" },
|
|
19
|
+
{ name: "agent", description: "Select agent", discordDescription: "Select or show the active agent", discordOptions: [textOption("value", "Agent id")] },
|
|
20
|
+
{ name: "new", description: "Start a new thread", discordDescription: "Create a new session", discordOptions: [textOption("value", "Workspace path")] },
|
|
21
|
+
{ name: "session", description: "Current thread details", discordDescription: "Show the active session" },
|
|
22
|
+
{ name: "sessions", description: "Browse and switch threads", discordDescription: "Browse recent sessions", discordOptions: [textOption("query", "Search query")] },
|
|
23
|
+
{ name: "switch", description: "Switch to a thread by ID", discordDescription: "Switch to a session", discordOptions: [textOption("thread_id", "Thread id", true)] },
|
|
24
|
+
{ name: "attach", description: "Bind a session to this topic", discordDescription: "Attach a session", discordOptions: [textOption("thread_id", "Thread id", true)] },
|
|
25
|
+
{ name: "handback", description: "Hand session back to CLI", discordDescription: "Hand the active session back to the native CLI" },
|
|
26
|
+
{ name: "sync", description: "Sync active session from CLI state", discordDescription: "Sync from local agent state" },
|
|
27
|
+
{ name: "pinned", description: "Show pinned threads" },
|
|
28
|
+
{ name: "pin", description: "Pin current or given thread", discordOptions: [textOption("value", "Thread id")] },
|
|
29
|
+
{ name: "unpin", description: "Unpin current or given thread", discordOptions: [textOption("value", "Thread id")] },
|
|
30
|
+
{ name: "retry", description: "Resend the last prompt", discordDescription: "Retry the last prompt" },
|
|
31
|
+
{ name: "queue", description: "Show queued prompts", discordDescription: "Show or manage queue", discordOptions: [textOption("action", "pause/resume/clear/run/cancel/top/up/down"), textOption("id", "Queue id")] },
|
|
32
|
+
{ name: "cancel", description: "Cancel a queued prompt", discordOptions: [textOption("value", "Queue id", true)] },
|
|
33
|
+
{ name: "clearqueue", description: "Clear queued prompts", discordDescription: "Clear queue" },
|
|
34
|
+
{ name: "artifacts", description: "List or resend generated files", discordDescription: "List or send artifacts", discordOptions: [textOption("value", "zip <turn-id>")] },
|
|
35
|
+
{ name: "workspaces", description: "List allowed workspaces" },
|
|
36
|
+
{ name: "abort", description: "Cancel current operation", discordDescription: "Abort the active task" },
|
|
37
|
+
{ name: "stop", description: "Cancel current operation", discordDescription: "Abort the active task" },
|
|
38
|
+
{ name: "launch", description: "Select launch profile", discordOptions: [textOption("value", "Launch profile id")] },
|
|
39
|
+
{ name: "launch_profiles", description: "Select launch profile", discordOptions: [textOption("value", "Launch profile id")] },
|
|
40
|
+
{ name: "fast", description: "Toggle fast mode", discordOptions: [textOption("value", "on/off")] },
|
|
41
|
+
{ name: "model", description: "View and change model", discordDescription: "Select or show models", discordOptions: [textOption("value", "Model id")] },
|
|
42
|
+
{ name: "effort", description: "Set reasoning effort", discordDescription: "Select reasoning effort", discordOptions: [textOption("value", "Reasoning value")] },
|
|
43
|
+
{ name: "reasoning", description: "Set reasoning effort", discordDescription: "Select reasoning effort", discordOptions: [textOption("value", "Reasoning value")] },
|
|
44
|
+
{ name: "mirror", description: "Control CLI mirroring", discordDescription: "Set mirror mode", discordOptions: [textOption("value", "off/status/final/full")] },
|
|
45
|
+
{ name: "notify", description: "Control notifications", discordDescription: "Set notification mode", discordOptions: [textOption("value", "off/minimal/all")] },
|
|
46
|
+
{ name: "auth", description: "Check auth status", discordDescription: "Show selected agent auth status" },
|
|
47
|
+
{ name: "login", description: "Start authentication", discordDescription: "Start selected agent login" },
|
|
48
|
+
{ name: "logout", description: "Sign out", discordDescription: "Sign out of the selected agent" },
|
|
49
|
+
{ name: "voice", description: "Voice transcription status", discordDescription: "Show or change voice settings", discordOptions: [textOption("value", "transcribe-only on/off")] },
|
|
50
|
+
{ name: "tasks", description: "Current turn progress", discordDescription: "Show recent tasks", discordOptions: [textOption("value", "Limit")] },
|
|
51
|
+
{ name: "progress", description: "Current turn progress", discordDescription: "Show current turn progress" },
|
|
52
|
+
{ name: "activity", description: "Thread activity timeline", discordDescription: "Show recent activity", discordOptions: [textOption("value", "Limit")] },
|
|
53
|
+
{ name: "audit", description: "Admin: recent audit events", discordDescription: "Show recent audit events", discordOptions: [textOption("value", "Limit")] },
|
|
54
|
+
{ name: "status", description: "Connector runtime status", discordDescription: "Show status" },
|
|
55
|
+
{ name: "health", description: "Connector health report", discordDescription: "Show health" },
|
|
56
|
+
{ name: "version", description: "Connector version", discordDescription: "Show versions" },
|
|
57
|
+
{ name: "logs", description: "Admin: show connector logs", discordDescription: "Show logs", discordOptions: [textOption("value", "Target and line count")] },
|
|
58
|
+
{ name: "diagnostics", description: "Admin: connector diagnostics", discordDescription: "Show diagnostics" },
|
|
59
|
+
{ name: "support", description: "Admin: export diagnostics bundle", discordDescription: "Show support diagnostics" },
|
|
60
|
+
{ name: "lock", description: "Lock session writes to you", discordDescription: "Lock this context" },
|
|
61
|
+
{ name: "unlock", description: "Release session write lock", discordDescription: "Unlock this context" },
|
|
62
|
+
{ name: "locks", description: "List session write locks", discordDescription: "List locks" },
|
|
63
|
+
{ name: "restart", description: "Admin: restart connector", discordDescription: "Restart NordRelay" },
|
|
64
|
+
{ name: "update", description: "Admin: update connector or agents", discordDescription: "Update NordRelay or agents", discordOptions: [textOption("target", "jobs, install, log, cancel, input, or agent id"), textOption("agent", "Agent id or job id"), textOption("input", "Text for update input")] },
|
|
65
|
+
];
|
|
66
|
+
export function telegramCommandCatalog() {
|
|
67
|
+
return CHANNEL_COMMANDS
|
|
68
|
+
.filter((entry) => entry.telegram !== false)
|
|
69
|
+
.map((entry) => ({
|
|
70
|
+
command: entry.name,
|
|
71
|
+
description: entry.telegramDescription ?? entry.description,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
export function discordCommandCatalog() {
|
|
75
|
+
return CHANNEL_COMMANDS
|
|
76
|
+
.filter((entry) => entry.discord !== false)
|
|
77
|
+
.map((entry) => ({
|
|
78
|
+
name: entry.name,
|
|
79
|
+
description: entry.discordDescription ?? entry.description,
|
|
80
|
+
options: entry.discordOptions ?? [],
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
export function discordHelpCommandList() {
|
|
84
|
+
return discordCommandCatalog()
|
|
85
|
+
.filter((entry) => !["start", "help", "prompt"].includes(entry.name))
|
|
86
|
+
.map((entry) => `/${entry.name}`)
|
|
87
|
+
.join(", ");
|
|
88
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
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";
|
|
4
|
+
import { logTailRequests, parseLogsCommand, renderAgentsAction, renderChannelsAction, renderLogTailsAction, } from "./channel-actions.js";
|
|
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";
|
|
10
|
+
import { formatCliPathHTML, formatCliPathPlain, renderActivityTimeline, renderAuditEvents, renderProgressHTML, renderProgressPlain, parseToggle, renderVersionCheckHTML, renderVersionCheckPlain, } from "./bot-rendering.js";
|
|
11
|
+
import { renderSessionInfoHTML, renderSessionInfoPlain } from "./session-format.js";
|
|
12
|
+
import { getAvailableBackends } from "./voice.js";
|
|
13
|
+
export class ChannelCommandService {
|
|
14
|
+
config;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
renderChannels() {
|
|
19
|
+
return renderChannelsAction(listChannelDescriptors());
|
|
20
|
+
}
|
|
21
|
+
renderAgents(agentIds = enabledAgents(this.config)) {
|
|
22
|
+
return renderAgentsAction(listAgentAdapterDescriptors(), agentIds);
|
|
23
|
+
}
|
|
24
|
+
renderPeers() {
|
|
25
|
+
const peers = new PeerStore().listPublic();
|
|
26
|
+
if (peers.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
plain: "No NordRelay peers configured.",
|
|
29
|
+
html: "No NordRelay peers configured.",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const plain = peers.map((peer) => [
|
|
33
|
+
`${peer.name} (${peer.id}) ${peer.enabled ? "enabled" : "disabled"}`,
|
|
34
|
+
`URL: ${peer.url ?? "-"}`,
|
|
35
|
+
`Node: ${peer.nodeId}`,
|
|
36
|
+
`Scopes: ${peer.scopes.join(", ") || "-"}`,
|
|
37
|
+
peer.remoteStatus || peer.lastLatencyMs !== undefined ? `Health: ${peer.remoteStatus ?? "seen"}${peer.lastLatencyMs !== undefined ? ` / ${peer.lastLatencyMs}ms` : ""}${peer.remoteVersion ? ` / v${peer.remoteVersion}` : ""}` : "",
|
|
38
|
+
Object.keys(peer.workspaceAliases ?? {}).length > 0 ? `Aliases: ${Object.entries(peer.workspaceAliases).map(([alias, workspace]) => `${alias}=${workspace}`).join(", ")}` : "",
|
|
39
|
+
peer.lastSeenAt ? `Last seen: ${peer.lastSeenAt}` : "",
|
|
40
|
+
peer.lastError ? `Last error: ${peer.lastError}` : "",
|
|
41
|
+
].filter(Boolean).join("\n")).join("\n\n");
|
|
42
|
+
const html = peers.map((peer) => [
|
|
43
|
+
`<b>${escapeHTML(peer.name)} (${escapeHTML(peer.id)})</b> <code>${peer.enabled ? "enabled" : "disabled"}</code>`,
|
|
44
|
+
`<b>URL:</b> <code>${escapeHTML(peer.url ?? "-")}</code>`,
|
|
45
|
+
`<b>Node:</b> <code>${escapeHTML(peer.nodeId)}</code>`,
|
|
46
|
+
`<b>Scopes:</b> <code>${escapeHTML(peer.scopes.join(", ") || "-")}</code>`,
|
|
47
|
+
peer.remoteStatus || peer.lastLatencyMs !== undefined ? `<b>Health:</b> <code>${escapeHTML(`${peer.remoteStatus ?? "seen"}${peer.lastLatencyMs !== undefined ? ` / ${peer.lastLatencyMs}ms` : ""}${peer.remoteVersion ? ` / v${peer.remoteVersion}` : ""}`)}</code>` : "",
|
|
48
|
+
Object.keys(peer.workspaceAliases ?? {}).length > 0 ? `<b>Aliases:</b> <code>${escapeHTML(Object.entries(peer.workspaceAliases).map(([alias, workspace]) => `${alias}=${workspace}`).join(", "))}</code>` : "",
|
|
49
|
+
peer.lastSeenAt ? `<b>Last seen:</b> <code>${escapeHTML(peer.lastSeenAt)}</code>` : "",
|
|
50
|
+
peer.lastError ? `<b>Last error:</b> <code>${escapeHTML(peer.lastError)}</code>` : "",
|
|
51
|
+
].filter(Boolean).join("\n")).join("\n\n");
|
|
52
|
+
return { plain, html };
|
|
53
|
+
}
|
|
54
|
+
renderTargetPreference(options) {
|
|
55
|
+
const argument = options.argument.trim();
|
|
56
|
+
const peers = new PeerStore().listPublic().filter((peer) => peer.enabled && peer.url);
|
|
57
|
+
if (argument) {
|
|
58
|
+
const normalized = argument.toLowerCase();
|
|
59
|
+
if (normalized === "local") {
|
|
60
|
+
options.preferencesStore.update(options.contextKey, { targetPeerId: null });
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const peer = peers.find((candidate) => candidate.id === argument || candidate.name.toLowerCase() === normalized || candidate.nodeId === argument);
|
|
64
|
+
if (!peer) {
|
|
65
|
+
return usageResponse("Unknown peer target. Use /target local or /target <peer-id>.");
|
|
66
|
+
}
|
|
67
|
+
options.preferencesStore.update(options.contextKey, { targetPeerId: peer.id });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const current = options.preferencesStore.get(options.contextKey).targetPeerId;
|
|
71
|
+
const currentPeer = current ? peers.find((peer) => peer.id === current) : null;
|
|
72
|
+
const target = currentPeer ? `${currentPeer.name} (${currentPeer.id})` : "local";
|
|
73
|
+
const available = peers.map((peer) => `${peer.id} ${peer.name}`).join("\n") || "No enabled outbound peers.";
|
|
74
|
+
return {
|
|
75
|
+
plain: [`Target: ${target}`, "", "Available peers:", available].join("\n"),
|
|
76
|
+
html: [`<b>Target:</b> <code>${escapeHTML(target)}</code>`, "", "<b>Available peers:</b>", `<code>${escapeHTML(available)}</code>`].join("\n"),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async renderLogs(argument) {
|
|
80
|
+
const logRequest = parseLogsCommand(argument);
|
|
81
|
+
const logs = await Promise.all(logTailRequests(logRequest.target).map(async (request) => ({
|
|
82
|
+
title: request.title,
|
|
83
|
+
tail: await readFormattedLogTail(logRequest.lines, request.path),
|
|
84
|
+
})));
|
|
85
|
+
return renderLogTailsAction(logs);
|
|
86
|
+
}
|
|
87
|
+
async renderVersion() {
|
|
88
|
+
const health = await getConnectorHealth(cliPathOptions(this.config));
|
|
89
|
+
const state = await readConnectorState();
|
|
90
|
+
const versions = await getVersionChecks(cliPathOptions(this.config));
|
|
91
|
+
const plain = [
|
|
92
|
+
renderVersionCheckPlain(versions.nordrelay),
|
|
93
|
+
`Runtime status: ${state.status ?? "unknown"}`,
|
|
94
|
+
formatCliPathPlain("Codex CLI", health.codexCliPath, health.codexCli),
|
|
95
|
+
renderVersionCheckPlain(versions.codex),
|
|
96
|
+
formatCliPathPlain("Pi CLI", health.piCliPath, health.piCli),
|
|
97
|
+
renderVersionCheckPlain(versions.pi),
|
|
98
|
+
formatCliPathPlain("Hermes CLI", health.hermesCliPath, health.hermesCli),
|
|
99
|
+
renderVersionCheckPlain(versions.hermes),
|
|
100
|
+
formatCliPathPlain("OpenClaw CLI", health.openClawCliPath, health.openClawCli),
|
|
101
|
+
renderVersionCheckPlain(versions.openclaw),
|
|
102
|
+
formatCliPathPlain("Claude Code CLI", health.claudeCodeCliPath, health.claudeCodeCli),
|
|
103
|
+
renderVersionCheckPlain(versions.claudeCode),
|
|
104
|
+
].join("\n");
|
|
105
|
+
const html = [
|
|
106
|
+
renderVersionCheckHTML(versions.nordrelay),
|
|
107
|
+
`<b>Runtime status:</b> <code>${escapeHTML(state.status ?? "unknown")}</code>`,
|
|
108
|
+
formatCliPathHTML("Codex CLI", health.codexCliPath, health.codexCli),
|
|
109
|
+
renderVersionCheckHTML(versions.codex),
|
|
110
|
+
formatCliPathHTML("Pi CLI", health.piCliPath, health.piCli),
|
|
111
|
+
renderVersionCheckHTML(versions.pi),
|
|
112
|
+
formatCliPathHTML("Hermes CLI", health.hermesCliPath, health.hermesCli),
|
|
113
|
+
renderVersionCheckHTML(versions.hermes),
|
|
114
|
+
formatCliPathHTML("OpenClaw CLI", health.openClawCliPath, health.openClawCli),
|
|
115
|
+
renderVersionCheckHTML(versions.openclaw),
|
|
116
|
+
formatCliPathHTML("Claude Code CLI", health.claudeCodeCliPath, health.claudeCodeCli),
|
|
117
|
+
renderVersionCheckHTML(versions.claudeCode),
|
|
118
|
+
].join("\n");
|
|
119
|
+
return { plain, html };
|
|
120
|
+
}
|
|
121
|
+
renderAuthStatus(status) {
|
|
122
|
+
const icon = status.authenticated ? "✅" : "❌";
|
|
123
|
+
return {
|
|
124
|
+
plain: [
|
|
125
|
+
`${icon} ${status.label} auth: ${status.authenticated ? "authenticated" : "not authenticated"}`,
|
|
126
|
+
`Method: ${status.method ?? "-"}`,
|
|
127
|
+
`Detail: ${status.detail}`,
|
|
128
|
+
].join("\n"),
|
|
129
|
+
html: [
|
|
130
|
+
`<b>${icon} ${escapeHTML(status.label)} auth:</b> <code>${status.authenticated ? "authenticated" : "not authenticated"}</code>`,
|
|
131
|
+
`<b>Method:</b> <code>${escapeHTML(status.method ?? "-")}</code>`,
|
|
132
|
+
`<b>Detail:</b> <code>${escapeHTML(status.detail)}</code>`,
|
|
133
|
+
].join("\n"),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
renderAuthActionResult(action, result) {
|
|
137
|
+
const label = action === "login" ? "Login" : "Logout";
|
|
138
|
+
const icon = result.success ? "✅" : "❌";
|
|
139
|
+
return {
|
|
140
|
+
plain: [`${icon} ${label} ${result.success ? "started" : "failed"}.`, "", result.message].join("\n"),
|
|
141
|
+
html: [`<b>${icon} ${escapeHTML(label)} ${result.success ? "started" : "failed"}.</b>`, "", `<code>${escapeHTML(result.message)}</code>`].join("\n"),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
renderHostAuthInstruction(label, command, action) {
|
|
145
|
+
const text = `${label} ${action} is not managed remotely. Run this on the host: ${command}`;
|
|
146
|
+
return {
|
|
147
|
+
plain: text,
|
|
148
|
+
html: `<b>${escapeHTML(label)} ${escapeHTML(action)} is not managed remotely.</b>\nRun this on the host:\n<code>${escapeHTML(command)}</code>`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
renderProgress(progress, queueLength, busyState, info) {
|
|
152
|
+
return {
|
|
153
|
+
plain: renderProgressPlain(progress, queueLength, busyState, info),
|
|
154
|
+
html: renderProgressHTML(progress, queueLength, busyState, info),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
renderActivity(threadId, events, options) {
|
|
158
|
+
return renderActivityTimeline(threadId, events, options);
|
|
159
|
+
}
|
|
160
|
+
renderAudit(events) {
|
|
161
|
+
return renderAuditEvents(events);
|
|
162
|
+
}
|
|
163
|
+
renderMirrorPreference(options) {
|
|
164
|
+
if (options.cliMirrorSupported === false) {
|
|
165
|
+
const text = `CLI mirroring is not supported for ${options.agentLabel ?? "this agent"} yet.`;
|
|
166
|
+
return { plain: text, html: escapeHTML(text) };
|
|
167
|
+
}
|
|
168
|
+
const argument = options.argument.trim();
|
|
169
|
+
if (argument) {
|
|
170
|
+
const normalized = argument.toLowerCase();
|
|
171
|
+
if (!["off", "status", "final", "full"].includes(normalized)) {
|
|
172
|
+
return usageResponse("Usage: /mirror [off|status|final|full]");
|
|
173
|
+
}
|
|
174
|
+
options.preferencesStore.update(options.contextKey, {
|
|
175
|
+
mirrorMode: parseMirrorMode(argument, this.defaultMirrorMode(options.source)),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const mode = this.effectiveMirrorMode(options.source, options.contextKey, options.preferencesStore);
|
|
179
|
+
const minInterval = options.source === "telegram" ? this.config.telegramMirrorMinUpdateMs : this.config.discordMirrorMinUpdateMs;
|
|
180
|
+
return {
|
|
181
|
+
plain: [
|
|
182
|
+
`CLI mirroring: ${mode}`,
|
|
183
|
+
`Minimum update interval: ${minInterval} ms`,
|
|
184
|
+
"Modes: off, status, final, full",
|
|
185
|
+
].join("\n"),
|
|
186
|
+
html: [
|
|
187
|
+
`<b>CLI mirroring:</b> <code>${escapeHTML(mode)}</code>`,
|
|
188
|
+
`<b>Minimum update interval:</b> <code>${minInterval} ms</code>`,
|
|
189
|
+
"<b>Modes:</b> <code>off</code>, <code>status</code>, <code>final</code>, <code>full</code>",
|
|
190
|
+
].join("\n"),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
renderNotifyPreference(options) {
|
|
194
|
+
const argument = options.argument.trim();
|
|
195
|
+
if (argument) {
|
|
196
|
+
const quietMatch = argument.match(/^quiet\s+(.+)$/i);
|
|
197
|
+
if (quietMatch) {
|
|
198
|
+
try {
|
|
199
|
+
const quietHours = quietMatch[1].toLowerCase() === "off" ? null : parseQuietHours(quietMatch[1]);
|
|
200
|
+
options.preferencesStore.update(options.contextKey, { quietHours });
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const text = `Invalid quiet hours: ${friendlyErrorText(error)}`;
|
|
204
|
+
return { plain: text, html: escapeHTML(text) };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const normalized = argument.toLowerCase();
|
|
209
|
+
if (!["off", "minimal", "all"].includes(normalized)) {
|
|
210
|
+
return usageResponse("Usage: /notify [off|minimal|all] or /notify quiet HH-HH");
|
|
211
|
+
}
|
|
212
|
+
options.preferencesStore.update(options.contextKey, {
|
|
213
|
+
notifyMode: parseNotifyMode(argument, this.defaultNotifyMode(options.source)),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const mode = this.effectiveNotifyMode(options.source, options.contextKey, options.preferencesStore);
|
|
218
|
+
const quietHours = this.effectiveQuietHours(options.source, options.contextKey, options.preferencesStore);
|
|
219
|
+
return {
|
|
220
|
+
plain: [
|
|
221
|
+
`Notifications: ${mode}`,
|
|
222
|
+
`Quiet hours: ${formatQuietHours(quietHours)}`,
|
|
223
|
+
`Currently quiet: ${isQuietNow(quietHours) ? "yes" : "no"}`,
|
|
224
|
+
].join("\n"),
|
|
225
|
+
html: [
|
|
226
|
+
`<b>Notifications:</b> <code>${escapeHTML(mode)}</code>`,
|
|
227
|
+
`<b>Quiet hours:</b> <code>${escapeHTML(formatQuietHours(quietHours))}</code>`,
|
|
228
|
+
`<b>Currently quiet:</b> <code>${isQuietNow(quietHours) ? "yes" : "no"}</code>`,
|
|
229
|
+
].join("\n"),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async renderVoicePreference(options) {
|
|
233
|
+
const argument = options.argument.trim();
|
|
234
|
+
if (argument) {
|
|
235
|
+
const parts = argument.split(/\s+/);
|
|
236
|
+
const key = parts[0]?.toLowerCase();
|
|
237
|
+
const value = parts.slice(1).join(" ").trim();
|
|
238
|
+
if (key === "backend" && value) {
|
|
239
|
+
const normalized = value.toLowerCase();
|
|
240
|
+
if (!["auto", "parakeet", "faster-whisper", "openai"].includes(normalized)) {
|
|
241
|
+
return usageResponse("Usage: /voice backend auto|parakeet|faster-whisper|openai");
|
|
242
|
+
}
|
|
243
|
+
options.preferencesStore.update(options.contextKey, { voiceBackend: parseVoiceBackendPreference(value) });
|
|
244
|
+
}
|
|
245
|
+
else if (key === "language") {
|
|
246
|
+
options.preferencesStore.update(options.contextKey, { voiceLanguage: value && value.toLowerCase() !== "auto" ? value : null });
|
|
247
|
+
}
|
|
248
|
+
else if (key === "transcribe_only" || key === "transcribe-only") {
|
|
249
|
+
const enabled = parseToggle(value);
|
|
250
|
+
if (enabled === undefined) {
|
|
251
|
+
return usageResponse("Usage: /voice transcribe_only on|off");
|
|
252
|
+
}
|
|
253
|
+
options.preferencesStore.update(options.contextKey, { voiceTranscribeOnly: enabled });
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
return usageResponse("Usage: /voice, /voice backend auto|parakeet|faster-whisper|openai, /voice language auto|language-code, /voice transcribe_only on|off");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const backends = await getAvailableBackends().catch(() => []);
|
|
260
|
+
if (backends.length === 0) {
|
|
261
|
+
const plain = [
|
|
262
|
+
"Voice transcription is not available.",
|
|
263
|
+
"",
|
|
264
|
+
"Install faster-whisper + ffmpeg, install parakeet-coreml on macOS Apple Silicon, or set OPENAI_API_KEY.",
|
|
265
|
+
"Cloud transcription uses OPENAI_API_KEY, not CODEX_API_KEY.",
|
|
266
|
+
].join("\n");
|
|
267
|
+
const html = [
|
|
268
|
+
"<b>Voice transcription is not available.</b>",
|
|
269
|
+
"",
|
|
270
|
+
"Install <code>faster-whisper</code> + ffmpeg, install <code>parakeet-coreml</code> on macOS Apple Silicon, or set <code>OPENAI_API_KEY</code>.",
|
|
271
|
+
"<i>Cloud transcription uses OPENAI_API_KEY, not CODEX_API_KEY.</i>",
|
|
272
|
+
].join("\n");
|
|
273
|
+
return { plain, html };
|
|
274
|
+
}
|
|
275
|
+
const prefs = options.preferencesStore.get(options.contextKey);
|
|
276
|
+
const backendPreference = prefs.voiceBackend ?? this.config.voicePreferredBackend;
|
|
277
|
+
const language = prefs.voiceLanguage === undefined ? this.config.voiceDefaultLanguage ?? null : prefs.voiceLanguage;
|
|
278
|
+
const transcribeOnly = prefs.voiceTranscribeOnly ?? this.config.voiceTranscribeOnly;
|
|
279
|
+
const joined = backends.join(" + ");
|
|
280
|
+
return {
|
|
281
|
+
plain: [
|
|
282
|
+
`Voice backends: ${joined}`,
|
|
283
|
+
`Preferred backend: ${backendPreference}`,
|
|
284
|
+
`Language: ${language ?? "auto"}`,
|
|
285
|
+
`Transcribe only: ${transcribeOnly ? "on" : "off"}`,
|
|
286
|
+
].join("\n"),
|
|
287
|
+
html: [
|
|
288
|
+
`<b>Voice backends:</b> <code>${escapeHTML(joined)}</code>`,
|
|
289
|
+
`<b>Preferred backend:</b> <code>${escapeHTML(backendPreference)}</code>`,
|
|
290
|
+
`<b>Language:</b> <code>${escapeHTML(language ?? "auto")}</code>`,
|
|
291
|
+
`<b>Transcribe only:</b> <code>${transcribeOnly ? "on" : "off"}</code>`,
|
|
292
|
+
].join("\n"),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
renderWorkspaces(info, workspaces) {
|
|
296
|
+
const unique = [...new Set(workspaces)].filter(Boolean);
|
|
297
|
+
const rows = unique.length > 0
|
|
298
|
+
? unique.map((workspace, index) => `${index + 1}. ${workspace}${workspace === info.workspace ? " (current)" : ""}`)
|
|
299
|
+
: [`No workspaces found in ${info.agentLabel} state.`];
|
|
300
|
+
return {
|
|
301
|
+
plain: [`${info.agentLabel} workspaces:`, ...rows].join("\n"),
|
|
302
|
+
html: [
|
|
303
|
+
`<b>${escapeHTML(info.agentLabel)} workspaces:</b>`,
|
|
304
|
+
...rows.map((line) => `<code>${escapeHTML(line)}</code>`),
|
|
305
|
+
].join("\n"),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
renderHandback(result) {
|
|
309
|
+
const command = result.command ?? (result.threadId
|
|
310
|
+
? `cd ${shellEscape(result.workspace)} && codex resume ${shellEscape(result.threadId)}`
|
|
311
|
+
: "");
|
|
312
|
+
if (!result.threadId || !command) {
|
|
313
|
+
const text = "This thread has not started yet, so there is no resumable thread ID. Send a message to create one, or start a new session.";
|
|
314
|
+
return { plain: text, html: escapeHTML(text) };
|
|
315
|
+
}
|
|
316
|
+
const label = result.label ?? "Agent CLI";
|
|
317
|
+
return {
|
|
318
|
+
plain: [
|
|
319
|
+
`Thread handed back to ${label}.`,
|
|
320
|
+
"",
|
|
321
|
+
"Run this in your terminal:",
|
|
322
|
+
command,
|
|
323
|
+
"",
|
|
324
|
+
"Send any message here to start a new NordRelay thread.",
|
|
325
|
+
].join("\n"),
|
|
326
|
+
html: [
|
|
327
|
+
`<b>Thread handed back to ${escapeHTML(label)}.</b>`,
|
|
328
|
+
"",
|
|
329
|
+
"Run this in your terminal:",
|
|
330
|
+
`<pre>${escapeHTML(command)}</pre>`,
|
|
331
|
+
"",
|
|
332
|
+
"Send any message here to start a new NordRelay thread.",
|
|
333
|
+
].join("\n"),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
defaultMirrorMode(source) {
|
|
337
|
+
return source === "telegram" ? this.config.telegramMirrorMode : this.config.discordMirrorMode;
|
|
338
|
+
}
|
|
339
|
+
defaultNotifyMode(source) {
|
|
340
|
+
return source === "telegram" ? this.config.telegramNotifyMode : this.config.discordNotifyMode;
|
|
341
|
+
}
|
|
342
|
+
defaultQuietHours(source) {
|
|
343
|
+
return source === "telegram" ? this.config.telegramQuietHours : this.config.discordQuietHours;
|
|
344
|
+
}
|
|
345
|
+
effectiveMirrorMode(source, contextKey, preferencesStore) {
|
|
346
|
+
return preferencesStore.get(contextKey).mirrorMode ?? this.defaultMirrorMode(source);
|
|
347
|
+
}
|
|
348
|
+
effectiveNotifyMode(source, contextKey, preferencesStore) {
|
|
349
|
+
return preferencesStore.get(contextKey).notifyMode ?? this.defaultNotifyMode(source);
|
|
350
|
+
}
|
|
351
|
+
effectiveQuietHours(source, contextKey, preferencesStore) {
|
|
352
|
+
const prefs = preferencesStore.get(contextKey);
|
|
353
|
+
return prefs.quietHours === undefined ? this.defaultQuietHours(source) : prefs.quietHours;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
export function cliPathOptions(config) {
|
|
357
|
+
return {
|
|
358
|
+
piCliPath: config.piCliPath,
|
|
359
|
+
hermesCliPath: config.hermesCliPath,
|
|
360
|
+
openClawCliPath: config.openClawCliPath,
|
|
361
|
+
claudeCodeCliPath: config.claudeCodeCliPath,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function shellEscape(value) {
|
|
365
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
366
|
+
}
|
|
367
|
+
function usageResponse(text) {
|
|
368
|
+
return { plain: text, html: escapeHTML(text) };
|
|
369
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { channelIdForContextKey } from "./context-key.js";
|
|
2
|
+
export class ChannelMirrorRegistry {
|
|
3
|
+
config;
|
|
4
|
+
promptStore;
|
|
5
|
+
states = new Map();
|
|
6
|
+
constructor(config, promptStore) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.promptStore = promptStore;
|
|
9
|
+
}
|
|
10
|
+
activeMirrorsForThread(agentId, threadId, knownContexts, preferences) {
|
|
11
|
+
const mirrors = [];
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
for (const meta of knownContexts) {
|
|
14
|
+
const metaAgentId = meta.agentId ?? this.config.defaultAgent;
|
|
15
|
+
if (meta.threadId !== threadId || metaAgentId !== agentId) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const source = activeSessionSourceForContextKey(meta.contextKey);
|
|
19
|
+
if (!isMirrorChannelSource(source) || seen.has(meta.contextKey)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const mode = this.effectiveMirrorMode(meta.contextKey, source, preferences);
|
|
23
|
+
if (mode === "off") {
|
|
24
|
+
this.states.delete(this.stateKey(source, meta.contextKey, agentId, threadId));
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
seen.add(meta.contextKey);
|
|
28
|
+
const mirror = {
|
|
29
|
+
source,
|
|
30
|
+
contextKey: meta.contextKey,
|
|
31
|
+
mode,
|
|
32
|
+
queueLength: this.promptStore.list(meta.contextKey).length,
|
|
33
|
+
queuePaused: this.promptStore.isPaused(meta.contextKey),
|
|
34
|
+
};
|
|
35
|
+
mirrors.push(mirror);
|
|
36
|
+
this.states.set(this.stateKey(source, meta.contextKey, agentId, threadId), {
|
|
37
|
+
...mirror,
|
|
38
|
+
agentId,
|
|
39
|
+
threadId,
|
|
40
|
+
updatedAt: new Date().toISOString(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return mirrors;
|
|
44
|
+
}
|
|
45
|
+
queueLengthForExternalSource(sourceContextKey, mirrors) {
|
|
46
|
+
return mirrors.reduce((sum, mirror) => sum + mirror.queueLength, this.promptStore.list(sourceContextKey).length);
|
|
47
|
+
}
|
|
48
|
+
queuePausedForExternalSource(sourceContextKey, mirrors) {
|
|
49
|
+
return mirrors.some((mirror) => mirror.queuePaused) || this.promptStore.isPaused(sourceContextKey);
|
|
50
|
+
}
|
|
51
|
+
effectiveMirrorMode(contextKey, source, preferences) {
|
|
52
|
+
const configured = source === "telegram" ? this.config.telegramMirrorMode : this.config.discordMirrorMode;
|
|
53
|
+
return preferences.get(contextKey).mirrorMode ?? configured;
|
|
54
|
+
}
|
|
55
|
+
snapshot() {
|
|
56
|
+
return [...this.states.values()].sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
57
|
+
}
|
|
58
|
+
stateKey(source, contextKey, agentId, threadId) {
|
|
59
|
+
return `${source}:${contextKey}:${agentId}:${threadId}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function activeSessionSourceForContextKey(contextKey) {
|
|
63
|
+
const channelId = channelIdForContextKey(contextKey);
|
|
64
|
+
if (channelId === "telegram") {
|
|
65
|
+
return "telegram";
|
|
66
|
+
}
|
|
67
|
+
if (channelId === "discord") {
|
|
68
|
+
return "discord";
|
|
69
|
+
}
|
|
70
|
+
if (channelId === "web") {
|
|
71
|
+
return "web";
|
|
72
|
+
}
|
|
73
|
+
return "cli";
|
|
74
|
+
}
|
|
75
|
+
export function isMirrorChannelSource(source) {
|
|
76
|
+
return source === "telegram" || source === "discord";
|
|
77
|
+
}
|