@nordbyte/nordrelay 0.4.1 → 0.5.1
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 +155 -64
- package/README.md +81 -65
- package/dist/access-control.js +126 -115
- package/dist/agent-updates.js +62 -9
- package/dist/bot-rendering.js +838 -0
- package/dist/bot-ui.js +1 -0
- package/dist/bot.js +342 -2498
- package/dist/channel-actions.js +8 -8
- package/dist/channel-runtime.js +89 -0
- package/dist/config-metadata.js +238 -0
- package/dist/config.js +0 -58
- package/dist/index.js +8 -0
- package/dist/operations.js +63 -9
- package/dist/relay-artifact-service.js +126 -0
- package/dist/relay-external-activity-monitor.js +216 -0
- package/dist/relay-queue-service.js +66 -0
- package/dist/relay-runtime-types.js +1 -0
- package/dist/relay-runtime.js +96 -354
- package/dist/settings-service.js +2 -117
- package/dist/support-bundle.js +205 -0
- package/dist/telegram-access-commands.js +123 -0
- package/dist/telegram-access-middleware.js +129 -0
- package/dist/telegram-agent-commands.js +212 -0
- package/dist/telegram-artifact-commands.js +139 -0
- package/dist/telegram-channel-runtime.js +132 -0
- package/dist/telegram-command-menu.js +55 -0
- package/dist/telegram-command-types.js +1 -0
- package/dist/telegram-diagnostics-command.js +102 -0
- package/dist/telegram-general-commands.js +52 -0
- package/dist/telegram-operational-commands.js +153 -0
- package/dist/telegram-output.js +216 -0
- package/dist/telegram-preference-commands.js +198 -0
- package/dist/telegram-queue-commands.js +278 -0
- package/dist/telegram-support-command.js +53 -0
- package/dist/telegram-update-commands.js +93 -0
- package/dist/user-management.js +708 -0
- package/dist/web-api-contract.js +104 -0
- package/dist/web-api-types.js +1 -0
- package/dist/web-dashboard-access-routes.js +163 -0
- package/dist/web-dashboard-artifact-routes.js +65 -0
- package/dist/web-dashboard-assets.js +35 -2
- package/dist/web-dashboard-http.js +143 -0
- package/dist/web-dashboard-pages.js +257 -0
- package/dist/web-dashboard-runtime-routes.js +92 -0
- package/dist/web-dashboard-session-routes.js +209 -0
- package/dist/web-dashboard-ui.js +14 -14
- package/dist/web-dashboard.js +330 -707
- package/dist/webui-assets/dashboard.css +989 -0
- package/dist/webui-assets/dashboard.js +1750 -0
- package/dist/zip-writer.js +83 -0
- package/package.json +13 -4
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/commands/remote.md +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +227 -78
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +1 -1
- package/dist/web-dashboard-client.js +0 -275
- package/dist/web-dashboard-style.js +0 -9
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function createZipBuffer(entries) {
|
|
2
|
+
const localParts = [];
|
|
3
|
+
const centralParts = [];
|
|
4
|
+
let offset = 0;
|
|
5
|
+
for (const entry of entries) {
|
|
6
|
+
const name = normalizeZipEntryName(entry.name);
|
|
7
|
+
const nameBuffer = Buffer.from(name, "utf8");
|
|
8
|
+
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data, "utf8");
|
|
9
|
+
const crc = crc32(data);
|
|
10
|
+
const date = entry.date ?? new Date();
|
|
11
|
+
const { dosTime, dosDate } = dateToDos(date);
|
|
12
|
+
const localHeader = Buffer.alloc(30);
|
|
13
|
+
localHeader.writeUInt32LE(0x04034b50, 0);
|
|
14
|
+
localHeader.writeUInt16LE(20, 4);
|
|
15
|
+
localHeader.writeUInt16LE(0x0800, 6);
|
|
16
|
+
localHeader.writeUInt16LE(0, 8);
|
|
17
|
+
localHeader.writeUInt16LE(dosTime, 10);
|
|
18
|
+
localHeader.writeUInt16LE(dosDate, 12);
|
|
19
|
+
localHeader.writeUInt32LE(crc, 14);
|
|
20
|
+
localHeader.writeUInt32LE(data.byteLength, 18);
|
|
21
|
+
localHeader.writeUInt32LE(data.byteLength, 22);
|
|
22
|
+
localHeader.writeUInt16LE(nameBuffer.byteLength, 26);
|
|
23
|
+
localHeader.writeUInt16LE(0, 28);
|
|
24
|
+
localParts.push(localHeader, nameBuffer, data);
|
|
25
|
+
const centralHeader = Buffer.alloc(46);
|
|
26
|
+
centralHeader.writeUInt32LE(0x02014b50, 0);
|
|
27
|
+
centralHeader.writeUInt16LE(20, 4);
|
|
28
|
+
centralHeader.writeUInt16LE(20, 6);
|
|
29
|
+
centralHeader.writeUInt16LE(0x0800, 8);
|
|
30
|
+
centralHeader.writeUInt16LE(0, 10);
|
|
31
|
+
centralHeader.writeUInt16LE(dosTime, 12);
|
|
32
|
+
centralHeader.writeUInt16LE(dosDate, 14);
|
|
33
|
+
centralHeader.writeUInt32LE(crc, 16);
|
|
34
|
+
centralHeader.writeUInt32LE(data.byteLength, 20);
|
|
35
|
+
centralHeader.writeUInt32LE(data.byteLength, 24);
|
|
36
|
+
centralHeader.writeUInt16LE(nameBuffer.byteLength, 28);
|
|
37
|
+
centralHeader.writeUInt16LE(0, 30);
|
|
38
|
+
centralHeader.writeUInt16LE(0, 32);
|
|
39
|
+
centralHeader.writeUInt16LE(0, 34);
|
|
40
|
+
centralHeader.writeUInt16LE(0, 36);
|
|
41
|
+
centralHeader.writeUInt32LE(0, 38);
|
|
42
|
+
centralHeader.writeUInt32LE(offset, 42);
|
|
43
|
+
centralParts.push(centralHeader, nameBuffer);
|
|
44
|
+
offset += localHeader.byteLength + nameBuffer.byteLength + data.byteLength;
|
|
45
|
+
}
|
|
46
|
+
const centralOffset = offset;
|
|
47
|
+
const centralDirectory = Buffer.concat(centralParts);
|
|
48
|
+
const end = Buffer.alloc(22);
|
|
49
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
50
|
+
end.writeUInt16LE(0, 4);
|
|
51
|
+
end.writeUInt16LE(0, 6);
|
|
52
|
+
end.writeUInt16LE(entries.length, 8);
|
|
53
|
+
end.writeUInt16LE(entries.length, 10);
|
|
54
|
+
end.writeUInt32LE(centralDirectory.byteLength, 12);
|
|
55
|
+
end.writeUInt32LE(centralOffset, 16);
|
|
56
|
+
end.writeUInt16LE(0, 20);
|
|
57
|
+
return Buffer.concat([...localParts, centralDirectory, end]);
|
|
58
|
+
}
|
|
59
|
+
function normalizeZipEntryName(name) {
|
|
60
|
+
return name.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
61
|
+
}
|
|
62
|
+
function dateToDos(date) {
|
|
63
|
+
const year = Math.max(1980, date.getFullYear());
|
|
64
|
+
return {
|
|
65
|
+
dosTime: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
|
|
66
|
+
dosDate: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const CRC32_TABLE = new Uint32Array(256);
|
|
70
|
+
for (let index = 0; index < CRC32_TABLE.length; index += 1) {
|
|
71
|
+
let value = index;
|
|
72
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
73
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
74
|
+
}
|
|
75
|
+
CRC32_TABLE[index] = value >>> 0;
|
|
76
|
+
}
|
|
77
|
+
function crc32(data) {
|
|
78
|
+
let crc = 0xffffffff;
|
|
79
|
+
for (const byte of data) {
|
|
80
|
+
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
81
|
+
}
|
|
82
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nordbyte/nordrelay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Remote control plane for coding agents across messaging channels.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Ricardo",
|
|
@@ -39,9 +39,13 @@
|
|
|
39
39
|
"docker-compose.yml"
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
|
-
"
|
|
43
|
-
"
|
|
42
|
+
"api:check": "node --import tsx scripts/generate-web-api-routes.mjs --check",
|
|
43
|
+
"api:generate": "node --import tsx scripts/generate-web-api-routes.mjs",
|
|
44
|
+
"build": "node scripts/clean-dist.mjs && npm run api:generate && tsc && node scripts/build-web-assets.mjs",
|
|
45
|
+
"check": "node --check plugins/nordrelay/scripts/nordrelay.mjs && npm run api:check && tsc --noEmit && npm run webui:check && node scripts/build-web-assets.mjs --check && node --import tsx scripts/generate-env-example.mjs --check",
|
|
44
46
|
"dev": "tsx src/index.ts",
|
|
47
|
+
"env:check": "node --import tsx scripts/generate-env-example.mjs --check",
|
|
48
|
+
"env:generate": "node --import tsx scripts/generate-env-example.mjs",
|
|
45
49
|
"foreground": "node plugins/nordrelay/scripts/nordrelay.mjs foreground",
|
|
46
50
|
"prepack": "npm run build",
|
|
47
51
|
"prepublishOnly": "npm run check && npm test && npm run build",
|
|
@@ -50,7 +54,10 @@
|
|
|
50
54
|
"status": "node plugins/nordrelay/scripts/nordrelay.mjs status",
|
|
51
55
|
"start": "node plugins/nordrelay/scripts/nordrelay.mjs start",
|
|
52
56
|
"stop": "node plugins/nordrelay/scripts/nordrelay.mjs stop",
|
|
53
|
-
"test": "vitest run"
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:e2e": "playwright test",
|
|
59
|
+
"test:all": "npm test && npm run test:e2e",
|
|
60
|
+
"webui:check": "tsc -p tsconfig.webui.json"
|
|
54
61
|
},
|
|
55
62
|
"dependencies": {
|
|
56
63
|
"@anthropic-ai/claude-agent-sdk": "^0.2.140",
|
|
@@ -60,8 +67,10 @@
|
|
|
60
67
|
"zod": "^4.4.3"
|
|
61
68
|
},
|
|
62
69
|
"devDependencies": {
|
|
70
|
+
"@playwright/test": "^1.60.0",
|
|
63
71
|
"@types/better-sqlite3": "^7.6.0",
|
|
64
72
|
"@types/node": "^25.5.0",
|
|
73
|
+
"esbuild": "^0.28.0",
|
|
65
74
|
"tsx": "^4.21.0",
|
|
66
75
|
"typescript": "^5.9.3",
|
|
67
76
|
"vitest": "^3.2.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nordrelay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Run a remote-control bridge for coding agents. Current adapters connect Codex, Pi, Hermes, and OpenClaw sessions to Telegram with streaming replies, multi-session controls, attachments, voice input, model selection, thread browsing, and handback.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ricardo",
|
|
@@ -21,7 +21,7 @@ Codex plugin commands are namespaced by the plugin id in current plugin-aware co
|
|
|
21
21
|
## Workflow
|
|
22
22
|
|
|
23
23
|
1. Locate the plugin root containing `.codex-plugin/plugin.json` with `"name": "nordrelay"`. In a source checkout this is usually `<repo>/plugins/nordrelay`.
|
|
24
|
-
2. Check whether `TELEGRAM_BOT_TOKEN`
|
|
24
|
+
2. Check whether `TELEGRAM_BOT_TOKEN` is available from the environment or from the NordRelay env file, and whether a NordRelay admin user exists.
|
|
25
25
|
3. Run the connector command from the plugin root:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
@@ -7,7 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import process from "node:process";
|
|
8
8
|
import readline from "node:readline/promises";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
11
|
|
|
12
12
|
const FALLBACK_VERSION = "0.3.1";
|
|
13
13
|
const require = createRequire(import.meta.url);
|
|
@@ -61,7 +61,10 @@ function parseArgs(argv) {
|
|
|
61
61
|
else if (arg === "--host") options.host = requireValue(copy, ++i, arg);
|
|
62
62
|
else if (arg === "--port") options.port = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
63
63
|
else if (arg === "--token") options.telegramBotToken = requireValue(copy, ++i, arg);
|
|
64
|
-
else if (arg === "--admin-
|
|
64
|
+
else if (arg === "--admin-email") options.adminEmail = requireValue(copy, ++i, arg);
|
|
65
|
+
else if (arg === "--admin-name") options.adminName = requireValue(copy, ++i, arg);
|
|
66
|
+
else if (arg === "--admin-password") options.adminPassword = requireValue(copy, ++i, arg);
|
|
67
|
+
else if (arg === "--telegram-user-id") options.telegramUserId = requireValue(copy, ++i, arg);
|
|
65
68
|
else if (arg === "--state-backend") options.stateBackend = requireValue(copy, ++i, arg);
|
|
66
69
|
else if (arg === "--enable-pi") options.enablePi = true;
|
|
67
70
|
else if (arg === "--enable-hermes") options.enableHermes = true;
|
|
@@ -128,14 +131,6 @@ function loadEnvFile(envPath) {
|
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
function normalizeEnvAliases() {
|
|
131
|
-
if (!process.env.TELEGRAM_ALLOWED_USER_IDS && process.env.TELEGRAM_ALLOWED_CHAT_IDS) {
|
|
132
|
-
process.env.TELEGRAM_ALLOWED_USER_IDS = process.env.TELEGRAM_ALLOWED_CHAT_IDS;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!process.env.TELEGRAM_ALLOWED_CHAT_IDS && process.env.TELEGRAM_ALLOWED_USER_IDS) {
|
|
136
|
-
process.env.TELEGRAM_ALLOWED_CHAT_IDS = process.env.TELEGRAM_ALLOWED_USER_IDS;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
134
|
if (!process.env.TOOL_VERBOSITY && envFlag("NORDRELAY_FORWARD_TOOL_OUTPUT")) {
|
|
140
135
|
process.env.TOOL_VERBOSITY = "all";
|
|
141
136
|
}
|
|
@@ -334,85 +329,180 @@ async function commandStatus(options) {
|
|
|
334
329
|
async function commandInit(options) {
|
|
335
330
|
await mkdirp(options.home);
|
|
336
331
|
const envPath = path.join(options.home, "nordrelay.env");
|
|
332
|
+
const userStore = await createUserStore(options.home);
|
|
337
333
|
if (fs.existsSync(envPath) && !options.force) {
|
|
338
334
|
console.log(`Config already exists: ${envPath}`);
|
|
339
335
|
console.log("Run with --force to overwrite.");
|
|
340
336
|
return;
|
|
341
337
|
}
|
|
342
338
|
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
? "
|
|
365
|
-
:
|
|
366
|
-
? "
|
|
367
|
-
:
|
|
368
|
-
? "
|
|
369
|
-
:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
339
|
+
const telegramBotToken = options.telegramBotToken ||
|
|
340
|
+
process.env.TELEGRAM_BOT_TOKEN ||
|
|
341
|
+
await ask(null, "Telegram bot token", "");
|
|
342
|
+
const adminEmail = options.adminEmail || await ask(null, "Admin email", "");
|
|
343
|
+
const adminName = options.adminName || await ask(null, "Admin name", "Admin");
|
|
344
|
+
const adminPassword = options.adminPassword || await askSecret(null, "Admin password", "");
|
|
345
|
+
const telegramUserId = options.telegramUserId || await ask(null, "Optional Telegram user id to link", "");
|
|
346
|
+
const enableCodex = options.disableCodex ? "false" : await askChoice(null, "Enable Codex", "true");
|
|
347
|
+
const enablePi = options.enablePi ? "true" : await askChoice(null, "Enable Pi", "false");
|
|
348
|
+
const enableHermes = options.enableHermes ? "true" : await askChoice(null, "Enable Hermes", "false");
|
|
349
|
+
const enableOpenClaw = options.enableOpenClaw ? "true" : await askChoice(null, "Enable OpenClaw", "false");
|
|
350
|
+
const enableClaudeCode = options.enableClaudeCode ? "true" : await askChoice(null, "Enable Claude Code", "false");
|
|
351
|
+
const stateBackend = options.stateBackend || await askChoice(null, "State backend (json/sqlite)", "json");
|
|
352
|
+
|
|
353
|
+
if (!telegramBotToken) throw new Error("Telegram bot token is required.");
|
|
354
|
+
if (!adminEmail) throw new Error("Admin email is required.");
|
|
355
|
+
if (!adminPassword) throw new Error("Admin password is required.");
|
|
356
|
+
if (enableCodex !== "true" && enablePi !== "true" && enableHermes !== "true" && enableOpenClaw !== "true" && enableClaudeCode !== "true") throw new Error("At least one agent must be enabled.");
|
|
357
|
+
const defaultAgent = enableCodex === "true"
|
|
358
|
+
? "codex"
|
|
359
|
+
: enablePi === "true"
|
|
360
|
+
? "pi"
|
|
361
|
+
: enableHermes === "true"
|
|
362
|
+
? "hermes"
|
|
363
|
+
: enableOpenClaw === "true"
|
|
364
|
+
? "openclaw"
|
|
365
|
+
: "claude-code";
|
|
366
|
+
|
|
367
|
+
const lines = [
|
|
368
|
+
"# NordRelay local runtime config.",
|
|
369
|
+
"# Keep this file private; it contains bot credentials.",
|
|
370
|
+
`TELEGRAM_BOT_TOKEN=${telegramBotToken}`,
|
|
371
|
+
`NORDRELAY_CODEX_ENABLED=${enableCodex}`,
|
|
372
|
+
`NORDRELAY_PI_ENABLED=${enablePi}`,
|
|
373
|
+
`NORDRELAY_HERMES_ENABLED=${enableHermes}`,
|
|
374
|
+
`NORDRELAY_OPENCLAW_ENABLED=${enableOpenClaw}`,
|
|
375
|
+
`NORDRELAY_CLAUDE_CODE_ENABLED=${enableClaudeCode}`,
|
|
376
|
+
`NORDRELAY_DEFAULT_AGENT=${defaultAgent}`,
|
|
377
|
+
"PI_DEFAULT_PROFILE=default",
|
|
378
|
+
"HERMES_API_BASE_URL=http://127.0.0.1:8642",
|
|
379
|
+
"HERMES_DEFAULT_PROFILE=default",
|
|
380
|
+
"OPENCLAW_GATEWAY_URL=ws://127.0.0.1:18789",
|
|
381
|
+
"OPENCLAW_AGENT_ID=main",
|
|
382
|
+
"OPENCLAW_DEFAULT_PROFILE=default",
|
|
383
|
+
"CLAUDE_CODE_DEFAULT_PROFILE=default",
|
|
384
|
+
"CLAUDE_CODE_MAX_TURNS=100",
|
|
385
|
+
`NORDRELAY_STATE_BACKEND=${stateBackend === "sqlite" ? "sqlite" : "json"}`,
|
|
386
|
+
"TELEGRAM_TRANSPORT=polling",
|
|
387
|
+
"TELEGRAM_AUTO_SEND_ARTIFACTS=false",
|
|
388
|
+
"",
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
await fsp.writeFile(envPath, lines.join("\n"), { mode: 0o600 });
|
|
392
|
+
await fsp.chmod(envPath, 0o600).catch(() => {});
|
|
393
|
+
userStore.createAdmin({
|
|
394
|
+
email: adminEmail,
|
|
395
|
+
displayName: adminName || adminEmail,
|
|
396
|
+
password: adminPassword,
|
|
397
|
+
telegramUserId: telegramUserId ? Number(telegramUserId) : undefined,
|
|
398
|
+
});
|
|
399
|
+
console.log(`Wrote ${envPath}`);
|
|
400
|
+
console.log(`Created admin user ${adminEmail}.`);
|
|
401
|
+
console.log("Run `nordrelay doctor` to validate the setup.");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function createUserStore(home) {
|
|
405
|
+
const modulePath = path.join(RUNTIME_ROOT, "dist", "user-management.js");
|
|
406
|
+
if (!fs.existsSync(modulePath)) {
|
|
407
|
+
throw new Error(`Missing user management runtime. Run \`npm run build\` in ${RUNTIME_ROOT}.`);
|
|
405
408
|
}
|
|
409
|
+
const mod = await import(pathToFileURL(modulePath).href);
|
|
410
|
+
return new mod.UserStore(home);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function parseUserFlags(argv) {
|
|
414
|
+
const copy = [...argv];
|
|
415
|
+
const subcommand = copy[0] && !copy[0].startsWith("-") ? copy.shift() : "list";
|
|
416
|
+
const flags = { subcommand };
|
|
417
|
+
for (let i = 0; i < copy.length; i += 1) {
|
|
418
|
+
const arg = copy[i];
|
|
419
|
+
if (arg === "--email") flags.email = requireValue(copy, ++i, arg);
|
|
420
|
+
else if (arg === "--name") flags.name = requireValue(copy, ++i, arg);
|
|
421
|
+
else if (arg === "--password") flags.password = requireValue(copy, ++i, arg);
|
|
422
|
+
else if (arg === "--group" || arg === "--groups") flags.groups = requireValue(copy, ++i, arg);
|
|
423
|
+
else if (arg === "--telegram-user-id") flags.telegramUserId = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
424
|
+
else if (arg === "--user-id") flags.userId = requireValue(copy, ++i, arg);
|
|
425
|
+
}
|
|
426
|
+
return flags;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function commandUser(options) {
|
|
430
|
+
await mkdirp(options.home);
|
|
431
|
+
loadEnvFiles(options.home);
|
|
432
|
+
const store = await createUserStore(options.home);
|
|
433
|
+
const flags = parseUserFlags(options.rawFlags);
|
|
434
|
+
if (flags.subcommand === "list") {
|
|
435
|
+
const snapshot = store.snapshot();
|
|
436
|
+
if (snapshot.users.length === 0) {
|
|
437
|
+
console.log("No users configured.");
|
|
438
|
+
console.log("Create the first admin with `nordrelay user create-admin --email you@example.com --name YourName`.");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
for (const user of snapshot.users) {
|
|
442
|
+
console.log(`${user.email} (${user.displayName}) ${user.active ? "active" : "disabled"} groups=${user.groups.map((group) => group.id).join(",") || "-"}`);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (flags.subcommand === "create-admin" || flags.subcommand === "create") {
|
|
448
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
449
|
+
const name = flags.name || await ask(null, "Display name", email);
|
|
450
|
+
const password = flags.password || await askSecret(null, "Password", "");
|
|
451
|
+
const groupIds = flags.subcommand === "create-admin"
|
|
452
|
+
? ["admin"]
|
|
453
|
+
: (flags.groups ? flags.groups.split(",").map((item) => item.trim()).filter(Boolean) : ["user"]);
|
|
454
|
+
const created = flags.subcommand === "create-admin"
|
|
455
|
+
? store.createAdmin({ email, displayName: name, password, telegramUserId: flags.telegramUserId })
|
|
456
|
+
: store.createUser({ email, displayName: name, password, groupIds, telegramUserId: flags.telegramUserId });
|
|
457
|
+
console.log(`Created user ${created.user.email} (${created.groups.map((group) => group.name).join(", ")}).`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (flags.subcommand === "reset-password") {
|
|
462
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
463
|
+
const password = flags.password || await askSecret(null, "New password", "");
|
|
464
|
+
const user = store.getUserByEmail(email);
|
|
465
|
+
if (!user) throw new Error(`User not found: ${email}`);
|
|
466
|
+
store.setPassword(user.user.id, password);
|
|
467
|
+
console.log(`Password updated for ${user.user.email}.`);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (flags.subcommand === "link-telegram") {
|
|
472
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
473
|
+
const telegramUserId = flags.telegramUserId || Number.parseInt(await ask(null, "Telegram user id", ""), 10);
|
|
474
|
+
const user = store.getUserByEmail(email);
|
|
475
|
+
if (!user) throw new Error(`User not found: ${email}`);
|
|
476
|
+
store.linkTelegramUser(user.user.id, { telegramUserId });
|
|
477
|
+
console.log(`Linked Telegram user ${telegramUserId} to ${user.user.email}.`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (flags.subcommand === "link-code") {
|
|
482
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
483
|
+
const user = store.getUserByEmail(email);
|
|
484
|
+
if (!user) throw new Error(`User not found: ${email}`);
|
|
485
|
+
const code = store.createTelegramLinkCode(user.user.id);
|
|
486
|
+
console.log(`Telegram link code for ${user.user.email}: ${code.code}`);
|
|
487
|
+
console.log(`Expires: ${code.expiresAt}`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
throw new Error("Usage: nordrelay user [list|create-admin|create|reset-password|link-telegram|link-code]");
|
|
406
492
|
}
|
|
407
493
|
|
|
408
494
|
async function commandDoctor(options) {
|
|
409
495
|
await mkdirp(options.home);
|
|
410
496
|
loadEnvFiles(options.home);
|
|
497
|
+
const userStore = await createUserStore(options.home).catch(() => null);
|
|
498
|
+
const userSnapshot = userStore?.snapshot();
|
|
411
499
|
const checks = [];
|
|
412
500
|
checks.push(check("Node.js >= 22", Number.parseInt(process.versions.node.split(".")[0], 10) >= 22, process.version));
|
|
413
501
|
checks.push(check("Telegram bot token", Boolean(process.env.TELEGRAM_BOT_TOKEN), process.env.TELEGRAM_BOT_TOKEN ? "configured" : "missing"));
|
|
414
|
-
checks.push(check("
|
|
415
|
-
checks.push(check("
|
|
502
|
+
checks.push(check("User store", Boolean(userStore), userStore ? userStore.filePath : "missing runtime", userStore ? "pass" : "fail"));
|
|
503
|
+
checks.push(check("Admin user", Boolean(userSnapshot?.adminConfigured), userSnapshot?.adminConfigured ? "configured" : "missing"));
|
|
504
|
+
checks.push(check("WebUI login", true, "required for every dashboard request"));
|
|
505
|
+
checks.push(check("Telegram access", true, "requires linked active users and enabled group chats"));
|
|
416
506
|
checks.push(check("Codex enabled flag", process.env.NORDRELAY_CODEX_ENABLED !== "false", `NORDRELAY_CODEX_ENABLED=${process.env.NORDRELAY_CODEX_ENABLED ?? "true"}`));
|
|
417
507
|
checks.push(check("Pi enabled flag", process.env.NORDRELAY_PI_ENABLED === "true" || process.env.NORDRELAY_PI_ENABLED === undefined, `NORDRELAY_PI_ENABLED=${process.env.NORDRELAY_PI_ENABLED ?? "false"}`, process.env.NORDRELAY_PI_ENABLED === "true" ? "pass" : "warn"));
|
|
418
508
|
checks.push(check("Hermes enabled flag", process.env.NORDRELAY_HERMES_ENABLED === "true", `NORDRELAY_HERMES_ENABLED=${process.env.NORDRELAY_HERMES_ENABLED ?? "false"}`, process.env.NORDRELAY_HERMES_ENABLED === "true" ? "pass" : "warn"));
|
|
@@ -687,10 +777,68 @@ function findRuntimeRoot() {
|
|
|
687
777
|
}
|
|
688
778
|
|
|
689
779
|
async function ask(rl, label, defaultValue) {
|
|
690
|
-
if (!rl) return defaultValue;
|
|
691
780
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
692
|
-
|
|
693
|
-
|
|
781
|
+
if (rl) {
|
|
782
|
+
const answer = (await rl.question(`${label}${suffix}: `)).trim();
|
|
783
|
+
return answer || defaultValue;
|
|
784
|
+
}
|
|
785
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return defaultValue;
|
|
786
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
787
|
+
try {
|
|
788
|
+
const answer = (await prompt.question(`${label}${suffix}: `)).trim();
|
|
789
|
+
return answer || defaultValue;
|
|
790
|
+
} finally {
|
|
791
|
+
prompt.close();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function askSecret(rl, label, defaultValue) {
|
|
796
|
+
void rl;
|
|
797
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return defaultValue;
|
|
798
|
+
const suffix = defaultValue ? " [hidden default]" : "";
|
|
799
|
+
return await new Promise((resolve) => {
|
|
800
|
+
const input = process.stdin;
|
|
801
|
+
const output = process.stdout;
|
|
802
|
+
const wasRaw = input.isRaw;
|
|
803
|
+
let value = "";
|
|
804
|
+
output.write(`${label}${suffix}: `);
|
|
805
|
+
input.setRawMode(true);
|
|
806
|
+
input.resume();
|
|
807
|
+
const cleanup = () => {
|
|
808
|
+
input.off("data", onData);
|
|
809
|
+
input.setRawMode(Boolean(wasRaw));
|
|
810
|
+
input.pause();
|
|
811
|
+
};
|
|
812
|
+
const finish = () => {
|
|
813
|
+
cleanup();
|
|
814
|
+
output.write("\n");
|
|
815
|
+
resolve(value || defaultValue);
|
|
816
|
+
};
|
|
817
|
+
const onData = (chunk) => {
|
|
818
|
+
const text = chunk.toString("utf8");
|
|
819
|
+
for (const char of text) {
|
|
820
|
+
if (char === "\u0003") {
|
|
821
|
+
cleanup();
|
|
822
|
+
output.write("\n");
|
|
823
|
+
process.exit(130);
|
|
824
|
+
}
|
|
825
|
+
if (char === "\r" || char === "\n") {
|
|
826
|
+
finish();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (char === "\u007f" || char === "\b") {
|
|
830
|
+
if (value.length > 0) {
|
|
831
|
+
value = value.slice(0, -1);
|
|
832
|
+
output.write("\b \b");
|
|
833
|
+
}
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
value += char;
|
|
837
|
+
output.write("*");
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
input.on("data", onData);
|
|
841
|
+
});
|
|
694
842
|
}
|
|
695
843
|
|
|
696
844
|
async function askChoice(rl, label, defaultValue) {
|
|
@@ -770,6 +918,7 @@ async function main() {
|
|
|
770
918
|
if (options.command === "stop") return commandStop(options);
|
|
771
919
|
if (options.command === "status") return commandStatus(options);
|
|
772
920
|
if (options.command === "init") return commandInit(options);
|
|
921
|
+
if (options.command === "user") return commandUser(options);
|
|
773
922
|
if (options.command === "doctor") return commandDoctor(options);
|
|
774
923
|
if (options.command === "web" || options.command === "dashboard") return commandWeb(options);
|
|
775
924
|
if (options.command === "restart") {
|
|
@@ -783,7 +932,7 @@ async function main() {
|
|
|
783
932
|
}
|
|
784
933
|
|
|
785
934
|
console.error(`Unknown command: ${options.command}`);
|
|
786
|
-
console.error("Usage: nordrelay [init|doctor|web|start|stop|restart|status|foreground|version]");
|
|
935
|
+
console.error("Usage: nordrelay [init|user|doctor|web|start|stop|restart|status|foreground|version]");
|
|
787
936
|
process.exitCode = 2;
|
|
788
937
|
}
|
|
789
938
|
|
|
@@ -21,6 +21,6 @@ node scripts/nordrelay.mjs status
|
|
|
21
21
|
node scripts/nordrelay.mjs stop
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The bridge needs `TELEGRAM_BOT_TOKEN` and
|
|
24
|
+
The bridge needs `TELEGRAM_BOT_TOKEN` and at least one NordRelay admin user. Telegram accounts must be linked to active NordRelay users; group or forum chats must be enabled by an admin before they can control agents.
|
|
25
25
|
|
|
26
26
|
Prefer `start` for normal use. Use `foreground` only when debugging connection problems, because it keeps the current command running. If the runtime is missing, run `npm install` and `npm run build` in the repository root.
|