@nordbyte/nordrelay 0.5.1 → 0.6.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 +65 -11
- package/README.md +97 -23
- package/dist/access-control.js +1 -0
- package/dist/activity-events.js +44 -0
- package/dist/agent-updates.js +18 -2
- package/dist/audit-log.js +40 -2
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +492 -7
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +34 -7
- package/dist/channel-command-service.js +156 -0
- package/dist/channel-turn-service.js +237 -0
- package/dist/codex-cli.js +1 -1
- package/dist/config-metadata.js +80 -13
- package/dist/config.js +77 -7
- package/dist/context-key.js +77 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2014 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +119 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +16 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +41 -0
- package/dist/operations.js +176 -119
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime.js +1003 -268
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/state-backend.js +3 -0
- package/dist/support-bundle.js +18 -1
- 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-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +2 -6
- package/dist/telegram-operational-commands.js +14 -6
- 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 +9 -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 +97 -13
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +149 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +358 -47
- package/package.json +3 -1
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +468 -22
package/dist/config-metadata.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const SECRET_KEYS = new Set([
|
|
2
2
|
"TELEGRAM_BOT_TOKEN",
|
|
3
|
+
"DISCORD_BOT_TOKEN",
|
|
3
4
|
"CODEX_API_KEY",
|
|
4
5
|
"HERMES_API_KEY",
|
|
5
6
|
"OPENCLAW_GATEWAY_TOKEN",
|
|
@@ -7,7 +8,24 @@ export const SECRET_KEYS = new Set([
|
|
|
7
8
|
"OPENAI_API_KEY",
|
|
8
9
|
"TELEGRAM_WEBHOOK_SECRET",
|
|
9
10
|
]);
|
|
11
|
+
const DISCORD_SETTING_HELP = {
|
|
12
|
+
DISCORD_ENABLED: "Set this to true after you create the Discord application, configure the bot token, and invite the bot to the server.",
|
|
13
|
+
DISCORD_BOT_TOKEN: "Discord Developer Portal: open your application, go to Bot, then copy or reset the bot token. Store only the token value here.",
|
|
14
|
+
DISCORD_CLIENT_ID: "Discord Developer Portal: open your application, go to General Information, then copy Application ID. This is the client id used for slash commands.",
|
|
15
|
+
DISCORD_GUILD_IDS: "Enable Developer Mode in Discord, right-click a server, choose Copy Server ID, then paste one or more comma-separated ids for fast guild slash-command registration.",
|
|
16
|
+
DISCORD_ALLOWED_GUILD_IDS: "Enable Developer Mode in Discord, right-click each allowed server, choose Copy Server ID, then enter the comma-separated allow-list. Leave blank to rely on NordRelay user/group access.",
|
|
17
|
+
DISCORD_ALLOWED_CHANNEL_IDS: "Enable Developer Mode in Discord, right-click a channel or thread, choose Copy Channel ID or Copy Thread ID, then enter the comma-separated allow-list. Leave blank to rely on registered channel access.",
|
|
18
|
+
DISCORD_MESSAGE_CONTENT_ENABLED: "Turn this on only if the bot has Message Content Intent enabled in the Discord Developer Portal under Bot > Privileged Gateway Intents.",
|
|
19
|
+
DISCORD_COMMAND_MODE: "Use slash for registered slash commands only, message for text commands like /session only, or both when Message Content Intent is enabled.",
|
|
20
|
+
DISCORD_AUTO_REGISTER_COMMANDS: "When enabled, NordRelay registers slash commands at startup. Guild ids update quickly; global command registration can take longer to appear in Discord.",
|
|
21
|
+
DISCORD_CLI_MIRROR_MODE: "Overrides the channel-neutral mirror default for Discord only. Leave blank to use NORDRELAY_CLI_MIRROR_MODE.",
|
|
22
|
+
DISCORD_CLI_MIRROR_MIN_UPDATE_MS: "Discord edit/update throttle for mirrored CLI activity. Increase this if Discord rate limits streaming status updates.",
|
|
23
|
+
DISCORD_NOTIFY_MODE: "Overrides the channel-neutral completion notification default for Discord only. Leave blank to use NORDRELAY_NOTIFY_MODE.",
|
|
24
|
+
DISCORD_QUIET_HOURS: "Use a local-time range like 22-7, off, or blank to inherit the channel-neutral quiet-hours setting.",
|
|
25
|
+
DISCORD_AUTO_SEND_ARTIFACTS: "Overrides automatic artifact upload behavior for Discord only. Leave blank to use NORDRELAY_AUTO_SEND_ARTIFACTS.",
|
|
26
|
+
};
|
|
10
27
|
export const SETTING_DEFINITIONS = [
|
|
28
|
+
setting("TELEGRAM_ENABLED", "Enable Telegram", "Telegram", "boolean", "Start the Telegram bot adapter.", true),
|
|
11
29
|
setting("TELEGRAM_BOT_TOKEN", "Telegram bot token", "Telegram", "secret", "BotFather token.", true),
|
|
12
30
|
setting("TELEGRAM_TRANSPORT", "Telegram transport", "Telegram", "string", "polling or webhook.", true, ["polling", "webhook"]),
|
|
13
31
|
setting("TELEGRAM_WEBHOOK_URL", "Webhook public URL", "Telegram", "string", "Public base URL for webhook mode.", true),
|
|
@@ -15,6 +33,20 @@ export const SETTING_DEFINITIONS = [
|
|
|
15
33
|
setting("TELEGRAM_WEBHOOK_PORT", "Webhook bind port", "Telegram", "number", "Local webhook bind port.", true),
|
|
16
34
|
setting("TELEGRAM_WEBHOOK_PATH", "Webhook path", "Telegram", "string", "Webhook request path.", true),
|
|
17
35
|
setting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "Telegram", "secret", "Optional Telegram webhook secret token.", true),
|
|
36
|
+
discordSetting("DISCORD_ENABLED", "Enable Discord", "boolean", "Start the Discord bot adapter.", true),
|
|
37
|
+
discordSetting("DISCORD_BOT_TOKEN", "Discord bot token", "secret", "Discord bot token.", true),
|
|
38
|
+
discordSetting("DISCORD_CLIENT_ID", "Discord client ID", "string", "Discord application/client id used for slash command registration.", true),
|
|
39
|
+
discordSetting("DISCORD_GUILD_IDS", "Discord guild IDs", "list", "Comma-separated guild ids for instant guild slash-command registration.", true),
|
|
40
|
+
discordSetting("DISCORD_ALLOWED_GUILD_IDS", "Allowed Discord guilds", "list", "Optional comma-separated guild allow-list.", true),
|
|
41
|
+
discordSetting("DISCORD_ALLOWED_CHANNEL_IDS", "Allowed Discord channels", "list", "Optional comma-separated channel allow-list before user/group checks.", true),
|
|
42
|
+
discordSetting("DISCORD_MESSAGE_CONTENT_ENABLED", "Message content intent", "boolean", "Read regular Discord text messages as prompts. Requires enabling the privileged intent in Discord.", true),
|
|
43
|
+
discordSetting("DISCORD_COMMAND_MODE", "Discord command mode", "string", "slash, message, or both.", true, ["slash", "message", "both"]),
|
|
44
|
+
discordSetting("DISCORD_AUTO_REGISTER_COMMANDS", "Auto-register slash commands", "boolean", "Register Discord slash commands on startup when client id is configured.", true),
|
|
45
|
+
discordSetting("DISCORD_CLI_MIRROR_MODE", "Discord mirror override", "string", "Optional Discord override for CLI mirror mode. Uses the NordRelay default when unset.", false, ["off", "status", "final", "full"]),
|
|
46
|
+
discordSetting("DISCORD_CLI_MIRROR_MIN_UPDATE_MS", "Discord mirror update override", "number", "Optional Discord override for mirrored edit interval.", true),
|
|
47
|
+
discordSetting("DISCORD_NOTIFY_MODE", "Discord notify override", "string", "Optional Discord override for completion notifications.", false, ["off", "minimal", "all"]),
|
|
48
|
+
discordSetting("DISCORD_QUIET_HOURS", "Discord quiet hours override", "string", "Optional Discord quiet hours override. Use HH-HH, off, or leave blank for default.", false),
|
|
49
|
+
discordSetting("DISCORD_AUTO_SEND_ARTIFACTS", "Discord auto-send artifacts override", "boolean", "Optional Discord override for automatic artifact summaries/uploads.", false),
|
|
18
50
|
setting("NORDRELAY_CODEX_ENABLED", "Enable Codex", "Agents", "boolean", "Allow Codex sessions.", true),
|
|
19
51
|
setting("NORDRELAY_PI_ENABLED", "Enable Pi", "Agents", "boolean", "Allow Pi sessions.", true),
|
|
20
52
|
setting("NORDRELAY_HERMES_ENABLED", "Enable Hermes", "Agents", "boolean", "Allow Hermes sessions through the Hermes API Server.", true),
|
|
@@ -69,10 +101,15 @@ export const SETTING_DEFINITIONS = [
|
|
|
69
101
|
setting("ENABLE_TELEGRAM_REACTIONS", "Enable Telegram reactions", "Operations", "boolean", "Send Telegram reactions.", true),
|
|
70
102
|
setting("TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS", "Telegram send interval", "Operations", "number", "Minimum send interval.", true),
|
|
71
103
|
setting("TELEGRAM_EDIT_MIN_INTERVAL_MS", "Telegram edit interval", "Operations", "number", "Minimum edit interval.", true),
|
|
72
|
-
setting("
|
|
73
|
-
setting("
|
|
74
|
-
setting("
|
|
75
|
-
setting("
|
|
104
|
+
setting("NORDRELAY_CLI_MIRROR_MODE", "Default CLI mirror mode", "Operations", "string", "Default mirror mode for chat adapters: off, status, final, or full.", false, ["off", "status", "final", "full"]),
|
|
105
|
+
setting("NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS", "Default mirror update interval", "Operations", "number", "Default minimum mirrored edit interval.", true),
|
|
106
|
+
setting("NORDRELAY_NOTIFY_MODE", "Default notify mode", "Operations", "string", "Default completion notifications: off, minimal, or all.", false, ["off", "minimal", "all"]),
|
|
107
|
+
setting("NORDRELAY_QUIET_HOURS", "Default quiet hours", "Operations", "string", "Default quiet hours. Use HH-HH, off, or leave blank.", false),
|
|
108
|
+
setting("NORDRELAY_AUTO_SEND_ARTIFACTS", "Default auto-send artifacts", "Operations", "boolean", "Default automatic artifact summaries/uploads for chat adapters.", false),
|
|
109
|
+
setting("TELEGRAM_CLI_MIRROR_MODE", "Telegram mirror override", "Operations", "string", "Optional Telegram override for CLI mirror mode. Uses the NordRelay default when unset.", false, ["off", "status", "final", "full"]),
|
|
110
|
+
setting("TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS", "Telegram mirror update override", "Operations", "number", "Optional Telegram override for mirrored edit interval.", true),
|
|
111
|
+
setting("TELEGRAM_NOTIFY_MODE", "Telegram notify override", "Operations", "string", "Optional Telegram override for completion notifications.", false, ["off", "minimal", "all"]),
|
|
112
|
+
setting("TELEGRAM_QUIET_HOURS", "Telegram quiet hours override", "Operations", "string", "Optional Telegram quiet hours override. Use HH-HH, off, or leave blank for default.", false),
|
|
76
113
|
setting("TELEGRAM_REDACT_PATTERNS", "Redaction patterns", "Operations", "list", "Additional comma-separated regex patterns.", true),
|
|
77
114
|
setting("NORDRELAY_UPDATE_METHOD", "Update method", "Operations", "string", "auto, npm, or git.", true, ["auto", "npm", "git"]),
|
|
78
115
|
setting("MAX_FILE_SIZE", "Max file size", "Artifacts", "number", "Max inbound/outbound file size.", true),
|
|
@@ -81,13 +118,16 @@ export const SETTING_DEFINITIONS = [
|
|
|
81
118
|
setting("ARTIFACT_MAX_INBOX_DIRS", "Max inbox dirs", "Artifacts", "number", "Maximum inbox dirs retained.", true),
|
|
82
119
|
setting("ARTIFACT_IGNORE_DIRS", "Artifact ignore dirs", "Artifacts", "list", "Extra ignored dirs or relative paths.", true),
|
|
83
120
|
setting("ARTIFACT_IGNORE_GLOBS", "Artifact ignore globs", "Artifacts", "list", "Extra ignored glob patterns.", true),
|
|
84
|
-
setting("TELEGRAM_AUTO_SEND_ARTIFACTS", "
|
|
121
|
+
setting("TELEGRAM_AUTO_SEND_ARTIFACTS", "Telegram auto-send artifacts override", "Artifacts", "boolean", "Optional Telegram override for automatic artifact summaries/uploads.", false),
|
|
85
122
|
setting("WORKSPACE_ALLOWED_ROOTS", "Workspace allowed roots", "Workspace", "list", "Restrict selectable workspaces.", true),
|
|
86
123
|
setting("WORKSPACE_WARN_ROOTS", "Workspace warn roots", "Workspace", "list", "Warn for broad workspace roots.", true),
|
|
87
124
|
setting("NORDRELAY_STATE_BACKEND", "State backend", "Workspace", "string", "json or sqlite.", true, ["json", "sqlite"]),
|
|
88
125
|
setting("NORDRELAY_AUDIT_MAX_EVENTS", "Audit max events", "Workspace", "number", "Retained audit events.", true),
|
|
89
126
|
setting("NORDRELAY_SESSION_LOCK_TTL_MS", "Session lock TTL", "Workspace", "number", "Write-lock TTL.", true),
|
|
127
|
+
setting("NORDRELAY_DASHBOARD_CACHE_TTL_MS", "Dashboard cache TTL", "Workspace", "number", "Stale-while-refresh TTL for expensive dashboard API snapshots.", true),
|
|
128
|
+
setting("NORDRELAY_UNIFIED_JOB_MAX_ITEMS", "Unified job history", "Workspace", "number", "Maximum persisted unified jobs retained for the WebUI jobs view.", true),
|
|
90
129
|
setting("NORDRELAY_VERSION_CACHE_TTL_MS", "Version cache TTL", "Workspace", "number", "NPM version cache TTL.", true),
|
|
130
|
+
setting("NORDRELAY_CLI_VERSION_CACHE_TTL_MS", "CLI version cache TTL", "Workspace", "number", "Installed agent CLI version cache TTL.", true),
|
|
91
131
|
setting("OPENAI_API_KEY", "OpenAI API key", "Voice", "secret", "Whisper fallback API key.", true),
|
|
92
132
|
setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
|
|
93
133
|
setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
|
|
@@ -103,7 +143,22 @@ export const SETTING_DEFINITIONS = [
|
|
|
103
143
|
setting("NORDRELAY_ENV_FILE", "Env file path", "Dashboard", "string", "Optional explicit env-file path used by the CLI wrapper and dashboard.", true),
|
|
104
144
|
];
|
|
105
145
|
const EXAMPLE_VALUES = {
|
|
146
|
+
"TELEGRAM_ENABLED": "true",
|
|
106
147
|
"TELEGRAM_BOT_TOKEN": "123456789:replace-me",
|
|
148
|
+
"DISCORD_ENABLED": "false",
|
|
149
|
+
"DISCORD_BOT_TOKEN": "",
|
|
150
|
+
"DISCORD_CLIENT_ID": "",
|
|
151
|
+
"DISCORD_GUILD_IDS": "",
|
|
152
|
+
"DISCORD_ALLOWED_GUILD_IDS": "",
|
|
153
|
+
"DISCORD_ALLOWED_CHANNEL_IDS": "",
|
|
154
|
+
"DISCORD_MESSAGE_CONTENT_ENABLED": "true",
|
|
155
|
+
"DISCORD_COMMAND_MODE": "both",
|
|
156
|
+
"DISCORD_AUTO_REGISTER_COMMANDS": "true",
|
|
157
|
+
"DISCORD_CLI_MIRROR_MODE": "",
|
|
158
|
+
"DISCORD_CLI_MIRROR_MIN_UPDATE_MS": "",
|
|
159
|
+
"DISCORD_NOTIFY_MODE": "",
|
|
160
|
+
"DISCORD_QUIET_HOURS": "",
|
|
161
|
+
"DISCORD_AUTO_SEND_ARTIFACTS": "",
|
|
107
162
|
"NORDRELAY_CODEX_ENABLED": "true",
|
|
108
163
|
"NORDRELAY_PI_ENABLED": "false",
|
|
109
164
|
"NORDRELAY_HERMES_ENABLED": "false",
|
|
@@ -164,9 +219,14 @@ const EXAMPLE_VALUES = {
|
|
|
164
219
|
"TELEGRAM_WEBHOOK_PORT": "8080",
|
|
165
220
|
"TELEGRAM_WEBHOOK_PATH": "/telegram/webhook",
|
|
166
221
|
"TELEGRAM_WEBHOOK_SECRET": "",
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
222
|
+
"NORDRELAY_CLI_MIRROR_MODE": "status",
|
|
223
|
+
"NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS": "4000",
|
|
224
|
+
"NORDRELAY_NOTIFY_MODE": "minimal",
|
|
225
|
+
"NORDRELAY_QUIET_HOURS": "",
|
|
226
|
+
"NORDRELAY_AUTO_SEND_ARTIFACTS": "false",
|
|
227
|
+
"TELEGRAM_CLI_MIRROR_MODE": "",
|
|
228
|
+
"TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS": "",
|
|
229
|
+
"TELEGRAM_NOTIFY_MODE": "",
|
|
170
230
|
"TELEGRAM_QUIET_HOURS": "",
|
|
171
231
|
"TELEGRAM_REDACT_PATTERNS": "",
|
|
172
232
|
"MAX_FILE_SIZE": "20971520",
|
|
@@ -175,11 +235,14 @@ const EXAMPLE_VALUES = {
|
|
|
175
235
|
"ARTIFACT_MAX_INBOX_DIRS": "30",
|
|
176
236
|
"ARTIFACT_IGNORE_DIRS": "",
|
|
177
237
|
"ARTIFACT_IGNORE_GLOBS": "",
|
|
178
|
-
"TELEGRAM_AUTO_SEND_ARTIFACTS": "
|
|
238
|
+
"TELEGRAM_AUTO_SEND_ARTIFACTS": "",
|
|
179
239
|
"NORDRELAY_STATE_BACKEND": "json",
|
|
180
240
|
"NORDRELAY_AUDIT_MAX_EVENTS": "1000",
|
|
181
241
|
"NORDRELAY_SESSION_LOCK_TTL_MS": "1800000",
|
|
242
|
+
"NORDRELAY_DASHBOARD_CACHE_TTL_MS": "10000",
|
|
243
|
+
"NORDRELAY_UNIFIED_JOB_MAX_ITEMS": "1000",
|
|
182
244
|
"NORDRELAY_VERSION_CACHE_TTL_MS": "3600000",
|
|
245
|
+
"NORDRELAY_CLI_VERSION_CACHE_TTL_MS": "60000",
|
|
183
246
|
"NORDRELAY_DASHBOARD_HOST": "127.0.0.1",
|
|
184
247
|
"NORDRELAY_DASHBOARD_PORT": "31878",
|
|
185
248
|
"NORDRELAY_ENV_FILE": "",
|
|
@@ -197,7 +260,8 @@ const EXAMPLE_VALUES = {
|
|
|
197
260
|
"FASTER_WHISPER_TIMEOUT_MS": "600000",
|
|
198
261
|
};
|
|
199
262
|
const GROUP_INTROS = {
|
|
200
|
-
Telegram: "
|
|
263
|
+
Telegram: "Telegram bot and transport settings.",
|
|
264
|
+
Discord: "Discord bot settings. Discord is opt-in and uses the same NordRelay users, groups, and permissions as Telegram.",
|
|
201
265
|
Agents: "Agent access. Codex is enabled by default; Pi, Hermes, OpenClaw, and Claude Code are opt-in.",
|
|
202
266
|
Codex: "Codex defaults for newly created or reattached sessions.",
|
|
203
267
|
Pi: "Pi coding agent defaults.",
|
|
@@ -216,7 +280,7 @@ export function envExampleValue(key) {
|
|
|
216
280
|
export function renderEnvExample() {
|
|
217
281
|
const lines = [
|
|
218
282
|
"# NordRelay runtime config example.",
|
|
219
|
-
"# Access is managed with NordRelay users, groups, linked
|
|
283
|
+
"# Access is managed with NordRelay users, groups, linked chat identities, and enabled group/guild channels.",
|
|
220
284
|
"# Create the first admin with `nordrelay init` or `nordrelay user create-admin`.",
|
|
221
285
|
];
|
|
222
286
|
let currentGroup = "";
|
|
@@ -233,6 +297,9 @@ export function renderEnvExample() {
|
|
|
233
297
|
}
|
|
234
298
|
return `${lines.join('\n')}\n`;
|
|
235
299
|
}
|
|
236
|
-
function setting(key, label, group, kind, description, restartRequired, options) {
|
|
237
|
-
return { key, label, group, kind, description, restartRequired, options };
|
|
300
|
+
function setting(key, label, group, kind, description, restartRequired, options, help) {
|
|
301
|
+
return { key, label, group, kind, description, help, restartRequired, options };
|
|
302
|
+
}
|
|
303
|
+
function discordSetting(key, label, kind, description, restartRequired, options) {
|
|
304
|
+
return setting(key, label, "Discord", kind, description, restartRequired, options, DISCORD_SETTING_HELP[key]);
|
|
238
305
|
}
|
package/dist/config.js
CHANGED
|
@@ -5,13 +5,19 @@ import { CLAUDE_CODE_EFFORT_LEVELS, HERMES_REASONING_EFFORTS, OPENCLAW_THINKING_
|
|
|
5
5
|
import { parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
|
|
6
6
|
export function loadConfig() {
|
|
7
7
|
loadEnvFile(path.resolve(process.cwd(), ".env"));
|
|
8
|
-
const
|
|
8
|
+
const telegramEnabled = parseBooleanEnv(optionalString(process.env.TELEGRAM_ENABLED), true);
|
|
9
|
+
const telegramBotToken = telegramEnabled ? requireEnv("TELEGRAM_BOT_TOKEN") : "";
|
|
9
10
|
const telegramRateLimitMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS), 80, "TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS");
|
|
10
11
|
const telegramEditMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_EDIT_MIN_INTERVAL_MS), 1_200, "TELEGRAM_EDIT_MIN_INTERVAL_MS");
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
12
|
+
const mirrorMode = parseMirrorMode(optionalString(process.env.NORDRELAY_CLI_MIRROR_MODE), "status");
|
|
13
|
+
const mirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS), 4_000, "NORDRELAY_CLI_MIRROR_MIN_UPDATE_MS");
|
|
14
|
+
const notifyMode = parseNotifyMode(optionalString(process.env.NORDRELAY_NOTIFY_MODE), "minimal");
|
|
15
|
+
const quietHours = parseQuietHoursOverride(process.env.NORDRELAY_QUIET_HOURS, null);
|
|
16
|
+
const autoSendArtifacts = parseBooleanEnv(optionalString(process.env.NORDRELAY_AUTO_SEND_ARTIFACTS), false);
|
|
17
|
+
const telegramMirrorMode = parseMirrorMode(optionalString(process.env.TELEGRAM_CLI_MIRROR_MODE), mirrorMode);
|
|
18
|
+
const telegramMirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS), mirrorMinUpdateMs, "TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS");
|
|
19
|
+
const telegramNotifyMode = parseNotifyMode(optionalString(process.env.TELEGRAM_NOTIFY_MODE), notifyMode);
|
|
20
|
+
const telegramQuietHours = parseQuietHoursOverride(process.env.TELEGRAM_QUIET_HOURS, quietHours);
|
|
15
21
|
const telegramRedactPatterns = parseOptionalStringList(optionalString(process.env.TELEGRAM_REDACT_PATTERNS));
|
|
16
22
|
const telegramTransport = parseTelegramTransport(optionalString(process.env.TELEGRAM_TRANSPORT));
|
|
17
23
|
const telegramWebhookUrl = optionalString(process.env.TELEGRAM_WEBHOOK_URL);
|
|
@@ -19,6 +25,19 @@ export function loadConfig() {
|
|
|
19
25
|
const telegramWebhookPort = parsePositiveIntegerEnv(optionalString(process.env.TELEGRAM_WEBHOOK_PORT), 8080, "TELEGRAM_WEBHOOK_PORT");
|
|
20
26
|
const telegramWebhookPath = parseWebhookPath(optionalString(process.env.TELEGRAM_WEBHOOK_PATH));
|
|
21
27
|
const telegramWebhookSecret = optionalString(process.env.TELEGRAM_WEBHOOK_SECRET);
|
|
28
|
+
const discordEnabled = parseBooleanEnv(optionalString(process.env.DISCORD_ENABLED), false);
|
|
29
|
+
const discordBotToken = optionalString(process.env.DISCORD_BOT_TOKEN);
|
|
30
|
+
const discordClientId = optionalString(process.env.DISCORD_CLIENT_ID);
|
|
31
|
+
const discordGuildIds = parseOptionalStringList(optionalString(process.env.DISCORD_GUILD_IDS));
|
|
32
|
+
const discordAllowedGuildIds = parseOptionalStringList(optionalString(process.env.DISCORD_ALLOWED_GUILD_IDS));
|
|
33
|
+
const discordAllowedChannelIds = parseOptionalStringList(optionalString(process.env.DISCORD_ALLOWED_CHANNEL_IDS));
|
|
34
|
+
const discordMessageContentEnabled = parseBooleanEnv(optionalString(process.env.DISCORD_MESSAGE_CONTENT_ENABLED), true);
|
|
35
|
+
const discordCommandMode = parseDiscordCommandMode(optionalString(process.env.DISCORD_COMMAND_MODE));
|
|
36
|
+
const discordAutoRegisterCommands = parseBooleanEnv(optionalString(process.env.DISCORD_AUTO_REGISTER_COMMANDS), true);
|
|
37
|
+
const discordMirrorMode = parseMirrorMode(optionalString(process.env.DISCORD_CLI_MIRROR_MODE), mirrorMode);
|
|
38
|
+
const discordMirrorMinUpdateMs = parseNonNegativeIntegerEnv(optionalString(process.env.DISCORD_CLI_MIRROR_MIN_UPDATE_MS), mirrorMinUpdateMs, "DISCORD_CLI_MIRROR_MIN_UPDATE_MS");
|
|
39
|
+
const discordNotifyMode = parseNotifyMode(optionalString(process.env.DISCORD_NOTIFY_MODE), notifyMode);
|
|
40
|
+
const discordQuietHours = parseQuietHoursOverride(process.env.DISCORD_QUIET_HOURS, quietHours);
|
|
22
41
|
const workspace = resolveWorkspace();
|
|
23
42
|
const workspaceAllowedRoots = parsePathList(optionalString(process.env.WORKSPACE_ALLOWED_ROOTS));
|
|
24
43
|
const workspaceWarnRoots = parsePathList(optionalString(process.env.WORKSPACE_WARN_ROOTS));
|
|
@@ -29,7 +48,8 @@ export function loadConfig() {
|
|
|
29
48
|
const artifactMaxInboxDirs = parsePositiveIntegerEnv(optionalString(process.env.ARTIFACT_MAX_INBOX_DIRS), 30, "ARTIFACT_MAX_INBOX_DIRS");
|
|
30
49
|
const artifactIgnoreDirs = parseOptionalStringList(optionalString(process.env.ARTIFACT_IGNORE_DIRS));
|
|
31
50
|
const artifactIgnoreGlobs = parseOptionalStringList(optionalString(process.env.ARTIFACT_IGNORE_GLOBS));
|
|
32
|
-
const telegramAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.TELEGRAM_AUTO_SEND_ARTIFACTS),
|
|
51
|
+
const telegramAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.TELEGRAM_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
|
|
52
|
+
const discordAutoSendArtifacts = parseBooleanEnv(optionalString(process.env.DISCORD_AUTO_SEND_ARTIFACTS), autoSendArtifacts);
|
|
33
53
|
const codexEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_CODEX_ENABLED), true);
|
|
34
54
|
const codexApiKey = optionalString(process.env.CODEX_API_KEY);
|
|
35
55
|
const codexModel = optionalString(process.env.CODEX_MODEL);
|
|
@@ -86,13 +106,27 @@ export function loadConfig() {
|
|
|
86
106
|
const voiceTranscribeOnly = parseBooleanEnv(optionalString(process.env.VOICE_TRANSCRIBE_ONLY), false);
|
|
87
107
|
const auditMaxEvents = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_AUDIT_MAX_EVENTS), 1000, "NORDRELAY_AUDIT_MAX_EVENTS");
|
|
88
108
|
const sessionLockTtlMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_SESSION_LOCK_TTL_MS), 30 * 60 * 1000, "NORDRELAY_SESSION_LOCK_TTL_MS");
|
|
89
|
-
|
|
109
|
+
const dashboardCacheTtlMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_DASHBOARD_CACHE_TTL_MS), 10_000, "NORDRELAY_DASHBOARD_CACHE_TTL_MS");
|
|
110
|
+
const unifiedJobMaxItems = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_UNIFIED_JOB_MAX_ITEMS), 1000, "NORDRELAY_UNIFIED_JOB_MAX_ITEMS");
|
|
111
|
+
if (telegramEnabled && telegramTransport === "webhook" && !telegramWebhookUrl) {
|
|
90
112
|
throw new Error("TELEGRAM_TRANSPORT=webhook requires TELEGRAM_WEBHOOK_URL");
|
|
91
113
|
}
|
|
114
|
+
if (discordEnabled && !discordBotToken) {
|
|
115
|
+
throw new Error("DISCORD_ENABLED=true requires DISCORD_BOT_TOKEN");
|
|
116
|
+
}
|
|
117
|
+
if (!telegramEnabled && !discordEnabled) {
|
|
118
|
+
throw new Error("At least one chat adapter must be enabled.");
|
|
119
|
+
}
|
|
92
120
|
return {
|
|
121
|
+
telegramEnabled,
|
|
93
122
|
telegramBotToken,
|
|
94
123
|
telegramRateLimitMinIntervalMs,
|
|
95
124
|
telegramEditMinIntervalMs,
|
|
125
|
+
mirrorMode,
|
|
126
|
+
mirrorMinUpdateMs,
|
|
127
|
+
notifyMode,
|
|
128
|
+
quietHours,
|
|
129
|
+
autoSendArtifacts,
|
|
96
130
|
telegramMirrorMode,
|
|
97
131
|
telegramMirrorMinUpdateMs,
|
|
98
132
|
telegramNotifyMode,
|
|
@@ -104,6 +138,20 @@ export function loadConfig() {
|
|
|
104
138
|
telegramWebhookPort,
|
|
105
139
|
telegramWebhookPath,
|
|
106
140
|
telegramWebhookSecret,
|
|
141
|
+
discordEnabled,
|
|
142
|
+
discordBotToken,
|
|
143
|
+
discordClientId,
|
|
144
|
+
discordGuildIds,
|
|
145
|
+
discordAllowedGuildIds,
|
|
146
|
+
discordAllowedChannelIds,
|
|
147
|
+
discordMessageContentEnabled,
|
|
148
|
+
discordCommandMode,
|
|
149
|
+
discordAutoRegisterCommands,
|
|
150
|
+
discordMirrorMode,
|
|
151
|
+
discordMirrorMinUpdateMs,
|
|
152
|
+
discordNotifyMode,
|
|
153
|
+
discordQuietHours,
|
|
154
|
+
discordAutoSendArtifacts,
|
|
107
155
|
workspace,
|
|
108
156
|
workspaceAllowedRoots,
|
|
109
157
|
workspaceWarnRoots,
|
|
@@ -170,6 +218,8 @@ export function loadConfig() {
|
|
|
170
218
|
voiceTranscribeOnly,
|
|
171
219
|
auditMaxEvents,
|
|
172
220
|
sessionLockTtlMs,
|
|
221
|
+
dashboardCacheTtlMs,
|
|
222
|
+
unifiedJobMaxItems,
|
|
173
223
|
};
|
|
174
224
|
}
|
|
175
225
|
/**
|
|
@@ -224,6 +274,16 @@ function optionalString(value) {
|
|
|
224
274
|
const trimmed = value?.trim();
|
|
225
275
|
return trimmed ? trimmed : undefined;
|
|
226
276
|
}
|
|
277
|
+
function parseQuietHoursOverride(value, fallback) {
|
|
278
|
+
const normalized = value?.trim().toLowerCase();
|
|
279
|
+
if (!normalized) {
|
|
280
|
+
return fallback;
|
|
281
|
+
}
|
|
282
|
+
if (normalized === "off" || normalized === "none" || normalized === "false" || normalized === "0") {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return parseQuietHours(normalized);
|
|
286
|
+
}
|
|
227
287
|
function parseOptionalStringList(raw) {
|
|
228
288
|
if (!raw) {
|
|
229
289
|
return [];
|
|
@@ -342,6 +402,16 @@ function parseTelegramTransport(raw) {
|
|
|
342
402
|
console.warn(`Invalid TELEGRAM_TRANSPORT value: "${raw}". Expected polling or webhook. Falling back to polling.`);
|
|
343
403
|
return "polling";
|
|
344
404
|
}
|
|
405
|
+
function parseDiscordCommandMode(raw) {
|
|
406
|
+
if (!raw) {
|
|
407
|
+
return "both";
|
|
408
|
+
}
|
|
409
|
+
if (raw === "slash" || raw === "message" || raw === "both") {
|
|
410
|
+
return raw;
|
|
411
|
+
}
|
|
412
|
+
console.warn(`Invalid DISCORD_COMMAND_MODE value: "${raw}". Expected slash, message, or both. Falling back to both.`);
|
|
413
|
+
return "both";
|
|
414
|
+
}
|
|
345
415
|
function parseWebhookPath(raw) {
|
|
346
416
|
if (!raw) {
|
|
347
417
|
return "/telegram/webhook";
|
package/dist/context-key.js
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function telegramContextKeyFromMessage(chatId, messageThreadId) {
|
|
2
2
|
if (messageThreadId !== undefined) {
|
|
3
3
|
return `${chatId}:${messageThreadId}`;
|
|
4
4
|
}
|
|
5
5
|
return `${chatId}`;
|
|
6
6
|
}
|
|
7
|
-
export function
|
|
7
|
+
export function telegramContextKeyFromCtx(ctx) {
|
|
8
8
|
const chatId = ctx.chat?.id;
|
|
9
9
|
if (chatId === undefined) {
|
|
10
10
|
return null;
|
|
11
11
|
}
|
|
12
12
|
const threadId = ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id;
|
|
13
|
-
return
|
|
13
|
+
return telegramContextKeyFromMessage(chatId, threadId);
|
|
14
14
|
}
|
|
15
|
-
export function
|
|
15
|
+
export function parseTelegramContextKey(key) {
|
|
16
16
|
const parts = key.split(":");
|
|
17
17
|
const chatId = Number(parts[0]);
|
|
18
18
|
const messageThreadId = parts[1] ? Number(parts[1]) : undefined;
|
|
19
19
|
return { chatId, messageThreadId };
|
|
20
20
|
}
|
|
21
|
+
export function contextKeyFromMessage(chatId, messageThreadId) {
|
|
22
|
+
return telegramContextKeyFromMessage(chatId, messageThreadId);
|
|
23
|
+
}
|
|
24
|
+
export function contextKeyFromCtx(ctx) {
|
|
25
|
+
return telegramContextKeyFromCtx(ctx);
|
|
26
|
+
}
|
|
27
|
+
export function parseContextKey(key) {
|
|
28
|
+
return parseTelegramContextKey(key);
|
|
29
|
+
}
|
|
21
30
|
export function isTopicContextKey(key) {
|
|
22
|
-
|
|
31
|
+
const parsed = parseChannelContextKey(key);
|
|
32
|
+
return Boolean(parsed?.topicId);
|
|
23
33
|
}
|
|
24
34
|
export function isTelegramContextKey(key) {
|
|
25
35
|
const parts = key.split(":");
|
|
@@ -44,3 +54,65 @@ export function isTelegramContextKey(key) {
|
|
|
44
54
|
const threadId = Number(threadIdText);
|
|
45
55
|
return Number.isSafeInteger(threadId) && threadId > 0;
|
|
46
56
|
}
|
|
57
|
+
export function discordContextKey(input) {
|
|
58
|
+
const guildId = input.guildId || "dm";
|
|
59
|
+
const topic = input.threadId && input.threadId !== input.channelId ? `:${input.threadId}` : "";
|
|
60
|
+
return `discord:${guildId}:${input.channelId}${topic}`;
|
|
61
|
+
}
|
|
62
|
+
export function isDiscordContextKey(key) {
|
|
63
|
+
return /^discord:[^:]+:[^:]+(?::[^:]+)?$/.test(key);
|
|
64
|
+
}
|
|
65
|
+
export function parseDiscordContextKey(key) {
|
|
66
|
+
if (!isDiscordContextKey(key)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const [, guild, channelId, threadId] = key.split(":");
|
|
70
|
+
if (!channelId) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
guildId: guild === "dm" ? undefined : guild,
|
|
75
|
+
channelId,
|
|
76
|
+
threadId,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function parseChannelContextKey(key) {
|
|
80
|
+
const rawKey = String(key);
|
|
81
|
+
if (isTelegramContextKey(rawKey)) {
|
|
82
|
+
const parsed = parseTelegramContextKey(rawKey);
|
|
83
|
+
return {
|
|
84
|
+
channelId: "telegram",
|
|
85
|
+
contextKey: rawKey,
|
|
86
|
+
chatId: String(parsed.chatId),
|
|
87
|
+
topicId: parsed.messageThreadId === undefined ? undefined : String(parsed.messageThreadId),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const discord = parseDiscordContextKey(rawKey);
|
|
91
|
+
if (discord) {
|
|
92
|
+
return {
|
|
93
|
+
channelId: "discord",
|
|
94
|
+
contextKey: rawKey,
|
|
95
|
+
chatId: discord.channelId,
|
|
96
|
+
topicId: discord.threadId,
|
|
97
|
+
guildId: discord.guildId,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (rawKey.startsWith("web:")) {
|
|
101
|
+
return {
|
|
102
|
+
channelId: "web",
|
|
103
|
+
contextKey: rawKey,
|
|
104
|
+
chatId: rawKey.slice("web:".length) || "dashboard",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (rawKey.startsWith("cli:")) {
|
|
108
|
+
return {
|
|
109
|
+
channelId: "cli",
|
|
110
|
+
contextKey: rawKey,
|
|
111
|
+
chatId: rawKey.slice("cli:".length) || "local",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
export function channelIdForContextKey(key) {
|
|
117
|
+
return parseChannelContextKey(key)?.channelId ?? "cli";
|
|
118
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { collectRecentWorkspaceArtifacts, createArtifactZipBundle, formatArtifactSummary, listRecentArtifactReports } from "./artifacts.js";
|
|
2
|
+
import { filterArtifactReports as filterArtifactReportsForCommand } from "./bot-rendering.js";
|
|
3
|
+
import { renderArtifactReportsAction } from "./channel-actions.js";
|
|
4
|
+
import { deliverChannelAction } from "./channel-runtime.js";
|
|
5
|
+
export function createDiscordArtifactCommandHandler(deps) {
|
|
6
|
+
return async (request, argument) => {
|
|
7
|
+
const session = await deps.getSession(request, { deferThreadStart: true });
|
|
8
|
+
const [action, turnId] = argument.trim().split(/\s+/, 2);
|
|
9
|
+
const info = session.getInfo();
|
|
10
|
+
const workspace = info.workspace;
|
|
11
|
+
const reports = await listRecentArtifactReports(workspace, 10, deps.config.maxFileSize);
|
|
12
|
+
if (reports.length === 0) {
|
|
13
|
+
await deps.reply(request, "No generated artifacts found for this workspace.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (action) {
|
|
17
|
+
if (action.toLowerCase() === "delete" && turnId) {
|
|
18
|
+
const selected = findArtifactReport(reports, turnId);
|
|
19
|
+
if (!selected) {
|
|
20
|
+
await deps.reply(request, `No artifact turn found for "${turnId}".`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const removed = await deps.artifactService.delete(workspace, selected.turnId);
|
|
24
|
+
deps.appendActivity(request, {
|
|
25
|
+
status: removed ? "info" : "failed",
|
|
26
|
+
type: "artifact_deleted",
|
|
27
|
+
threadId: info.threadId,
|
|
28
|
+
workspace,
|
|
29
|
+
agentId: info.agentId,
|
|
30
|
+
detail: selected.turnId,
|
|
31
|
+
});
|
|
32
|
+
await deps.reply(request, removed ? `Deleted artifact turn: ${selected.turnId}` : `Artifact turn not found: ${selected.turnId}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const filtered = filterArtifactReportsForCommand(reports, argument);
|
|
36
|
+
if (filtered) {
|
|
37
|
+
if (filtered.length === 0) {
|
|
38
|
+
await deps.reply(request, `No artifacts matched "${argument}".`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
await deliverChannelAction(deps.runtime, request.context, renderDiscordArtifactReports(request.contextKey, filtered));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const normalizedAction = action.toLowerCase();
|
|
45
|
+
const shouldZip = normalizedAction === "zip";
|
|
46
|
+
const shouldSend = normalizedAction === "send";
|
|
47
|
+
const selected = findArtifactReport(reports, shouldZip || shouldSend ? turnId : action);
|
|
48
|
+
if (!selected) {
|
|
49
|
+
await deps.reply(request, `No artifact turn found for "${argument}".`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
deps.appendActivity(request, {
|
|
53
|
+
status: "info",
|
|
54
|
+
type: shouldZip ? "artifact_zip_sent" : "artifacts_sent",
|
|
55
|
+
threadId: info.threadId,
|
|
56
|
+
workspace,
|
|
57
|
+
agentId: info.agentId,
|
|
58
|
+
detail: selected.turnId,
|
|
59
|
+
});
|
|
60
|
+
if (shouldZip) {
|
|
61
|
+
await deliverDiscordArtifactZip(deps, request, selected);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await deliverDiscordArtifactReport(deps, request, selected);
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await deliverChannelAction(deps.runtime, request.context, renderDiscordArtifactReports(request.contextKey, reports));
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export async function sendRecentDiscordArtifacts(deps, request, session, since, turnId) {
|
|
72
|
+
const report = await collectRecentWorkspaceArtifacts(session.getInfo().workspace, {
|
|
73
|
+
since,
|
|
74
|
+
until: new Date(),
|
|
75
|
+
maxFileSize: deps.config.maxFileSize,
|
|
76
|
+
limit: 5,
|
|
77
|
+
});
|
|
78
|
+
if (report.artifacts.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await deps.reply(request, `${report.artifacts.length} artifacts generated.`);
|
|
82
|
+
for (const artifact of report.artifacts.slice(0, 5)) {
|
|
83
|
+
await sendDiscordArtifactFile(deps, request, artifact);
|
|
84
|
+
}
|
|
85
|
+
deps.appendActivity(request, {
|
|
86
|
+
status: "info",
|
|
87
|
+
type: "artifacts_sent",
|
|
88
|
+
detail: `${report.artifacts.length} artifacts for ${turnId}`,
|
|
89
|
+
threadId: session.getInfo().threadId,
|
|
90
|
+
workspace: session.getInfo().workspace,
|
|
91
|
+
agentId: session.getInfo().agentId,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function renderDiscordArtifactReports(contextKey, reports) {
|
|
95
|
+
const rendered = renderArtifactReportsAction(reports);
|
|
96
|
+
return {
|
|
97
|
+
...rendered,
|
|
98
|
+
buttons: reports.slice(0, 5).map((report, index) => [
|
|
99
|
+
{ label: `${index + 1} Send`, action: `discord_artifact_send:${contextKey}:${report.turnId}` },
|
|
100
|
+
{ label: `${index + 1} ZIP`, action: `discord_artifact_zip:${contextKey}:${report.turnId}` },
|
|
101
|
+
{ label: `${index + 1} Delete`, action: `discord_artifact_delete:${contextKey}:${report.turnId}` },
|
|
102
|
+
]),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function findArtifactReport(reports, requested) {
|
|
106
|
+
const value = requested?.trim();
|
|
107
|
+
if (!value || value.toLowerCase() === "latest") {
|
|
108
|
+
return reports[0];
|
|
109
|
+
}
|
|
110
|
+
return reports.find((report) => report.turnId === value || report.turnId.startsWith(value));
|
|
111
|
+
}
|
|
112
|
+
async function deliverDiscordArtifactZip(deps, request, report) {
|
|
113
|
+
const bundle = await createArtifactZipBundle(report.artifacts, report.outDir, {
|
|
114
|
+
maxFileSize: deps.config.maxFileSize,
|
|
115
|
+
bundleName: `nordrelay-artifacts-${report.turnId}.zip`,
|
|
116
|
+
});
|
|
117
|
+
if (!bundle) {
|
|
118
|
+
await deps.reply(request, "Could not create a ZIP bundle for this artifact turn.");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!deps.runtime.sendFile) {
|
|
122
|
+
await deps.reply(request, "This Discord runtime cannot send artifact files.");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
await deps.runtime.sendFile(request.context, { localPath: bundle.localPath, name: bundle.name });
|
|
126
|
+
await deps.reply(request, `Sent ZIP artifact bundle: ${bundle.name}`);
|
|
127
|
+
}
|
|
128
|
+
async function deliverDiscordArtifactReport(deps, request, report) {
|
|
129
|
+
if (report.artifacts.length === 0 && report.skippedCount === 0 && !report.omittedCount) {
|
|
130
|
+
await deps.reply(request, "No generated artifacts found for this turn.");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
let failedCount = 0;
|
|
134
|
+
let bundledArtifact = null;
|
|
135
|
+
if (report.artifacts.length > 5) {
|
|
136
|
+
bundledArtifact = await createArtifactZipBundle(report.artifacts, report.outDir, {
|
|
137
|
+
maxFileSize: deps.config.maxFileSize,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const delivered = bundledArtifact ? [bundledArtifact] : report.artifacts;
|
|
141
|
+
for (const artifact of delivered) {
|
|
142
|
+
if (!await sendDiscordArtifactFile(deps, request, artifact)) {
|
|
143
|
+
failedCount += 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const summary = formatArtifactSummary(report.artifacts, report.skippedCount + failedCount, report.omittedCount);
|
|
147
|
+
if (summary) {
|
|
148
|
+
const bundleNote = bundledArtifact ? `\nSent as ZIP: ${bundledArtifact.name}` : "";
|
|
149
|
+
await deps.reply(request, `${summary}${bundleNote}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function sendDiscordArtifactFile(deps, request, artifact) {
|
|
153
|
+
if (!deps.runtime.sendFile) {
|
|
154
|
+
await deps.reply(request, "This Discord runtime cannot send artifact files.");
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
await deps.runtime.sendFile(request.context, { localPath: artifact.localPath, name: artifact.name });
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.error(`Failed to send Discord artifact ${artifact.name}:`, error);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|