@nordbyte/nordrelay 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -0
- package/README.md +67 -6
- package/dist/access-control.js +6 -1
- package/dist/activity-events.js +2 -2
- package/dist/bot-preferences.js +1 -0
- package/dist/bot.js +77 -6
- package/dist/channel-adapter.js +11 -5
- package/dist/channel-command-catalog.js +88 -0
- package/dist/channel-command-service.js +214 -1
- package/dist/channel-mirror-registry.js +77 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +15 -0
- package/dist/config.js +31 -6
- package/dist/context-key.js +10 -0
- package/dist/discord-bot.js +85 -26
- package/dist/discord-command-surface.js +11 -73
- package/dist/index.js +20 -0
- package/dist/metrics.js +46 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +256 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-runtime-service.js +636 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +294 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-runtime-helpers.js +208 -0
- package/dist/relay-runtime.js +72 -274
- package/dist/remote-prompt.js +98 -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/web-api-contract.js +8 -0
- package/dist/web-dashboard-pages.js +12 -0
- package/dist/web-dashboard-peer-routes.js +204 -0
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +12 -0
- package/dist/webui-assets/dashboard.js +427 -14
- package/package.json +3 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +373 -7
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
|
@@ -128,6 +128,13 @@ export const SETTING_DEFINITIONS = [
|
|
|
128
128
|
setting("NORDRELAY_UNIFIED_JOB_MAX_ITEMS", "Unified job history", "Workspace", "number", "Maximum persisted unified jobs retained for the WebUI jobs view.", true),
|
|
129
129
|
setting("NORDRELAY_VERSION_CACHE_TTL_MS", "Version cache TTL", "Workspace", "number", "NPM version cache TTL.", true),
|
|
130
130
|
setting("NORDRELAY_CLI_VERSION_CACHE_TTL_MS", "CLI version cache TTL", "Workspace", "number", "Installed agent CLI version cache TTL.", true),
|
|
131
|
+
setting("NORDRELAY_PEER_ENABLED", "Enable peer server", "Peers", "boolean", "Expose the dedicated authenticated NordRelay peer API.", true),
|
|
132
|
+
setting("NORDRELAY_PEER_NAME", "Peer display name", "Peers", "string", "Human-readable name shown to paired NordRelay instances.", true),
|
|
133
|
+
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),
|
|
134
|
+
setting("NORDRELAY_PEER_PORT", "Peer port", "Peers", "number", "Port for the peer API.", true),
|
|
135
|
+
setting("NORDRELAY_PEER_PUBLIC_URL", "Peer public URL", "Peers", "string", "Optional public URL other instances should use for this node.", true),
|
|
136
|
+
setting("NORDRELAY_PEER_TLS_ENABLED", "Peer TLS enabled", "Peers", "boolean", "Serve the peer API over HTTPS with an automatically generated local certificate.", true),
|
|
137
|
+
setting("NORDRELAY_PEER_REQUIRE_TLS", "Require peer TLS", "Peers", "boolean", "Reject plaintext peer serving on non-loopback hosts.", true),
|
|
131
138
|
setting("OPENAI_API_KEY", "OpenAI API key", "Voice", "secret", "Whisper fallback API key.", true),
|
|
132
139
|
setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
|
|
133
140
|
setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
|
|
@@ -243,6 +250,13 @@ const EXAMPLE_VALUES = {
|
|
|
243
250
|
"NORDRELAY_UNIFIED_JOB_MAX_ITEMS": "1000",
|
|
244
251
|
"NORDRELAY_VERSION_CACHE_TTL_MS": "3600000",
|
|
245
252
|
"NORDRELAY_CLI_VERSION_CACHE_TTL_MS": "60000",
|
|
253
|
+
"NORDRELAY_PEER_ENABLED": "false",
|
|
254
|
+
"NORDRELAY_PEER_NAME": "",
|
|
255
|
+
"NORDRELAY_PEER_HOST": "127.0.0.1",
|
|
256
|
+
"NORDRELAY_PEER_PORT": "31979",
|
|
257
|
+
"NORDRELAY_PEER_PUBLIC_URL": "",
|
|
258
|
+
"NORDRELAY_PEER_TLS_ENABLED": "true",
|
|
259
|
+
"NORDRELAY_PEER_REQUIRE_TLS": "true",
|
|
246
260
|
"NORDRELAY_DASHBOARD_HOST": "127.0.0.1",
|
|
247
261
|
"NORDRELAY_DASHBOARD_PORT": "31878",
|
|
248
262
|
"NORDRELAY_ENV_FILE": "",
|
|
@@ -271,6 +285,7 @@ const GROUP_INTROS = {
|
|
|
271
285
|
Operations: "Runtime output, logging, update, and Telegram behavior controls.",
|
|
272
286
|
Artifacts: "File, artifact, and retention controls.",
|
|
273
287
|
Workspace: "State and workspace guardrails.",
|
|
288
|
+
Peers: "Optional NordRelay-to-NordRelay federation. Pairing is explicit, authenticated, scoped, and TLS-protected.",
|
|
274
289
|
Voice: "Optional voice transcription settings.",
|
|
275
290
|
Dashboard: "Local WebUI dashboard. User login is required for every page, API route, SSE stream, artifact download, and health endpoint.",
|
|
276
291
|
};
|
package/dist/config.js
CHANGED
|
@@ -5,8 +5,9 @@ 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
|
|
9
|
-
const
|
|
8
|
+
const adapterWarnings = [];
|
|
9
|
+
const requestedTelegramEnabled = parseBooleanEnv(optionalString(process.env.TELEGRAM_ENABLED), true);
|
|
10
|
+
const telegramBotToken = optionalString(process.env.TELEGRAM_BOT_TOKEN) ?? "";
|
|
10
11
|
const telegramRateLimitMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS), 80, "TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS");
|
|
11
12
|
const telegramEditMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_EDIT_MIN_INTERVAL_MS), 1_200, "TELEGRAM_EDIT_MIN_INTERVAL_MS");
|
|
12
13
|
const mirrorMode = parseMirrorMode(optionalString(process.env.NORDRELAY_CLI_MIRROR_MODE), "status");
|
|
@@ -25,7 +26,7 @@ export function loadConfig() {
|
|
|
25
26
|
const telegramWebhookPort = parsePositiveIntegerEnv(optionalString(process.env.TELEGRAM_WEBHOOK_PORT), 8080, "TELEGRAM_WEBHOOK_PORT");
|
|
26
27
|
const telegramWebhookPath = parseWebhookPath(optionalString(process.env.TELEGRAM_WEBHOOK_PATH));
|
|
27
28
|
const telegramWebhookSecret = optionalString(process.env.TELEGRAM_WEBHOOK_SECRET);
|
|
28
|
-
const
|
|
29
|
+
const requestedDiscordEnabled = parseBooleanEnv(optionalString(process.env.DISCORD_ENABLED), false);
|
|
29
30
|
const discordBotToken = optionalString(process.env.DISCORD_BOT_TOKEN);
|
|
30
31
|
const discordClientId = optionalString(process.env.DISCORD_CLIENT_ID);
|
|
31
32
|
const discordGuildIds = parseOptionalStringList(optionalString(process.env.DISCORD_GUILD_IDS));
|
|
@@ -108,16 +109,33 @@ export function loadConfig() {
|
|
|
108
109
|
const sessionLockTtlMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_SESSION_LOCK_TTL_MS), 30 * 60 * 1000, "NORDRELAY_SESSION_LOCK_TTL_MS");
|
|
109
110
|
const dashboardCacheTtlMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_DASHBOARD_CACHE_TTL_MS), 10_000, "NORDRELAY_DASHBOARD_CACHE_TTL_MS");
|
|
110
111
|
const unifiedJobMaxItems = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_UNIFIED_JOB_MAX_ITEMS), 1000, "NORDRELAY_UNIFIED_JOB_MAX_ITEMS");
|
|
112
|
+
const peerEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_ENABLED), false);
|
|
113
|
+
const peerName = optionalString(process.env.NORDRELAY_PEER_NAME);
|
|
114
|
+
const peerHost = optionalString(process.env.NORDRELAY_PEER_HOST) ?? "127.0.0.1";
|
|
115
|
+
const peerPort = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_PEER_PORT), 31979, "NORDRELAY_PEER_PORT");
|
|
116
|
+
const peerPublicUrl = optionalString(process.env.NORDRELAY_PEER_PUBLIC_URL);
|
|
117
|
+
const peerTlsEnabled = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_TLS_ENABLED), true);
|
|
118
|
+
const peerRequireTls = parseBooleanEnv(optionalString(process.env.NORDRELAY_PEER_REQUIRE_TLS), true);
|
|
119
|
+
let telegramEnabled = requestedTelegramEnabled;
|
|
111
120
|
if (telegramEnabled && telegramTransport === "webhook" && !telegramWebhookUrl) {
|
|
112
|
-
|
|
121
|
+
telegramEnabled = false;
|
|
122
|
+
adapterWarnings.push("Telegram disabled: TELEGRAM_TRANSPORT=webhook requires TELEGRAM_WEBHOOK_URL.");
|
|
113
123
|
}
|
|
124
|
+
if (telegramEnabled && !telegramBotToken) {
|
|
125
|
+
telegramEnabled = false;
|
|
126
|
+
adapterWarnings.push("Telegram disabled: TELEGRAM_BOT_TOKEN is missing.");
|
|
127
|
+
}
|
|
128
|
+
let discordEnabled = requestedDiscordEnabled;
|
|
114
129
|
if (discordEnabled && !discordBotToken) {
|
|
115
|
-
|
|
130
|
+
discordEnabled = false;
|
|
131
|
+
adapterWarnings.push("Discord disabled: DISCORD_ENABLED=true requires DISCORD_BOT_TOKEN.");
|
|
116
132
|
}
|
|
117
133
|
if (!telegramEnabled && !discordEnabled) {
|
|
118
|
-
|
|
134
|
+
const detail = adapterWarnings.length > 0 ? ` ${adapterWarnings.join(" ")}` : "";
|
|
135
|
+
throw new Error(`At least one usable chat adapter must be enabled.${detail}`);
|
|
119
136
|
}
|
|
120
137
|
return {
|
|
138
|
+
adapterWarnings,
|
|
121
139
|
telegramEnabled,
|
|
122
140
|
telegramBotToken,
|
|
123
141
|
telegramRateLimitMinIntervalMs,
|
|
@@ -220,6 +238,13 @@ export function loadConfig() {
|
|
|
220
238
|
sessionLockTtlMs,
|
|
221
239
|
dashboardCacheTtlMs,
|
|
222
240
|
unifiedJobMaxItems,
|
|
241
|
+
peerEnabled,
|
|
242
|
+
peerName,
|
|
243
|
+
peerHost,
|
|
244
|
+
peerPort,
|
|
245
|
+
peerPublicUrl,
|
|
246
|
+
peerTlsEnabled,
|
|
247
|
+
peerRequireTls,
|
|
223
248
|
};
|
|
224
249
|
}
|
|
225
250
|
/**
|
package/dist/context-key.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parsePeerRuntimeContextKey } from "./peer-context.js";
|
|
1
2
|
export function telegramContextKeyFromMessage(chatId, messageThreadId) {
|
|
2
3
|
if (messageThreadId !== undefined) {
|
|
3
4
|
return `${chatId}:${messageThreadId}`;
|
|
@@ -111,6 +112,15 @@ export function parseChannelContextKey(key) {
|
|
|
111
112
|
chatId: rawKey.slice("cli:".length) || "local",
|
|
112
113
|
};
|
|
113
114
|
}
|
|
115
|
+
const peer = parsePeerRuntimeContextKey(rawKey);
|
|
116
|
+
if (peer) {
|
|
117
|
+
return {
|
|
118
|
+
channelId: "peer",
|
|
119
|
+
contextKey: rawKey,
|
|
120
|
+
chatId: peer.peerId,
|
|
121
|
+
topicId: peer.sourceContextKey,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
114
124
|
return null;
|
|
115
125
|
}
|
|
116
126
|
export function channelIdForContextKey(key) {
|
package/dist/discord-bot.js
CHANGED
|
@@ -9,10 +9,12 @@ import { enabledAgents } from "./agent-factory.js";
|
|
|
9
9
|
import { collectRecentWorkspaceArtifacts, ensureOutDir, formatArtifactSummary, persistWorkspaceArtifactReport } from "./artifacts.js";
|
|
10
10
|
import { buildFileInstructions, outboxPath, stageFile } from "./attachments.js";
|
|
11
11
|
import { AuditLogStore } from "./audit-log.js";
|
|
12
|
-
import { BotPreferencesStore
|
|
12
|
+
import { BotPreferencesStore } from "./bot-preferences.js";
|
|
13
13
|
import { capabilitiesOf, filterActivityEvents, formatLocalDateTime, parseActivityOptions, renderExternalMirrorEvent, renderExternalMirrorStatus, renderPromptFailure, trimLine } from "./bot-rendering.js";
|
|
14
14
|
import { renderAgentUpdateJobAction, renderAgentUpdateJobsAction, renderAgentUpdateLogAction, renderAgentUpdatePickerAction, renderQueueListAction } from "./channel-actions.js";
|
|
15
15
|
import { ChannelCommandService } from "./channel-command-service.js";
|
|
16
|
+
import { discordHelpCommandList } from "./channel-command-catalog.js";
|
|
17
|
+
import { runChannelPeerPrompt } from "./channel-peer-prompt.js";
|
|
16
18
|
import { deliverChannelAction } from "./channel-runtime.js";
|
|
17
19
|
import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
|
|
18
20
|
import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
|
|
@@ -25,6 +27,7 @@ import { friendlyErrorText } from "./error-messages.js";
|
|
|
25
27
|
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
|
|
26
28
|
import { spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
|
|
27
29
|
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
30
|
+
import { RemoteRelayClient } from "./peer-client.js";
|
|
28
31
|
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
29
32
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
30
33
|
import { RelayArtifactService } from "./relay-artifact-service.js";
|
|
@@ -46,7 +49,8 @@ export function createDiscordBridge(config, registry) {
|
|
|
46
49
|
return null;
|
|
47
50
|
}
|
|
48
51
|
if (!config.discordBotToken) {
|
|
49
|
-
|
|
52
|
+
console.warn("Discord adapter disabled: DISCORD_ENABLED=true requires DISCORD_BOT_TOKEN.");
|
|
53
|
+
return null;
|
|
50
54
|
}
|
|
51
55
|
configureRedaction(config.telegramRedactPatterns);
|
|
52
56
|
const intents = [
|
|
@@ -522,10 +526,44 @@ export function createDiscordBridge(config, registry) {
|
|
|
522
526
|
await reply(request, `Session is locked by ${lock?.ownerLabel || lock?.ownerUserId || "another user"}.`);
|
|
523
527
|
return true;
|
|
524
528
|
};
|
|
529
|
+
const remoteClient = new RemoteRelayClient();
|
|
530
|
+
const handleRemotePrompt = async (request, envelope) => {
|
|
531
|
+
const targetPeerId = preferencesStore.get(request.contextKey).targetPeerId ?? undefined;
|
|
532
|
+
return runChannelPeerPrompt({
|
|
533
|
+
targetPeerId,
|
|
534
|
+
contextKey: request.contextKey,
|
|
535
|
+
prompt: envelope,
|
|
536
|
+
remoteClient,
|
|
537
|
+
editMinIntervalMs: EDIT_DEBOUNCE_MS,
|
|
538
|
+
typingIntervalMs: TYPING_INTERVAL_MS,
|
|
539
|
+
sendTyping: () => runtime.sendTyping(request.context),
|
|
540
|
+
sendResponse: async (text) => {
|
|
541
|
+
const rendered = trimDiscordMessage(text);
|
|
542
|
+
const sent = await runtime.sendMessage(request.context, { text: rendered, fallbackText: rendered });
|
|
543
|
+
return sent.messageId;
|
|
544
|
+
},
|
|
545
|
+
editResponse: async (messageId, text) => {
|
|
546
|
+
const rendered = trimDiscordMessage(text);
|
|
547
|
+
await runtime.editMessage(request.context, messageId, { text: rendered, fallbackText: rendered });
|
|
548
|
+
},
|
|
549
|
+
sendTurnStart: (remotePrompt) => reply(request, `Remote peer working on:\n${remotePrompt}`),
|
|
550
|
+
sendToolStart: (toolName) => reply(request, `Remote tool: ${toolName}`),
|
|
551
|
+
sendQueued: async (queueId) => {
|
|
552
|
+
await reply(request, `Remote prompt queued${queueId ? `: ${queueId}` : ""}.`, queueId ? {
|
|
553
|
+
buttons: [[{ label: "Cancel queued message", action: `discord_peer_queue_cancel:${targetPeerId}:${queueId}` }]],
|
|
554
|
+
} : undefined);
|
|
555
|
+
},
|
|
556
|
+
sendCompleted: () => reply(request, "Remote turn completed."),
|
|
557
|
+
sendFailure: (message) => reply(request, `Remote peer failed: ${message}`),
|
|
558
|
+
});
|
|
559
|
+
};
|
|
525
560
|
const handlePrompt = async (request, input, artifactOutDir, options = {}) => {
|
|
526
561
|
const session = await getSession(request);
|
|
527
562
|
const envelope = toPromptEnvelope(input, artifactOutDir);
|
|
528
563
|
envelope.activityActor = actorFor(request);
|
|
564
|
+
if (!options.fromQueue && await handleRemotePrompt(request, envelope)) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
529
567
|
if (!options.fromQueue && await denyIfLocked(request)) {
|
|
530
568
|
return;
|
|
531
569
|
}
|
|
@@ -893,6 +931,17 @@ export function createDiscordBridge(config, registry) {
|
|
|
893
931
|
case "channels":
|
|
894
932
|
await deliverChannelAction(runtime, request.context, commandService.renderChannels());
|
|
895
933
|
return;
|
|
934
|
+
case "peers":
|
|
935
|
+
await deliverChannelAction(runtime, request.context, commandService.renderPeers());
|
|
936
|
+
return;
|
|
937
|
+
case "target":
|
|
938
|
+
await deliverChannelAction(runtime, request.context, commandService.renderTargetPreference({
|
|
939
|
+
source: "discord",
|
|
940
|
+
contextKey: request.contextKey,
|
|
941
|
+
argument,
|
|
942
|
+
preferencesStore,
|
|
943
|
+
}));
|
|
944
|
+
return;
|
|
896
945
|
case "agents":
|
|
897
946
|
await deliverChannelAction(runtime, request.context, commandService.renderAgents());
|
|
898
947
|
return;
|
|
@@ -1046,7 +1095,7 @@ export function createDiscordBridge(config, registry) {
|
|
|
1046
1095
|
"",
|
|
1047
1096
|
"Send a message to prompt the selected agent, or use slash commands.",
|
|
1048
1097
|
"",
|
|
1049
|
-
|
|
1098
|
+
`Core commands: ${discordHelpCommandList()}.`,
|
|
1050
1099
|
"",
|
|
1051
1100
|
renderSessionInfoPlain(session.getInfo()),
|
|
1052
1101
|
].join("\n"));
|
|
@@ -1595,33 +1644,32 @@ export function createDiscordBridge(config, registry) {
|
|
|
1595
1644
|
await deliverChannelAction(runtime, request.context, commandService.renderHandback(result));
|
|
1596
1645
|
};
|
|
1597
1646
|
const commandMirror = async (request, argument) => {
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
await
|
|
1647
|
+
const session = await getSession(request, { deferThreadStart: true });
|
|
1648
|
+
const info = session.getInfo();
|
|
1649
|
+
await deliverChannelAction(runtime, request.context, commandService.renderMirrorPreference({
|
|
1650
|
+
source: "discord",
|
|
1651
|
+
contextKey: request.contextKey,
|
|
1652
|
+
argument,
|
|
1653
|
+
preferencesStore,
|
|
1654
|
+
cliMirrorSupported: capabilitiesOf(info).cliMirror,
|
|
1655
|
+
agentLabel: info.agentLabel,
|
|
1656
|
+
}));
|
|
1601
1657
|
};
|
|
1602
1658
|
const commandNotify = async (request, argument) => {
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1659
|
+
await deliverChannelAction(runtime, request.context, commandService.renderNotifyPreference({
|
|
1660
|
+
source: "discord",
|
|
1661
|
+
contextKey: request.contextKey,
|
|
1662
|
+
argument,
|
|
1663
|
+
preferencesStore,
|
|
1664
|
+
}));
|
|
1606
1665
|
};
|
|
1607
1666
|
const commandVoice = async (request, argument) => {
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
preferencesStore.update(request.contextKey, { voiceLanguage: parts[1] === "auto" ? null : parts[1] });
|
|
1615
|
-
}
|
|
1616
|
-
else if ((parts[0] === "transcribe-only" || parts[0] === "transcribe_only") && parts[1]) {
|
|
1617
|
-
preferencesStore.update(request.contextKey, { voiceTranscribeOnly: ["on", "true", "yes", "1"].includes(parts[1]) });
|
|
1618
|
-
}
|
|
1619
|
-
else if (argument.trim()) {
|
|
1620
|
-
await reply(request, "Usage: `/voice`, `/voice backend auto|parakeet|faster-whisper|openai`, `/voice language auto|<code>`, or `/voice transcribe_only on|off`.");
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
const prefs = preferencesStore.get(request.contextKey);
|
|
1624
|
-
await reply(request, `Voice backend: ${prefs.voiceBackend ?? config.voicePreferredBackend}\nLanguage: ${prefs.voiceLanguage ?? config.voiceDefaultLanguage ?? "auto"}\nTranscribe only: ${prefs.voiceTranscribeOnly ?? config.voiceTranscribeOnly}`);
|
|
1667
|
+
await deliverChannelAction(runtime, request.context, await commandService.renderVoicePreference({
|
|
1668
|
+
source: "discord",
|
|
1669
|
+
contextKey: request.contextKey,
|
|
1670
|
+
argument,
|
|
1671
|
+
preferencesStore,
|
|
1672
|
+
}));
|
|
1625
1673
|
};
|
|
1626
1674
|
const commandRegisterChannel = async (request) => {
|
|
1627
1675
|
const channel = userStore.registerDiscordChannel({
|
|
@@ -1781,6 +1829,17 @@ export function createDiscordBridge(config, registry) {
|
|
|
1781
1829
|
await commandQueue(request, `${queueMatch[1]} ${queueMatch[3]}`);
|
|
1782
1830
|
return;
|
|
1783
1831
|
}
|
|
1832
|
+
const peerQueueMatch = action.match(/^discord_peer_queue_cancel:([^:]+):([^:]+)$/);
|
|
1833
|
+
if (peerQueueMatch?.[1] && peerQueueMatch[2]) {
|
|
1834
|
+
await remoteClient.webProxy(peerQueueMatch[1], {
|
|
1835
|
+
method: "POST",
|
|
1836
|
+
path: "/api/queue",
|
|
1837
|
+
body: { action: "cancel", id: peerQueueMatch[2] },
|
|
1838
|
+
contextKey: request.contextKey,
|
|
1839
|
+
}, actorFor(request), request.contextKey);
|
|
1840
|
+
await reply(request, `Cancelled remote queued prompt ${peerQueueMatch[2]}.`, { ephemeral: true });
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1784
1843
|
const artifactMatch = action.match(/^discord_artifact_(send|zip|delete):(.+):([^:]+)$/);
|
|
1785
1844
|
if (artifactMatch?.[1] && artifactMatch[2] === request.contextKey) {
|
|
1786
1845
|
await commandArtifacts(request, `${artifactMatch[1]} ${artifactMatch[3]}`);
|