@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,73 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export function createChannelTurnLifecycle(promptDescription) {
|
|
3
|
+
const startedAt = Date.now();
|
|
4
|
+
const turnId = randomUUID().slice(0, 12);
|
|
5
|
+
const progress = {
|
|
6
|
+
status: "running",
|
|
7
|
+
promptDescription,
|
|
8
|
+
startedAt,
|
|
9
|
+
updatedAt: startedAt,
|
|
10
|
+
toolCounts: new Map(),
|
|
11
|
+
textCharacters: 0,
|
|
12
|
+
};
|
|
13
|
+
const touch = () => {
|
|
14
|
+
progress.updatedAt = Date.now();
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
turnId,
|
|
18
|
+
startedAt,
|
|
19
|
+
progress,
|
|
20
|
+
touch,
|
|
21
|
+
recordTextDelta: (characters) => {
|
|
22
|
+
progress.textCharacters += Math.max(0, characters);
|
|
23
|
+
touch();
|
|
24
|
+
},
|
|
25
|
+
recordToolStart: (toolName) => {
|
|
26
|
+
progress.currentTool = toolName;
|
|
27
|
+
progress.lastTool = toolName;
|
|
28
|
+
progress.toolCounts.set(toolName, (progress.toolCounts.get(toolName) ?? 0) + 1);
|
|
29
|
+
touch();
|
|
30
|
+
},
|
|
31
|
+
recordToolUpdate: touch,
|
|
32
|
+
recordToolEnd: () => {
|
|
33
|
+
progress.currentTool = undefined;
|
|
34
|
+
touch();
|
|
35
|
+
},
|
|
36
|
+
recordCompleted: () => {
|
|
37
|
+
progress.status = "completed";
|
|
38
|
+
progress.completedAt = Date.now();
|
|
39
|
+
progress.updatedAt = progress.completedAt;
|
|
40
|
+
},
|
|
41
|
+
recordFailed: (error) => {
|
|
42
|
+
progress.status = "failed";
|
|
43
|
+
progress.error = error;
|
|
44
|
+
progress.completedAt = Date.now();
|
|
45
|
+
progress.updatedAt = progress.completedAt;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function createChannelTypingLoop(options) {
|
|
50
|
+
let timer;
|
|
51
|
+
let running = false;
|
|
52
|
+
const sendTyping = () => {
|
|
53
|
+
void options.sendTyping().catch(() => { });
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
start: () => {
|
|
57
|
+
if (running) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
running = true;
|
|
61
|
+
timer = setInterval(sendTyping, options.intervalMs);
|
|
62
|
+
timer.unref?.();
|
|
63
|
+
sendTyping();
|
|
64
|
+
},
|
|
65
|
+
stop: () => {
|
|
66
|
+
running = false;
|
|
67
|
+
if (timer) {
|
|
68
|
+
clearInterval(timer);
|
|
69
|
+
timer = undefined;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/codex-state.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isCodexApprovalPolicy, isCodexSandboxMode, } from "./codex-launch.js";
|
|
4
|
+
const ROLLOUT_CACHE_MAX_EVENTS = 200;
|
|
5
|
+
const rolloutSnapshotCache = new Map();
|
|
4
6
|
export const FALLBACK_MODELS = [
|
|
5
7
|
{ slug: "gpt-5.5", displayName: "GPT-5.5" },
|
|
6
8
|
{ slug: "gpt-5.4", displayName: "GPT-5.4" },
|
|
@@ -74,31 +76,7 @@ export function getThreadUsage(id) {
|
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
export function getThreadActivity(id, options = {}) {
|
|
77
|
-
|
|
78
|
-
if (!rolloutPath || !existsSync(rolloutPath)) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
const parsed = parseActivityFromRollout(id, rolloutPath, readFileSync(rolloutPath, "utf8"));
|
|
83
|
-
const fileModifiedAtMs = statSync(rolloutPath).mtimeMs;
|
|
84
|
-
const updatedAtMs = Math.max(parsed.updatedAt?.getTime() ?? 0, fileModifiedAtMs);
|
|
85
|
-
const updatedAt = updatedAtMs > 0 ? new Date(updatedAtMs) : parsed.updatedAt;
|
|
86
|
-
const staleAfterMs = options.staleAfterMs ?? 5 * 60 * 1000;
|
|
87
|
-
const nowMs = options.nowMs ?? Date.now();
|
|
88
|
-
const stale = Boolean(parsed.active &&
|
|
89
|
-
updatedAt &&
|
|
90
|
-
staleAfterMs > 0 &&
|
|
91
|
-
nowMs - updatedAt.getTime() > staleAfterMs);
|
|
92
|
-
return {
|
|
93
|
-
...parsed,
|
|
94
|
-
updatedAt,
|
|
95
|
-
stale,
|
|
96
|
-
active: parsed.active && !stale,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
79
|
+
return getThreadRolloutSnapshot(id, { ...options, maxEvents: 0 })?.activity ?? null;
|
|
102
80
|
}
|
|
103
81
|
export function getThreadRolloutSnapshot(id, options = {}) {
|
|
104
82
|
const rolloutPath = getThreadRolloutPath(id);
|
|
@@ -106,8 +84,9 @@ export function getThreadRolloutSnapshot(id, options = {}) {
|
|
|
106
84
|
return null;
|
|
107
85
|
}
|
|
108
86
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
87
|
+
const fileModifiedAtMs = statSync(rolloutPath).mtimeMs;
|
|
88
|
+
const parsed = readCachedRolloutSnapshot(id, rolloutPath);
|
|
89
|
+
return finalizeRolloutSnapshot(parsed, fileModifiedAtMs, options);
|
|
111
90
|
}
|
|
112
91
|
catch {
|
|
113
92
|
return null;
|
|
@@ -167,13 +146,7 @@ export function getThreadRolloutPath(id) {
|
|
|
167
146
|
}) ?? null);
|
|
168
147
|
}
|
|
169
148
|
function parseUsageFromRollout(contents) {
|
|
170
|
-
|
|
171
|
-
let contextUsedPercent = null;
|
|
172
|
-
let lastTokenUsage = null;
|
|
173
|
-
let totalTokenUsage = null;
|
|
174
|
-
let rateLimits = null;
|
|
175
|
-
let updatedAt = null;
|
|
176
|
-
for (const line of contents.split(/\r?\n/)) {
|
|
149
|
+
for (const line of iterateLinesReverse(contents)) {
|
|
177
150
|
if (!line.includes('"token_count"')) {
|
|
178
151
|
continue;
|
|
179
152
|
}
|
|
@@ -188,6 +161,7 @@ function parseUsageFromRollout(contents) {
|
|
|
188
161
|
if (payload?.type !== "token_count") {
|
|
189
162
|
continue;
|
|
190
163
|
}
|
|
164
|
+
let updatedAt = null;
|
|
191
165
|
const timestamp = readString(readObject(event)?.timestamp);
|
|
192
166
|
if (timestamp) {
|
|
193
167
|
const parsedTimestamp = new Date(timestamp);
|
|
@@ -196,54 +170,116 @@ function parseUsageFromRollout(contents) {
|
|
|
196
170
|
}
|
|
197
171
|
}
|
|
198
172
|
const info = readObject(payload.info);
|
|
199
|
-
const
|
|
200
|
-
const
|
|
173
|
+
const totalTokenUsage = parseTokenUsage(readObject(info?.total_token_usage));
|
|
174
|
+
const lastTokenUsage = parseTokenUsage(readObject(info?.last_token_usage));
|
|
201
175
|
const parsedContextWindow = readNumber(info?.model_context_window);
|
|
202
|
-
|
|
203
|
-
|
|
176
|
+
const contextWindow = parsedContextWindow !== null && parsedContextWindow > 0
|
|
177
|
+
? parsedContextWindow
|
|
178
|
+
: null;
|
|
179
|
+
const contextUsedPercent = lastTokenUsage && contextWindow
|
|
180
|
+
? Math.min(100, (lastTokenUsage.totalTokens / contextWindow) * 100)
|
|
181
|
+
: null;
|
|
182
|
+
const rateLimits = parseRateLimits(readObject(payload.rate_limits));
|
|
183
|
+
if (!lastTokenUsage && !totalTokenUsage && !rateLimits) {
|
|
184
|
+
continue;
|
|
204
185
|
}
|
|
205
|
-
|
|
206
|
-
|
|
186
|
+
return {
|
|
187
|
+
contextWindow,
|
|
188
|
+
contextUsedPercent,
|
|
189
|
+
lastTokenUsage,
|
|
190
|
+
totalTokenUsage,
|
|
191
|
+
rateLimits,
|
|
192
|
+
updatedAt,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function readCachedRolloutSnapshot(threadId, rolloutPath) {
|
|
198
|
+
const size = statSync(rolloutPath).size;
|
|
199
|
+
const cached = rolloutSnapshotCache.get(rolloutPath);
|
|
200
|
+
if (cached && size >= cached.byteOffset) {
|
|
201
|
+
const suffix = size > cached.byteOffset
|
|
202
|
+
? readFileRangeUtf8(rolloutPath, cached.byteOffset, size - cached.byteOffset)
|
|
203
|
+
: "";
|
|
204
|
+
if (!suffix.trim()) {
|
|
205
|
+
return cached.parsed;
|
|
207
206
|
}
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
const parsed = parseRolloutSnapshot(threadId, rolloutPath, suffix, {
|
|
208
|
+
base: cached.parsed,
|
|
209
|
+
maxEvents: ROLLOUT_CACHE_MAX_EVENTS,
|
|
210
|
+
});
|
|
211
|
+
rolloutSnapshotCache.set(rolloutPath, { byteOffset: size, parsed });
|
|
212
|
+
return parsed;
|
|
213
|
+
}
|
|
214
|
+
const contents = readFileSync(rolloutPath, "utf8");
|
|
215
|
+
const parsed = parseRolloutSnapshot(threadId, rolloutPath, contents, {
|
|
216
|
+
maxEvents: ROLLOUT_CACHE_MAX_EVENTS,
|
|
217
|
+
});
|
|
218
|
+
rolloutSnapshotCache.set(rolloutPath, {
|
|
219
|
+
byteOffset: Buffer.byteLength(contents),
|
|
220
|
+
parsed,
|
|
221
|
+
});
|
|
222
|
+
return parsed;
|
|
223
|
+
}
|
|
224
|
+
function readFileRangeUtf8(filePath, position, length) {
|
|
225
|
+
if (length <= 0) {
|
|
226
|
+
return "";
|
|
227
|
+
}
|
|
228
|
+
const fd = openSync(filePath, "r");
|
|
229
|
+
try {
|
|
230
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
231
|
+
const bytesRead = readSync(fd, buffer, 0, length, position);
|
|
232
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
closeSync(fd);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function* iterateLinesReverse(contents) {
|
|
239
|
+
let end = contents.length;
|
|
240
|
+
while (end > 0) {
|
|
241
|
+
let start = contents.lastIndexOf("\n", end - 1);
|
|
242
|
+
const lineStart = start === -1 ? 0 : start + 1;
|
|
243
|
+
let line = contents.slice(lineStart, end);
|
|
244
|
+
if (line.endsWith("\r")) {
|
|
245
|
+
line = line.slice(0, -1);
|
|
210
246
|
}
|
|
211
|
-
if (
|
|
212
|
-
|
|
247
|
+
if (line.trim()) {
|
|
248
|
+
yield line;
|
|
213
249
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
rateLimits = parsedRateLimits;
|
|
250
|
+
if (start === -1) {
|
|
251
|
+
break;
|
|
217
252
|
}
|
|
253
|
+
end = start;
|
|
218
254
|
}
|
|
219
|
-
if (!lastTokenUsage && !totalTokenUsage && !rateLimits) {
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
contextWindow,
|
|
224
|
-
contextUsedPercent,
|
|
225
|
-
lastTokenUsage,
|
|
226
|
-
totalTokenUsage,
|
|
227
|
-
rateLimits,
|
|
228
|
-
updatedAt,
|
|
229
|
-
};
|
|
230
255
|
}
|
|
231
|
-
function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
let
|
|
236
|
-
let
|
|
237
|
-
let
|
|
238
|
-
|
|
239
|
-
let latestUserMessage = null;
|
|
240
|
-
let latestToolName = null;
|
|
241
|
-
const events = [];
|
|
256
|
+
function parseRolloutSnapshot(threadId, rolloutPath, contents, options = {}) {
|
|
257
|
+
let activeTurnId = options.base?.activity.active ? options.base.activity.turnId : null;
|
|
258
|
+
let startedAt = options.base?.activity.active ? options.base.activity.startedAt : null;
|
|
259
|
+
let updatedAt = options.base?.activity.updatedAt ?? null;
|
|
260
|
+
let latestAgentMessage = options.base?.latestAgentMessage ?? null;
|
|
261
|
+
let latestUserMessage = options.base?.latestUserMessage ?? null;
|
|
262
|
+
let latestToolName = options.base?.latestToolName ?? null;
|
|
263
|
+
const events = [...(options.base?.events ?? [])];
|
|
242
264
|
const lines = contents.split(/\r?\n/);
|
|
265
|
+
const lineNumberOffset = options.base?.lineCount ?? 0;
|
|
266
|
+
let lineCount = lineNumberOffset;
|
|
267
|
+
const afterLine = options.afterLine ?? 0;
|
|
268
|
+
const maxEvents = options.maxEvents ?? Number.POSITIVE_INFINITY;
|
|
269
|
+
const pushEvent = (event) => {
|
|
270
|
+
if (maxEvents <= 0 || event.lineNumber <= afterLine) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
events.push(event);
|
|
274
|
+
if (Number.isFinite(maxEvents) && events.length > maxEvents) {
|
|
275
|
+
events.splice(0, events.length - maxEvents);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
243
278
|
for (const [index, line] of lines.entries()) {
|
|
244
279
|
if (!line.trim()) {
|
|
245
280
|
continue;
|
|
246
281
|
}
|
|
282
|
+
lineCount += 1;
|
|
247
283
|
if (!line.includes('"task_') &&
|
|
248
284
|
!line.includes('"turn_') &&
|
|
249
285
|
!line.includes('"user_message"') &&
|
|
@@ -262,7 +298,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
262
298
|
const eventObject = readObject(event);
|
|
263
299
|
const payload = readObject(eventObject?.payload);
|
|
264
300
|
const eventTimestamp = parseTimestamp(readString(eventObject?.timestamp));
|
|
265
|
-
const lineNumber = index + 1;
|
|
301
|
+
const lineNumber = lineNumberOffset + index + 1;
|
|
266
302
|
if (activeTurnId && eventTimestamp) {
|
|
267
303
|
updatedAt = eventTimestamp;
|
|
268
304
|
}
|
|
@@ -274,7 +310,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
274
310
|
activeTurnId = readString(payload?.turn_id);
|
|
275
311
|
startedAt = parseUnixSeconds(readNumber(payload?.started_at)) ?? eventTimestamp;
|
|
276
312
|
updatedAt = eventTimestamp ?? startedAt;
|
|
277
|
-
|
|
313
|
+
pushEvent({
|
|
278
314
|
lineNumber,
|
|
279
315
|
kind: "task",
|
|
280
316
|
timestamp: eventTimestamp,
|
|
@@ -289,7 +325,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
289
325
|
}
|
|
290
326
|
if (isTaskTerminalEvent(type)) {
|
|
291
327
|
const turnId = readString(payload?.turn_id);
|
|
292
|
-
|
|
328
|
+
pushEvent({
|
|
293
329
|
lineNumber,
|
|
294
330
|
kind: "task",
|
|
295
331
|
timestamp: eventTimestamp,
|
|
@@ -309,7 +345,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
309
345
|
}
|
|
310
346
|
if (type === "user_message") {
|
|
311
347
|
latestUserMessage = readString(payload?.message);
|
|
312
|
-
|
|
348
|
+
pushEvent({
|
|
313
349
|
lineNumber,
|
|
314
350
|
kind: "user",
|
|
315
351
|
timestamp: eventTimestamp,
|
|
@@ -324,7 +360,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
324
360
|
}
|
|
325
361
|
if (type === "agent_message") {
|
|
326
362
|
latestAgentMessage = readString(payload?.message);
|
|
327
|
-
|
|
363
|
+
pushEvent({
|
|
328
364
|
lineNumber,
|
|
329
365
|
kind: "agent",
|
|
330
366
|
timestamp: eventTimestamp,
|
|
@@ -339,7 +375,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
339
375
|
}
|
|
340
376
|
if (type === "function_call") {
|
|
341
377
|
latestToolName = readString(payload?.name);
|
|
342
|
-
|
|
378
|
+
pushEvent({
|
|
343
379
|
lineNumber,
|
|
344
380
|
kind: "tool",
|
|
345
381
|
timestamp: eventTimestamp,
|
|
@@ -353,7 +389,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
353
389
|
continue;
|
|
354
390
|
}
|
|
355
391
|
if (type === "function_call_output") {
|
|
356
|
-
|
|
392
|
+
pushEvent({
|
|
357
393
|
lineNumber,
|
|
358
394
|
kind: "tool",
|
|
359
395
|
timestamp: eventTimestamp,
|
|
@@ -369,7 +405,7 @@ function parseRolloutSnapshot(threadId, rolloutPath, contents) {
|
|
|
369
405
|
return {
|
|
370
406
|
threadId,
|
|
371
407
|
rolloutPath,
|
|
372
|
-
lineCount
|
|
408
|
+
lineCount,
|
|
373
409
|
activity: {
|
|
374
410
|
threadId,
|
|
375
411
|
rolloutPath,
|
package/dist/config-metadata.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export const SECRET_KEYS = new Set([
|
|
2
2
|
"TELEGRAM_BOT_TOKEN",
|
|
3
3
|
"DISCORD_BOT_TOKEN",
|
|
4
|
+
"SLACK_BOT_TOKEN",
|
|
5
|
+
"SLACK_APP_TOKEN",
|
|
6
|
+
"SLACK_SIGNING_SECRET",
|
|
4
7
|
"CODEX_API_KEY",
|
|
5
8
|
"HERMES_API_KEY",
|
|
6
9
|
"OPENCLAW_GATEWAY_TOKEN",
|
|
@@ -24,15 +27,34 @@ const DISCORD_SETTING_HELP = {
|
|
|
24
27
|
DISCORD_QUIET_HOURS: "Use a local-time range like 22-7, off, or blank to inherit the channel-neutral quiet-hours setting.",
|
|
25
28
|
DISCORD_AUTO_SEND_ARTIFACTS: "Overrides automatic artifact upload behavior for Discord only. Leave blank to use NORDRELAY_AUTO_SEND_ARTIFACTS.",
|
|
26
29
|
};
|
|
30
|
+
const SLACK_SETTING_HELP = {
|
|
31
|
+
SLACK_ENABLED: "Create a Slack app, install it into the workspace, then set bot/app tokens before enabling Slack.",
|
|
32
|
+
SLACK_BOT_TOKEN: "Slack app OAuth & Permissions: copy the bot token that starts with xoxb-.",
|
|
33
|
+
SLACK_APP_TOKEN: "Slack app Basic Information: create an app-level token with connections:write. Required for Socket Mode.",
|
|
34
|
+
SLACK_SIGNING_SECRET: "Slack app Basic Information: copy Signing Secret. Required only when Socket Mode is disabled.",
|
|
35
|
+
SLACK_ALLOWED_TEAM_IDS: "Optional workspace allow-list. Copy Team IDs from Slack event payloads or app diagnostics.",
|
|
36
|
+
SLACK_ALLOWED_CHANNEL_IDS: "Optional channel allow-list before NordRelay user/group checks. Copy channel IDs from Slack channel details.",
|
|
37
|
+
SLACK_COMMAND: "Slash command configured in the Slack app. Defaults to /nordrelay.",
|
|
38
|
+
};
|
|
39
|
+
const TELEGRAM_SETTING_HELP = {
|
|
40
|
+
TELEGRAM_ENABLED: "Enable this only after the BotFather token is configured and NordRelay users/chats are allowed through the user management system.",
|
|
41
|
+
TELEGRAM_BOT_TOKEN: "Telegram BotFather: open @BotFather, create a bot with /newbot, then paste only the token value.",
|
|
42
|
+
TELEGRAM_TRANSPORT: "Use polling for the simplest setup. Use webhook only when this NordRelay instance is reachable from Telegram through public HTTPS.",
|
|
43
|
+
TELEGRAM_WEBHOOK_URL: "Public HTTPS base URL for Telegram webhook delivery, for example https://relay.example.com.",
|
|
44
|
+
TELEGRAM_WEBHOOK_HOST: "Local interface where NordRelay binds the webhook listener. Use 127.0.0.1 behind a reverse proxy or 0.0.0.0 only when the endpoint is protected.",
|
|
45
|
+
TELEGRAM_WEBHOOK_PORT: "Local port for the Telegram webhook listener.",
|
|
46
|
+
TELEGRAM_WEBHOOK_PATH: "Webhook request path registered with Telegram. It must start with /.",
|
|
47
|
+
TELEGRAM_WEBHOOK_SECRET: "Optional secret token Telegram sends in X-Telegram-Bot-Api-Secret-Token. Use a random value for webhook mode.",
|
|
48
|
+
};
|
|
27
49
|
export const SETTING_DEFINITIONS = [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
telegramSetting("TELEGRAM_ENABLED", "Enable Telegram", "boolean", "Start the Telegram bot adapter.", true),
|
|
51
|
+
telegramSetting("TELEGRAM_BOT_TOKEN", "Telegram bot token", "secret", "BotFather token.", true),
|
|
52
|
+
telegramSetting("TELEGRAM_TRANSPORT", "Telegram transport", "string", "polling or webhook.", true, ["polling", "webhook"]),
|
|
53
|
+
telegramSetting("TELEGRAM_WEBHOOK_URL", "Webhook public URL", "string", "Public base URL for webhook mode.", true),
|
|
54
|
+
telegramSetting("TELEGRAM_WEBHOOK_HOST", "Webhook bind host", "string", "Local webhook bind host.", true),
|
|
55
|
+
telegramSetting("TELEGRAM_WEBHOOK_PORT", "Webhook bind port", "number", "Local webhook bind port.", true),
|
|
56
|
+
telegramSetting("TELEGRAM_WEBHOOK_PATH", "Webhook path", "string", "Webhook request path.", true),
|
|
57
|
+
telegramSetting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "secret", "Optional Telegram webhook secret token.", true),
|
|
36
58
|
discordSetting("DISCORD_ENABLED", "Enable Discord", "boolean", "Start the Discord bot adapter.", true),
|
|
37
59
|
discordSetting("DISCORD_BOT_TOKEN", "Discord bot token", "secret", "Discord bot token.", true),
|
|
38
60
|
discordSetting("DISCORD_CLIENT_ID", "Discord client ID", "string", "Discord application/client id used for slash command registration.", true),
|
|
@@ -47,6 +69,21 @@ export const SETTING_DEFINITIONS = [
|
|
|
47
69
|
discordSetting("DISCORD_NOTIFY_MODE", "Discord notify override", "string", "Optional Discord override for completion notifications.", false, ["off", "minimal", "all"]),
|
|
48
70
|
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
71
|
discordSetting("DISCORD_AUTO_SEND_ARTIFACTS", "Discord auto-send artifacts override", "boolean", "Optional Discord override for automatic artifact summaries/uploads.", false),
|
|
72
|
+
slackSetting("SLACK_ENABLED", "Enable Slack", "boolean", "Start the Slack bot adapter.", true),
|
|
73
|
+
slackSetting("SLACK_BOT_TOKEN", "Slack bot token", "secret", "Slack bot token.", true),
|
|
74
|
+
slackSetting("SLACK_APP_TOKEN", "Slack app token", "secret", "Slack app-level token for Socket Mode.", true),
|
|
75
|
+
slackSetting("SLACK_SIGNING_SECRET", "Slack signing secret", "secret", "Slack signing secret for HTTP Events mode.", true),
|
|
76
|
+
slackSetting("SLACK_SOCKET_MODE", "Slack Socket Mode", "boolean", "Use Slack Socket Mode instead of an HTTP events receiver.", true),
|
|
77
|
+
slackSetting("SLACK_PORT", "Slack HTTP port", "number", "HTTP port used when Slack Socket Mode is disabled.", true),
|
|
78
|
+
slackSetting("SLACK_ALLOWED_TEAM_IDS", "Allowed Slack teams", "list", "Optional comma-separated Slack team/workspace allow-list.", true),
|
|
79
|
+
slackSetting("SLACK_ALLOWED_CHANNEL_IDS", "Allowed Slack channels", "list", "Optional comma-separated Slack channel allow-list before user/group checks.", true),
|
|
80
|
+
slackSetting("SLACK_MESSAGE_CONTENT_ENABLED", "Slack message content", "boolean", "Read regular Slack text messages as prompts.", true),
|
|
81
|
+
slackSetting("SLACK_COMMAND", "Slack Slash command", "string", "Slash command configured in Slack.", true),
|
|
82
|
+
slackSetting("SLACK_CLI_MIRROR_MODE", "Slack mirror override", "string", "Optional Slack override for CLI mirror mode. Uses the NordRelay default when unset.", false, ["off", "status", "final", "full"]),
|
|
83
|
+
slackSetting("SLACK_CLI_MIRROR_MIN_UPDATE_MS", "Slack mirror update override", "number", "Optional Slack override for mirrored edit interval.", true),
|
|
84
|
+
slackSetting("SLACK_NOTIFY_MODE", "Slack notify override", "string", "Optional Slack override for completion notifications.", false, ["off", "minimal", "all"]),
|
|
85
|
+
slackSetting("SLACK_QUIET_HOURS", "Slack quiet hours override", "string", "Optional Slack quiet hours override. Use HH-HH, off, or leave blank for default.", false),
|
|
86
|
+
slackSetting("SLACK_AUTO_SEND_ARTIFACTS", "Slack auto-send artifacts override", "boolean", "Optional Slack override for automatic artifact summaries/uploads.", false),
|
|
50
87
|
setting("NORDRELAY_CODEX_ENABLED", "Enable Codex", "Agents", "boolean", "Allow Codex sessions.", true),
|
|
51
88
|
setting("NORDRELAY_PI_ENABLED", "Enable Pi", "Agents", "boolean", "Allow Pi sessions.", true),
|
|
52
89
|
setting("NORDRELAY_HERMES_ENABLED", "Enable Hermes", "Agents", "boolean", "Allow Hermes sessions through the Hermes API Server.", true),
|
|
@@ -128,6 +165,13 @@ export const SETTING_DEFINITIONS = [
|
|
|
128
165
|
setting("NORDRELAY_UNIFIED_JOB_MAX_ITEMS", "Unified job history", "Workspace", "number", "Maximum persisted unified jobs retained for the WebUI jobs view.", true),
|
|
129
166
|
setting("NORDRELAY_VERSION_CACHE_TTL_MS", "Version cache TTL", "Workspace", "number", "NPM version cache TTL.", true),
|
|
130
167
|
setting("NORDRELAY_CLI_VERSION_CACHE_TTL_MS", "CLI version cache TTL", "Workspace", "number", "Installed agent CLI version cache TTL.", true),
|
|
168
|
+
setting("NORDRELAY_PEER_ENABLED", "Enable peer server", "Peers", "boolean", "Expose the dedicated authenticated NordRelay peer API.", true),
|
|
169
|
+
setting("NORDRELAY_PEER_NAME", "Peer display name", "Peers", "string", "Human-readable name shown to paired NordRelay instances.", true),
|
|
170
|
+
setting("NORDRELAY_PEER_HOST", "Peer bind host", "Peers", "string", "Bind host for the peer API. Use 127.0.0.1 for local-only or a LAN/interface IP when explicitly exposing peers.", true),
|
|
171
|
+
setting("NORDRELAY_PEER_PORT", "Peer port", "Peers", "number", "Port for the peer API.", true),
|
|
172
|
+
setting("NORDRELAY_PEER_PUBLIC_URL", "Peer public URL", "Peers", "string", "Optional public URL other instances should use for this node.", true),
|
|
173
|
+
setting("NORDRELAY_PEER_TLS_ENABLED", "Peer TLS enabled", "Peers", "boolean", "Serve the peer API over HTTPS with an automatically generated local certificate.", true),
|
|
174
|
+
setting("NORDRELAY_PEER_REQUIRE_TLS", "Require peer TLS", "Peers", "boolean", "Reject plaintext peer serving on non-loopback hosts.", true),
|
|
131
175
|
setting("OPENAI_API_KEY", "OpenAI API key", "Voice", "secret", "Whisper fallback API key.", true),
|
|
132
176
|
setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
|
|
133
177
|
setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
|
|
@@ -159,6 +203,21 @@ const EXAMPLE_VALUES = {
|
|
|
159
203
|
"DISCORD_NOTIFY_MODE": "",
|
|
160
204
|
"DISCORD_QUIET_HOURS": "",
|
|
161
205
|
"DISCORD_AUTO_SEND_ARTIFACTS": "",
|
|
206
|
+
"SLACK_ENABLED": "false",
|
|
207
|
+
"SLACK_BOT_TOKEN": "",
|
|
208
|
+
"SLACK_APP_TOKEN": "",
|
|
209
|
+
"SLACK_SIGNING_SECRET": "",
|
|
210
|
+
"SLACK_SOCKET_MODE": "true",
|
|
211
|
+
"SLACK_PORT": "3000",
|
|
212
|
+
"SLACK_ALLOWED_TEAM_IDS": "",
|
|
213
|
+
"SLACK_ALLOWED_CHANNEL_IDS": "",
|
|
214
|
+
"SLACK_MESSAGE_CONTENT_ENABLED": "true",
|
|
215
|
+
"SLACK_COMMAND": "/nordrelay",
|
|
216
|
+
"SLACK_CLI_MIRROR_MODE": "",
|
|
217
|
+
"SLACK_CLI_MIRROR_MIN_UPDATE_MS": "",
|
|
218
|
+
"SLACK_NOTIFY_MODE": "",
|
|
219
|
+
"SLACK_QUIET_HOURS": "",
|
|
220
|
+
"SLACK_AUTO_SEND_ARTIFACTS": "",
|
|
162
221
|
"NORDRELAY_CODEX_ENABLED": "true",
|
|
163
222
|
"NORDRELAY_PI_ENABLED": "false",
|
|
164
223
|
"NORDRELAY_HERMES_ENABLED": "false",
|
|
@@ -243,6 +302,13 @@ const EXAMPLE_VALUES = {
|
|
|
243
302
|
"NORDRELAY_UNIFIED_JOB_MAX_ITEMS": "1000",
|
|
244
303
|
"NORDRELAY_VERSION_CACHE_TTL_MS": "3600000",
|
|
245
304
|
"NORDRELAY_CLI_VERSION_CACHE_TTL_MS": "60000",
|
|
305
|
+
"NORDRELAY_PEER_ENABLED": "false",
|
|
306
|
+
"NORDRELAY_PEER_NAME": "",
|
|
307
|
+
"NORDRELAY_PEER_HOST": "127.0.0.1",
|
|
308
|
+
"NORDRELAY_PEER_PORT": "31979",
|
|
309
|
+
"NORDRELAY_PEER_PUBLIC_URL": "",
|
|
310
|
+
"NORDRELAY_PEER_TLS_ENABLED": "true",
|
|
311
|
+
"NORDRELAY_PEER_REQUIRE_TLS": "true",
|
|
246
312
|
"NORDRELAY_DASHBOARD_HOST": "127.0.0.1",
|
|
247
313
|
"NORDRELAY_DASHBOARD_PORT": "31878",
|
|
248
314
|
"NORDRELAY_ENV_FILE": "",
|
|
@@ -262,6 +328,7 @@ const EXAMPLE_VALUES = {
|
|
|
262
328
|
const GROUP_INTROS = {
|
|
263
329
|
Telegram: "Telegram bot and transport settings.",
|
|
264
330
|
Discord: "Discord bot settings. Discord is opt-in and uses the same NordRelay users, groups, and permissions as Telegram.",
|
|
331
|
+
Slack: "Slack bot settings. Slack is opt-in and uses the same NordRelay users, groups, and permissions as Telegram and Discord.",
|
|
265
332
|
Agents: "Agent access. Codex is enabled by default; Pi, Hermes, OpenClaw, and Claude Code are opt-in.",
|
|
266
333
|
Codex: "Codex defaults for newly created or reattached sessions.",
|
|
267
334
|
Pi: "Pi coding agent defaults.",
|
|
@@ -271,6 +338,7 @@ const GROUP_INTROS = {
|
|
|
271
338
|
Operations: "Runtime output, logging, update, and Telegram behavior controls.",
|
|
272
339
|
Artifacts: "File, artifact, and retention controls.",
|
|
273
340
|
Workspace: "State and workspace guardrails.",
|
|
341
|
+
Peers: "Optional NordRelay-to-NordRelay federation. Pairing is explicit, authenticated, scoped, and TLS-protected.",
|
|
274
342
|
Voice: "Optional voice transcription settings.",
|
|
275
343
|
Dashboard: "Local WebUI dashboard. User login is required for every page, API route, SSE stream, artifact download, and health endpoint.",
|
|
276
344
|
};
|
|
@@ -303,3 +371,9 @@ function setting(key, label, group, kind, description, restartRequired, options,
|
|
|
303
371
|
function discordSetting(key, label, kind, description, restartRequired, options) {
|
|
304
372
|
return setting(key, label, "Discord", kind, description, restartRequired, options, DISCORD_SETTING_HELP[key]);
|
|
305
373
|
}
|
|
374
|
+
function telegramSetting(key, label, kind, description, restartRequired, options) {
|
|
375
|
+
return setting(key, label, "Telegram", kind, description, restartRequired, options, TELEGRAM_SETTING_HELP[key]);
|
|
376
|
+
}
|
|
377
|
+
function slackSetting(key, label, kind, description, restartRequired, options) {
|
|
378
|
+
return setting(key, label, "Slack", kind, description, restartRequired, options, SLACK_SETTING_HELP[key]);
|
|
379
|
+
}
|