@nordbyte/nordrelay 0.6.0 → 0.8.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 +52 -0
- package/README.md +171 -50
- package/dist/access-control.js +6 -1
- package/dist/activity-events.js +2 -2
- package/dist/adapter-conformance.js +61 -0
- package/dist/bot-preferences.js +1 -0
- package/dist/bot.js +95 -37
- package/dist/channel-adapter.js +44 -11
- package/dist/channel-command-catalog.js +94 -0
- package/dist/channel-command-core.js +60 -0
- package/dist/channel-command-service.js +230 -1
- package/dist/channel-mirror-registry.js +84 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-prompt-engine.js +177 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-lifecycle.js +73 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +82 -8
- package/dist/config.js +79 -7
- package/dist/context-key.js +42 -0
- package/dist/discord-bot.js +173 -342
- package/dist/discord-command-surface.js +11 -73
- package/dist/index.js +29 -0
- package/dist/metrics.js +48 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +288 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-readiness.js +77 -0
- package/dist/peer-runtime-service.js +658 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +307 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-runtime-helpers.js +210 -0
- package/dist/relay-runtime.js +79 -274
- package/dist/remote-prompt.js +98 -0
- package/dist/settings-wizard-test.js +216 -0
- package/dist/slack-artifacts.js +165 -0
- package/dist/slack-bot.js +1461 -0
- package/dist/slack-channel-runtime.js +147 -0
- package/dist/slack-command-surface.js +46 -0
- package/dist/slack-diagnostics.js +116 -0
- package/dist/slack-rate-limit.js +139 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-general-commands.js +14 -0
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/user-management-crypto.js +38 -0
- package/dist/user-management-normalize.js +188 -0
- package/dist/user-management-types.js +1 -0
- package/dist/user-management.js +193 -196
- package/dist/web-api-contract.js +16 -0
- package/dist/web-dashboard-access-routes.js +62 -0
- package/dist/web-dashboard-assets.js +1 -0
- package/dist/web-dashboard-pages.js +26 -4
- package/dist/web-dashboard-peer-routes.js +225 -0
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +46 -0
- package/dist/web-state.js +2 -2
- package/dist/webui-assets/dashboard.css +193 -0
- package/dist/webui-assets/dashboard.js +870 -57
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +468 -11
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { listAgentAdapterDescriptors } from "./agent-adapter.js";
|
|
2
|
+
import { agentFeatureStates } from "./agent-feature-matrix.js";
|
|
3
|
+
import { listChannelDescriptors, } from "./channel-adapter.js";
|
|
4
|
+
import { channelCatalogCommandNames, } from "./channel-command-core.js";
|
|
5
|
+
export const CHANNEL_FEATURES = [
|
|
6
|
+
{ key: "text", label: "Text", description: "Send and receive plain text prompts and replies." },
|
|
7
|
+
{ key: "streaming-edits", label: "Streaming edits", description: "Update an in-flight answer instead of sending only a final message." },
|
|
8
|
+
{ key: "typing", label: "Typing/status", description: "Show activity while an agent turn is still running." },
|
|
9
|
+
{ key: "inline-buttons", label: "Buttons", description: "Expose interactive choices for sessions, queue items, updates, artifacts, and aborts." },
|
|
10
|
+
{ key: "files", label: "Files", description: "Receive or send generic files." },
|
|
11
|
+
{ key: "photos", label: "Photos", description: "Receive image inputs for multimodal-capable agents." },
|
|
12
|
+
{ key: "voice", label: "Voice", description: "Receive audio and run transcription before prompting." },
|
|
13
|
+
{ key: "topics", label: "Threads/topics", description: "Keep independent contexts per topic, thread, forum topic, or equivalent channel scope." },
|
|
14
|
+
{ key: "webhooks", label: "Webhooks", description: "Support inbound HTTP webhook/event delivery where the platform provides it." },
|
|
15
|
+
];
|
|
16
|
+
export function channelFeatureStates(capabilities) {
|
|
17
|
+
const supported = new Set(capabilities);
|
|
18
|
+
return CHANNEL_FEATURES.map((feature) => ({
|
|
19
|
+
...feature,
|
|
20
|
+
supported: supported.has(feature.key),
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
export function buildAdapterConformanceMatrix(input = {}) {
|
|
24
|
+
const agents = input.agents ?? listAgentAdapterDescriptors();
|
|
25
|
+
const channels = input.channels ?? listChannelDescriptors();
|
|
26
|
+
return {
|
|
27
|
+
generatedAt: new Date().toISOString(),
|
|
28
|
+
agents: agents.map((adapter) => {
|
|
29
|
+
const features = agentFeatureStates(adapter.capabilities);
|
|
30
|
+
return {
|
|
31
|
+
id: adapter.id,
|
|
32
|
+
label: adapter.label,
|
|
33
|
+
status: adapter.status,
|
|
34
|
+
features,
|
|
35
|
+
supported: features.filter((feature) => feature.supported).map((feature) => feature.key),
|
|
36
|
+
unsupported: features.filter((feature) => !feature.supported).map((feature) => feature.key),
|
|
37
|
+
notes: adapter.notes,
|
|
38
|
+
};
|
|
39
|
+
}),
|
|
40
|
+
channels: channels.map((adapter) => {
|
|
41
|
+
const features = channelFeatureStates(adapter.capabilities);
|
|
42
|
+
return {
|
|
43
|
+
id: adapter.id,
|
|
44
|
+
label: adapter.label,
|
|
45
|
+
status: adapter.status,
|
|
46
|
+
enabled: adapter.enabled,
|
|
47
|
+
features,
|
|
48
|
+
supported: features.filter((feature) => feature.supported).map((feature) => feature.key),
|
|
49
|
+
unsupported: features.filter((feature) => !feature.supported).map((feature) => feature.key),
|
|
50
|
+
commands: commandNamesForChannel(adapter.id),
|
|
51
|
+
notes: adapter.notes,
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function commandNamesForChannel(id) {
|
|
57
|
+
if (id === "telegram" || id === "discord" || id === "slack") {
|
|
58
|
+
return channelCatalogCommandNames(id);
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
package/dist/bot-preferences.js
CHANGED
|
@@ -117,6 +117,7 @@ function normalizePreferences(value) {
|
|
|
117
117
|
voiceBackend: isVoiceBackendPreference(candidate.voiceBackend) ? candidate.voiceBackend : undefined,
|
|
118
118
|
voiceLanguage: typeof candidate.voiceLanguage === "string" ? candidate.voiceLanguage : candidate.voiceLanguage === null ? null : undefined,
|
|
119
119
|
voiceTranscribeOnly: typeof candidate.voiceTranscribeOnly === "boolean" ? candidate.voiceTranscribeOnly : undefined,
|
|
120
|
+
targetPeerId: typeof candidate.targetPeerId === "string" ? candidate.targetPeerId : candidate.targetPeerId === null ? null : undefined,
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
function pruneEmptyPreferences(preferences) {
|
package/dist/bot.js
CHANGED
|
@@ -12,7 +12,9 @@ import { formatSessionLabel } from "./bot-ui.js";
|
|
|
12
12
|
import { BotPreferencesStore, isQuietNow, } from "./bot-preferences.js";
|
|
13
13
|
import { renderAgentUpdateJobAction } from "./channel-actions.js";
|
|
14
14
|
import { ChannelCommandService } from "./channel-command-service.js";
|
|
15
|
+
import { runChannelPeerPrompt } from "./channel-peer-prompt.js";
|
|
15
16
|
import { deliverChannelAction } from "./channel-runtime.js";
|
|
17
|
+
import { createChannelTurnLifecycle, createChannelTypingLoop } from "./channel-turn-lifecycle.js";
|
|
16
18
|
import { agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
|
|
17
19
|
import { getExternalActivityForSession, getExternalSnapshotForSession, } from "./agent-activity.js";
|
|
18
20
|
import { checkAuthStatus, clearAuthCache, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
|
|
@@ -24,6 +26,7 @@ import { escapeHTML } from "./format.js";
|
|
|
24
26
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
25
27
|
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
|
|
26
28
|
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
29
|
+
import { RemoteRelayClient } from "./peer-client.js";
|
|
27
30
|
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
28
31
|
import { configureRedaction, redactText } from "./redaction.js";
|
|
29
32
|
import { canWriteWithLock, SessionLockStore } from "./session-locks.js";
|
|
@@ -984,6 +987,76 @@ export function createBot(config, registry) {
|
|
|
984
987
|
].join("\n");
|
|
985
988
|
await safeReply(ctx, html, { fallbackText: plain, replyMarkup: keyboard });
|
|
986
989
|
};
|
|
990
|
+
const remoteClient = new RemoteRelayClient();
|
|
991
|
+
const handleRemoteUserPrompt = async (ctx, contextKey, chatId, prompt) => {
|
|
992
|
+
const targetPeerId = preferencesStore.get(contextKey).targetPeerId ?? undefined;
|
|
993
|
+
const parsed = parseContextKey(contextKey);
|
|
994
|
+
const messageThreadId = parsed.messageThreadId;
|
|
995
|
+
return runChannelPeerPrompt({
|
|
996
|
+
targetPeerId,
|
|
997
|
+
contextKey,
|
|
998
|
+
prompt,
|
|
999
|
+
remoteClient,
|
|
1000
|
+
editMinIntervalMs: config.telegramEditMinIntervalMs,
|
|
1001
|
+
typingIntervalMs: TYPING_INTERVAL_MS,
|
|
1002
|
+
sendTyping: () => sendChatActionSafe(ctx.api, chatId, "typing", messageThreadId),
|
|
1003
|
+
sendResponse: async (text) => {
|
|
1004
|
+
const message = await sendTextMessage(ctx.api, chatId, escapeHTML(text), {
|
|
1005
|
+
fallbackText: text,
|
|
1006
|
+
messageThreadId,
|
|
1007
|
+
});
|
|
1008
|
+
return message.message_id;
|
|
1009
|
+
},
|
|
1010
|
+
editResponse: (messageId, text) => safeEditMessage(bot, chatId, messageId, escapeHTML(text), {
|
|
1011
|
+
fallbackText: text,
|
|
1012
|
+
}),
|
|
1013
|
+
sendTurnStart: (remotePrompt) => safeReply(ctx, `<b>Remote peer working on:</b>\n${escapeHTML(remotePrompt)}`, {
|
|
1014
|
+
fallbackText: `Remote peer working on:\n${remotePrompt}`,
|
|
1015
|
+
}),
|
|
1016
|
+
sendToolStart: (toolName) => safeReply(ctx, `<b>Remote tool:</b> <code>${escapeHTML(toolName)}</code>`, {
|
|
1017
|
+
fallbackText: `Remote tool: ${toolName}`,
|
|
1018
|
+
}),
|
|
1019
|
+
sendQueued: async (queueId) => {
|
|
1020
|
+
const keyboard = queueId ? new InlineKeyboard().text("Cancel queued message", `peer_queue_cancel:${targetPeerId}:${queueId}`) : undefined;
|
|
1021
|
+
await safeReply(ctx, escapeHTML(`Remote prompt queued${queueId ? `: ${queueId}` : ""}.`), {
|
|
1022
|
+
fallbackText: `Remote prompt queued${queueId ? `: ${queueId}` : ""}.`,
|
|
1023
|
+
replyMarkup: keyboard,
|
|
1024
|
+
});
|
|
1025
|
+
},
|
|
1026
|
+
sendCompleted: () => safeReply(ctx, escapeHTML("Remote turn completed."), { fallbackText: "Remote turn completed." }),
|
|
1027
|
+
sendFailure: (message) => safeReply(ctx, escapeHTML(`Remote peer failed: ${message}`), {
|
|
1028
|
+
fallbackText: `Remote peer failed: ${message}`,
|
|
1029
|
+
}),
|
|
1030
|
+
});
|
|
1031
|
+
};
|
|
1032
|
+
bot.callbackQuery(/^peer_queue_cancel:([^:]+):([a-z0-9]+)$/, async (ctx) => {
|
|
1033
|
+
const targetPeerId = ctx.match?.[1];
|
|
1034
|
+
const queueId = ctx.match?.[2];
|
|
1035
|
+
const contextKey = contextKeyFromCtx(ctx);
|
|
1036
|
+
if (!targetPeerId || !queueId || !contextKey) {
|
|
1037
|
+
await ctx.answerCallbackQuery();
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
await remoteClient.webProxy(targetPeerId, {
|
|
1042
|
+
method: "POST",
|
|
1043
|
+
path: "/api/queue",
|
|
1044
|
+
body: { action: "cancel", id: queueId },
|
|
1045
|
+
contextKey,
|
|
1046
|
+
}, telegramActivityActor(ctx), contextKey);
|
|
1047
|
+
await ctx.answerCallbackQuery({ text: `Cancelled remote queued prompt ${queueId}.` });
|
|
1048
|
+
const chatId = ctx.chat?.id;
|
|
1049
|
+
const messageId = ctx.callbackQuery.message?.message_id;
|
|
1050
|
+
if (chatId && messageId) {
|
|
1051
|
+
await safeEditMessage(bot, chatId, messageId, escapeHTML(`Cancelled remote queued prompt ${queueId}.`), {
|
|
1052
|
+
fallbackText: `Cancelled remote queued prompt ${queueId}.`,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
await ctx.answerCallbackQuery({ text: friendlyErrorText(error), show_alert: true });
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
987
1060
|
const handleUserPrompt = async (ctx, contextKey, chatId, session, prompt, options = {}) => {
|
|
988
1061
|
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
989
1062
|
return;
|
|
@@ -995,6 +1068,9 @@ export function createBot(config, registry) {
|
|
|
995
1068
|
...rawEnvelope,
|
|
996
1069
|
activityActor: rawEnvelope.activityActor ?? telegramActivityActor(ctx),
|
|
997
1070
|
};
|
|
1071
|
+
if (!options.fromQueue && await handleRemoteUserPrompt(ctx, contextKey, chatId, envelope)) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
998
1074
|
if (!options.fromQueue && await denyIfLocked(ctx, contextKey, session)) {
|
|
999
1075
|
return;
|
|
1000
1076
|
}
|
|
@@ -1043,14 +1119,8 @@ export function createBot(config, registry) {
|
|
|
1043
1119
|
}
|
|
1044
1120
|
const busyState = getBusyState(contextKey);
|
|
1045
1121
|
busyState.processing = true;
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
promptDescription: envelope.description,
|
|
1049
|
-
startedAt: Date.now(),
|
|
1050
|
-
updatedAt: Date.now(),
|
|
1051
|
-
toolCounts: new Map(),
|
|
1052
|
-
textCharacters: 0,
|
|
1053
|
-
};
|
|
1122
|
+
const turnLifecycle = createChannelTurnLifecycle(envelope.description);
|
|
1123
|
+
const progress = turnLifecycle.progress;
|
|
1054
1124
|
turnProgress.set(contextKey, progress);
|
|
1055
1125
|
const abortKeyboard = new InlineKeyboard().text("⏹ Abort", `agent_abort:${contextKey}`);
|
|
1056
1126
|
const toolVerbosity = config.toolVerbosity;
|
|
@@ -1072,12 +1142,13 @@ export function createBot(config, registry) {
|
|
|
1072
1142
|
let promptStartedAt;
|
|
1073
1143
|
const toolActivityNames = new Map();
|
|
1074
1144
|
const toolActivityStartedAt = new Map();
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1145
|
+
const typingLoop = createChannelTypingLoop({
|
|
1146
|
+
intervalMs: TYPING_INTERVAL_MS,
|
|
1147
|
+
sendTyping: () => sendChatActionSafe(bot.api, chatId, "typing", messageThreadId),
|
|
1148
|
+
});
|
|
1149
|
+
typingLoop.start();
|
|
1079
1150
|
const stopTyping = () => {
|
|
1080
|
-
|
|
1151
|
+
typingLoop.stop();
|
|
1081
1152
|
};
|
|
1082
1153
|
const clearFlushTimer = () => {
|
|
1083
1154
|
if (flushTimer) {
|
|
@@ -1228,6 +1299,7 @@ export function createBot(config, registry) {
|
|
|
1228
1299
|
return;
|
|
1229
1300
|
}
|
|
1230
1301
|
finalized = true;
|
|
1302
|
+
turnLifecycle.recordCompleted();
|
|
1231
1303
|
stopTyping();
|
|
1232
1304
|
clearFlushTimer();
|
|
1233
1305
|
if (responseMessagePromise) {
|
|
@@ -1256,8 +1328,7 @@ export function createBot(config, registry) {
|
|
|
1256
1328
|
const callbacks = {
|
|
1257
1329
|
onTextDelta: (delta) => {
|
|
1258
1330
|
accumulatedText += delta;
|
|
1259
|
-
|
|
1260
|
-
progress.updatedAt = Date.now();
|
|
1331
|
+
turnLifecycle.recordTextDelta(delta.length);
|
|
1261
1332
|
if (!responseMessageId) {
|
|
1262
1333
|
void ensureResponseMessage()
|
|
1263
1334
|
.then(() => {
|
|
@@ -1271,10 +1342,7 @@ export function createBot(config, registry) {
|
|
|
1271
1342
|
scheduleFlush();
|
|
1272
1343
|
},
|
|
1273
1344
|
onToolStart: (toolName, toolCallId) => {
|
|
1274
|
-
|
|
1275
|
-
progress.lastTool = toolName;
|
|
1276
|
-
progress.updatedAt = Date.now();
|
|
1277
|
-
progress.toolCounts.set(toolName, (progress.toolCounts.get(toolName) ?? 0) + 1);
|
|
1345
|
+
turnLifecycle.recordToolStart(toolName);
|
|
1278
1346
|
toolActivityNames.set(toolCallId, toolName);
|
|
1279
1347
|
toolActivityStartedAt.set(toolCallId, Date.now());
|
|
1280
1348
|
appendTelegramActivity(ctx, contextKey, session, {
|
|
@@ -1318,7 +1386,7 @@ export function createBot(config, registry) {
|
|
|
1318
1386
|
});
|
|
1319
1387
|
},
|
|
1320
1388
|
onToolUpdate: (toolCallId, partialResult) => {
|
|
1321
|
-
|
|
1389
|
+
turnLifecycle.recordToolUpdate();
|
|
1322
1390
|
if (toolVerbosity === "none" || toolVerbosity === "summary") {
|
|
1323
1391
|
return;
|
|
1324
1392
|
}
|
|
@@ -1329,8 +1397,7 @@ export function createBot(config, registry) {
|
|
|
1329
1397
|
state.partialResult = appendWithCap(state.partialResult, partialResult, TOOL_OUTPUT_PREVIEW_LIMIT);
|
|
1330
1398
|
},
|
|
1331
1399
|
onToolEnd: (toolCallId, isError) => {
|
|
1332
|
-
|
|
1333
|
-
progress.updatedAt = Date.now();
|
|
1400
|
+
turnLifecycle.recordToolEnd();
|
|
1334
1401
|
const activityToolName = toolActivityNames.get(toolCallId) ?? "tool";
|
|
1335
1402
|
const activityStartedAt = toolActivityStartedAt.get(toolCallId);
|
|
1336
1403
|
appendTelegramActivity(ctx, contextKey, session, {
|
|
@@ -1375,7 +1442,7 @@ export function createBot(config, registry) {
|
|
|
1375
1442
|
});
|
|
1376
1443
|
},
|
|
1377
1444
|
onTodoUpdate: (items) => {
|
|
1378
|
-
|
|
1445
|
+
turnLifecycle.touch();
|
|
1379
1446
|
if (toolVerbosity === "none") {
|
|
1380
1447
|
return;
|
|
1381
1448
|
}
|
|
@@ -1407,7 +1474,7 @@ export function createBot(config, registry) {
|
|
|
1407
1474
|
},
|
|
1408
1475
|
onTurnComplete: (usage) => {
|
|
1409
1476
|
lastTurnUsage = usage;
|
|
1410
|
-
|
|
1477
|
+
turnLifecycle.touch();
|
|
1411
1478
|
},
|
|
1412
1479
|
onAgentEnd: () => {
|
|
1413
1480
|
void finalizeResponse().catch((error) => {
|
|
@@ -1522,9 +1589,7 @@ export function createBot(config, registry) {
|
|
|
1522
1589
|
await pruneArtifacts(session.getInfo().workspace);
|
|
1523
1590
|
}
|
|
1524
1591
|
}
|
|
1525
|
-
|
|
1526
|
-
progress.completedAt = Date.now();
|
|
1527
|
-
progress.updatedAt = progress.completedAt;
|
|
1592
|
+
turnLifecycle.recordCompleted();
|
|
1528
1593
|
auditContext(ctx, contextKey, session, {
|
|
1529
1594
|
action: "prompt_completed",
|
|
1530
1595
|
status: "ok",
|
|
@@ -1539,8 +1604,7 @@ export function createBot(config, registry) {
|
|
|
1539
1604
|
});
|
|
1540
1605
|
}
|
|
1541
1606
|
catch (error) {
|
|
1542
|
-
|
|
1543
|
-
progress.error = friendlyErrorText(error);
|
|
1607
|
+
turnLifecycle.recordFailed(friendlyErrorText(error));
|
|
1544
1608
|
auditContext(ctx, contextKey, session, {
|
|
1545
1609
|
action: "prompt_failed",
|
|
1546
1610
|
status: "failed",
|
|
@@ -1557,8 +1621,6 @@ export function createBot(config, registry) {
|
|
|
1557
1621
|
durationMs: Date.now() - promptStartedAt,
|
|
1558
1622
|
});
|
|
1559
1623
|
}
|
|
1560
|
-
progress.completedAt = Date.now();
|
|
1561
|
-
progress.updatedAt = progress.completedAt;
|
|
1562
1624
|
stopTyping();
|
|
1563
1625
|
clearFlushTimer();
|
|
1564
1626
|
if (responseMessagePromise) {
|
|
@@ -1887,6 +1949,7 @@ export function createBot(config, registry) {
|
|
|
1887
1949
|
isTopicContext,
|
|
1888
1950
|
replyChannelAction,
|
|
1889
1951
|
commandService,
|
|
1952
|
+
preferencesStore,
|
|
1890
1953
|
});
|
|
1891
1954
|
registerTelegramAgentCommands({
|
|
1892
1955
|
bot,
|
|
@@ -1914,14 +1977,9 @@ export function createBot(config, registry) {
|
|
|
1914
1977
|
registerTelegramPreferenceCommands({
|
|
1915
1978
|
bot,
|
|
1916
1979
|
config,
|
|
1980
|
+
commandService,
|
|
1917
1981
|
preferencesStore,
|
|
1918
1982
|
getContextSession,
|
|
1919
|
-
getEffectiveMirrorMode,
|
|
1920
|
-
getEffectiveNotifyMode,
|
|
1921
|
-
getEffectiveQuietHours,
|
|
1922
|
-
getEffectiveVoiceBackend,
|
|
1923
|
-
getEffectiveVoiceLanguage,
|
|
1924
|
-
isVoiceTranscribeOnly,
|
|
1925
1983
|
});
|
|
1926
1984
|
registerTelegramDiagnosticsCommands({
|
|
1927
1985
|
bot,
|
package/dist/channel-adapter.js
CHANGED
|
@@ -19,6 +19,17 @@ const DISCORD_CAPABILITIES = [
|
|
|
19
19
|
"voice",
|
|
20
20
|
"topics",
|
|
21
21
|
];
|
|
22
|
+
const SLACK_CAPABILITIES = [
|
|
23
|
+
"text",
|
|
24
|
+
"streaming-edits",
|
|
25
|
+
"typing",
|
|
26
|
+
"inline-buttons",
|
|
27
|
+
"files",
|
|
28
|
+
"photos",
|
|
29
|
+
"voice",
|
|
30
|
+
"topics",
|
|
31
|
+
"webhooks",
|
|
32
|
+
];
|
|
22
33
|
const PLANNED_CHANNELS = [
|
|
23
34
|
{
|
|
24
35
|
id: "whatsapp",
|
|
@@ -27,12 +38,6 @@ const PLANNED_CHANNELS = [
|
|
|
27
38
|
status: "planned",
|
|
28
39
|
notes: "Requires a WhatsApp Business provider integration.",
|
|
29
40
|
},
|
|
30
|
-
{
|
|
31
|
-
id: "slack",
|
|
32
|
-
label: "Slack",
|
|
33
|
-
capabilities: ["text", "streaming-edits", "typing", "inline-buttons", "files"],
|
|
34
|
-
status: "planned",
|
|
35
|
-
},
|
|
36
41
|
{
|
|
37
42
|
id: "matrix",
|
|
38
43
|
label: "Matrix",
|
|
@@ -45,7 +50,8 @@ export class TelegramChannelAdapter {
|
|
|
45
50
|
label = "Telegram";
|
|
46
51
|
capabilities = new Set(TELEGRAM_CAPABILITIES);
|
|
47
52
|
describe() {
|
|
48
|
-
const
|
|
53
|
+
const requested = process.env.TELEGRAM_ENABLED !== "false";
|
|
54
|
+
const enabled = requested && Boolean(process.env.TELEGRAM_BOT_TOKEN);
|
|
49
55
|
return {
|
|
50
56
|
id: this.id,
|
|
51
57
|
label: this.label,
|
|
@@ -53,8 +59,10 @@ export class TelegramChannelAdapter {
|
|
|
53
59
|
status: "available",
|
|
54
60
|
enabled,
|
|
55
61
|
notes: enabled
|
|
56
|
-
? "Telegram bot runtime is enabled
|
|
57
|
-
:
|
|
62
|
+
? "Telegram bot runtime is enabled."
|
|
63
|
+
: requested
|
|
64
|
+
? "Telegram bot runtime is disabled because TELEGRAM_BOT_TOKEN is missing."
|
|
65
|
+
: "Telegram bot runtime is disabled.",
|
|
58
66
|
};
|
|
59
67
|
}
|
|
60
68
|
}
|
|
@@ -63,7 +71,8 @@ export class DiscordChannelAdapter {
|
|
|
63
71
|
label = "Discord";
|
|
64
72
|
capabilities = new Set(DISCORD_CAPABILITIES);
|
|
65
73
|
describe() {
|
|
66
|
-
const
|
|
74
|
+
const requested = process.env.DISCORD_ENABLED === "true";
|
|
75
|
+
const enabled = requested && Boolean(process.env.DISCORD_BOT_TOKEN);
|
|
67
76
|
return {
|
|
68
77
|
id: this.id,
|
|
69
78
|
label: this.label,
|
|
@@ -72,7 +81,30 @@ export class DiscordChannelAdapter {
|
|
|
72
81
|
enabled,
|
|
73
82
|
notes: enabled
|
|
74
83
|
? "Discord bot runtime is enabled."
|
|
75
|
-
:
|
|
84
|
+
: requested
|
|
85
|
+
? "Discord bot runtime is disabled because DISCORD_BOT_TOKEN is missing."
|
|
86
|
+
: "Enable with DISCORD_ENABLED=true and DISCORD_BOT_TOKEN.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export class SlackChannelAdapter {
|
|
91
|
+
id = "slack";
|
|
92
|
+
label = "Slack";
|
|
93
|
+
capabilities = new Set(SLACK_CAPABILITIES);
|
|
94
|
+
describe() {
|
|
95
|
+
const requested = process.env.SLACK_ENABLED === "true";
|
|
96
|
+
const enabled = requested && Boolean(process.env.SLACK_BOT_TOKEN) && Boolean(process.env.SLACK_APP_TOKEN);
|
|
97
|
+
return {
|
|
98
|
+
id: this.id,
|
|
99
|
+
label: this.label,
|
|
100
|
+
capabilities: [...this.capabilities],
|
|
101
|
+
status: "available",
|
|
102
|
+
enabled,
|
|
103
|
+
notes: enabled
|
|
104
|
+
? "Slack bot runtime is enabled."
|
|
105
|
+
: requested
|
|
106
|
+
? "Slack bot runtime is disabled because SLACK_BOT_TOKEN or SLACK_APP_TOKEN is missing."
|
|
107
|
+
: "Enable with SLACK_ENABLED=true, SLACK_BOT_TOKEN, and SLACK_APP_TOKEN.",
|
|
76
108
|
};
|
|
77
109
|
}
|
|
78
110
|
}
|
|
@@ -80,6 +112,7 @@ export function listChannelDescriptors() {
|
|
|
80
112
|
return [
|
|
81
113
|
new TelegramChannelAdapter().describe(),
|
|
82
114
|
new DiscordChannelAdapter().describe(),
|
|
115
|
+
new SlackChannelAdapter().describe(),
|
|
83
116
|
...PLANNED_CHANNELS,
|
|
84
117
|
];
|
|
85
118
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
}
|
|
89
|
+
export function slackHelpCommandList() {
|
|
90
|
+
return CHANNEL_COMMANDS
|
|
91
|
+
.filter((entry) => entry.slack !== false && !["start", "help", "prompt"].includes(entry.name))
|
|
92
|
+
.map((entry) => `/${entry.name}`)
|
|
93
|
+
.join(", ");
|
|
94
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CHANNEL_COMMANDS } from "./channel-command-catalog.js";
|
|
2
|
+
import { normalizeChannelCommandName } from "./channel-runtime.js";
|
|
3
|
+
export function createSharedChannelCommandDispatcher(input) {
|
|
4
|
+
const handlers = new Map();
|
|
5
|
+
for (const binding of input.bindings) {
|
|
6
|
+
for (const name of binding.names) {
|
|
7
|
+
const normalized = normalizeChannelCommandName(name);
|
|
8
|
+
if (!normalized) {
|
|
9
|
+
throw new Error("Channel command name is required.");
|
|
10
|
+
}
|
|
11
|
+
if (handlers.has(normalized)) {
|
|
12
|
+
throw new Error(`Duplicate ${input.transport} command binding: ${normalized}`);
|
|
13
|
+
}
|
|
14
|
+
handlers.set(normalized, binding.handler);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
transport: input.transport,
|
|
19
|
+
commandNames: [...handlers.keys()].sort(),
|
|
20
|
+
async dispatch(request, command, argument) {
|
|
21
|
+
const normalized = normalizeChannelCommandName(command);
|
|
22
|
+
const handler = handlers.get(normalized);
|
|
23
|
+
if (!handler) {
|
|
24
|
+
return { matched: false, command: normalized };
|
|
25
|
+
}
|
|
26
|
+
await handler(request, argument, normalized);
|
|
27
|
+
return { matched: true, command: normalized };
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function channelCatalogCommandNames(transport) {
|
|
32
|
+
return CHANNEL_COMMANDS
|
|
33
|
+
.filter((entry) => {
|
|
34
|
+
if (transport === "telegram")
|
|
35
|
+
return entry.telegram !== false;
|
|
36
|
+
if (transport === "discord")
|
|
37
|
+
return entry.discord !== false;
|
|
38
|
+
return entry.slack !== false;
|
|
39
|
+
})
|
|
40
|
+
.map((entry) => normalizeChannelCommandName(entry.name))
|
|
41
|
+
.sort();
|
|
42
|
+
}
|
|
43
|
+
export function channelCommandCoverage(input) {
|
|
44
|
+
const advertised = new Set(channelCatalogCommandNames(input.transport));
|
|
45
|
+
const implemented = new Set([...input.implemented].map(normalizeChannelCommandName));
|
|
46
|
+
for (const [canonical, aliases] of Object.entries(input.aliases ?? {})) {
|
|
47
|
+
if (!implemented.has(normalizeChannelCommandName(canonical)))
|
|
48
|
+
continue;
|
|
49
|
+
for (const alias of aliases) {
|
|
50
|
+
implemented.add(normalizeChannelCommandName(alias));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const aliasNames = new Set(Object.values(input.aliases ?? {}).flat().map(normalizeChannelCommandName));
|
|
54
|
+
return {
|
|
55
|
+
advertised: [...advertised].sort(),
|
|
56
|
+
implemented: [...implemented].sort(),
|
|
57
|
+
missing: [...advertised].filter((name) => !implemented.has(name)).sort(),
|
|
58
|
+
extra: [...implemented].filter((name) => !advertised.has(name) && !aliasNames.has(name)).sort(),
|
|
59
|
+
};
|
|
60
|
+
}
|