@nordbyte/nordrelay 0.5.0 → 0.5.2
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 +2 -0
- package/README.md +23 -14
- package/dist/access-control.js +2 -0
- package/dist/agent-updates.js +61 -10
- package/dist/bot-ui.js +1 -0
- package/dist/bot.js +142 -1065
- package/dist/channel-actions.js +8 -8
- package/dist/codex-cli.js +1 -1
- package/dist/config-metadata.js +2 -0
- package/dist/operations.js +233 -122
- 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 +119 -371
- package/dist/state-backend.js +3 -0
- package/dist/support-bundle.js +221 -0
- package/dist/telegram-agent-commands.js +212 -0
- package/dist/telegram-artifact-commands.js +139 -0
- package/dist/telegram-command-menu.js +1 -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-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 +6 -1
- package/dist/web-api-contract.js +79 -31
- 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 +2 -0
- 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.js +44 -882
- package/dist/webui-assets/dashboard.css +74 -4
- package/dist/webui-assets/dashboard.js +163 -24
- package/dist/zip-writer.js +83 -0
- package/package.json +10 -4
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +258 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nordbyte/nordrelay",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Remote control plane for coding agents across messaging channels.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Ricardo",
|
|
@@ -39,8 +39,10 @@
|
|
|
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",
|
|
45
47
|
"env:check": "node --import tsx scripts/generate-env-example.mjs --check",
|
|
46
48
|
"env:generate": "node --import tsx scripts/generate-env-example.mjs",
|
|
@@ -52,7 +54,10 @@
|
|
|
52
54
|
"status": "node plugins/nordrelay/scripts/nordrelay.mjs status",
|
|
53
55
|
"start": "node plugins/nordrelay/scripts/nordrelay.mjs start",
|
|
54
56
|
"stop": "node plugins/nordrelay/scripts/nordrelay.mjs stop",
|
|
55
|
-
"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"
|
|
56
61
|
},
|
|
57
62
|
"dependencies": {
|
|
58
63
|
"@anthropic-ai/claude-agent-sdk": "^0.2.140",
|
|
@@ -62,6 +67,7 @@
|
|
|
62
67
|
"zod": "^4.4.3"
|
|
63
68
|
},
|
|
64
69
|
"devDependencies": {
|
|
70
|
+
"@playwright/test": "^1.60.0",
|
|
65
71
|
"@types/better-sqlite3": "^7.6.0",
|
|
66
72
|
"@types/node": "^25.5.0",
|
|
67
73
|
"esbuild": "^0.28.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nordrelay",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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",
|
|
@@ -51,6 +51,8 @@ function parseArgs(argv) {
|
|
|
51
51
|
force: false,
|
|
52
52
|
host: undefined,
|
|
53
53
|
port: undefined,
|
|
54
|
+
restartAfterUpdate: true,
|
|
55
|
+
updateMethod: undefined,
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
for (let i = 0; i < copy.length; i += 1) {
|
|
@@ -60,6 +62,9 @@ function parseArgs(argv) {
|
|
|
60
62
|
else if (arg === "--force") options.force = true;
|
|
61
63
|
else if (arg === "--host") options.host = requireValue(copy, ++i, arg);
|
|
62
64
|
else if (arg === "--port") options.port = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
65
|
+
else if (arg === "--method") options.updateMethod = requireValue(copy, ++i, arg);
|
|
66
|
+
else if (arg === "--no-restart") options.restartAfterUpdate = false;
|
|
67
|
+
else if (arg === "--restart") options.restartAfterUpdate = true;
|
|
63
68
|
else if (arg === "--token") options.telegramBotToken = requireValue(copy, ++i, arg);
|
|
64
69
|
else if (arg === "--admin-email") options.adminEmail = requireValue(copy, ++i, arg);
|
|
65
70
|
else if (arg === "--admin-name") options.adminName = requireValue(copy, ++i, arg);
|
|
@@ -326,6 +331,244 @@ async function commandStatus(options) {
|
|
|
326
331
|
if (state.error) console.log(`Error: ${state.error}`);
|
|
327
332
|
}
|
|
328
333
|
|
|
334
|
+
async function commandUpdate(options) {
|
|
335
|
+
await mkdirp(options.home);
|
|
336
|
+
loadEnvFiles(options.home);
|
|
337
|
+
const method = resolveUpdateMethod(options);
|
|
338
|
+
const updateLog = path.join(options.home, "update.log");
|
|
339
|
+
await mkdirp(path.dirname(updateLog));
|
|
340
|
+
const log = fs.createWriteStream(updateLog, { flags: "a" });
|
|
341
|
+
const sourceRoot = RUNTIME_ROOT;
|
|
342
|
+
const wasRunning = isProcessRunning(await readPid(options.pidFile));
|
|
343
|
+
const summary = method === "npm"
|
|
344
|
+
? "Install latest @nordbyte/nordrelay with npm, verify the CLI, and restart if the connector is running."
|
|
345
|
+
: "Pull origin/main, install dependencies, run check, tests, build, and restart if the connector is running.";
|
|
346
|
+
|
|
347
|
+
console.log(`Starting NordRelay update (${method}).`);
|
|
348
|
+
console.log(`Source: ${sourceRoot}`);
|
|
349
|
+
console.log(`Log: ${updateLog}`);
|
|
350
|
+
logUpdateLine(log, `Starting ${method} connector self-update`);
|
|
351
|
+
logUpdateLine(log, summary);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
if (method === "npm") {
|
|
355
|
+
await runNpmSelfUpdate(sourceRoot, log);
|
|
356
|
+
} else {
|
|
357
|
+
await runGitSelfUpdate(sourceRoot, log);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (options.restartAfterUpdate && wasRunning) {
|
|
361
|
+
await runLoggedStep(log, "Restart NordRelay connector", process.execPath, [
|
|
362
|
+
SCRIPT_PATH,
|
|
363
|
+
"restart",
|
|
364
|
+
"--keep-pending-updates",
|
|
365
|
+
"--home",
|
|
366
|
+
options.home,
|
|
367
|
+
], { cwd: sourceRoot });
|
|
368
|
+
} else if (options.restartAfterUpdate) {
|
|
369
|
+
logUpdateLine(log, "Connector was not running; restart skipped.");
|
|
370
|
+
console.log("Connector was not running; restart skipped.");
|
|
371
|
+
} else {
|
|
372
|
+
logUpdateLine(log, "Restart skipped by --no-restart.");
|
|
373
|
+
console.log("Restart skipped by --no-restart.");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
logUpdateLine(log, "NordRelay update completed.");
|
|
377
|
+
console.log("NordRelay update completed.");
|
|
378
|
+
} catch (error) {
|
|
379
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
380
|
+
logUpdateLine(log, `ERROR ${message}`);
|
|
381
|
+
console.error(`Update failed: ${message}`);
|
|
382
|
+
process.exitCode = 1;
|
|
383
|
+
} finally {
|
|
384
|
+
await closeLogStream(log);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function resolveUpdateMethod(options) {
|
|
389
|
+
const requested = (options.updateMethod || process.env.NORDRELAY_UPDATE_METHOD || "auto").trim().toLowerCase();
|
|
390
|
+
if (!requested || requested === "auto") {
|
|
391
|
+
return fs.existsSync(path.join(RUNTIME_ROOT, ".git")) ? "git" : "npm";
|
|
392
|
+
}
|
|
393
|
+
if (requested === "npm" || requested === "git") {
|
|
394
|
+
return requested;
|
|
395
|
+
}
|
|
396
|
+
throw new Error(`Unsupported update method "${requested}". Use auto, npm, or git.`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function runNpmSelfUpdate(sourceRoot, log) {
|
|
400
|
+
const npm = resolveNpmSpawnCommand();
|
|
401
|
+
if (!npm) {
|
|
402
|
+
throw new Error("npm was not found. Install Node.js/npm or add npm to PATH.");
|
|
403
|
+
}
|
|
404
|
+
await runLoggedStep(log, "Install latest NordRelay package", npm.command, [
|
|
405
|
+
...npm.argsPrefix,
|
|
406
|
+
"install",
|
|
407
|
+
"-g",
|
|
408
|
+
"@nordbyte/nordrelay@latest",
|
|
409
|
+
], { cwd: os.homedir(), shell: npm.shell });
|
|
410
|
+
await runVerifyNordRelayCli(sourceRoot, log);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function runGitSelfUpdate(sourceRoot, log) {
|
|
414
|
+
const git = resolveRequiredCommand("git");
|
|
415
|
+
const npm = resolveNpmSpawnCommand();
|
|
416
|
+
if (!npm) {
|
|
417
|
+
throw new Error("npm was not found. Install Node.js/npm or add npm to PATH.");
|
|
418
|
+
}
|
|
419
|
+
await runLoggedStep(log, "Pull latest source", git.command, ["pull", "--ff-only", "origin", "main"], { cwd: sourceRoot, shell: git.shell });
|
|
420
|
+
await runLoggedStep(log, "Install dependencies", npm.command, [...npm.argsPrefix, "install"], { cwd: sourceRoot, shell: npm.shell });
|
|
421
|
+
await runLoggedStep(log, "Run checks", npm.command, [...npm.argsPrefix, "run", "check"], { cwd: sourceRoot, shell: npm.shell });
|
|
422
|
+
await runLoggedStep(log, "Run tests", npm.command, [...npm.argsPrefix, "test"], { cwd: sourceRoot, shell: npm.shell });
|
|
423
|
+
await runLoggedStep(log, "Build runtime", npm.command, [...npm.argsPrefix, "run", "build"], { cwd: sourceRoot, shell: npm.shell });
|
|
424
|
+
await runVerifyNordRelayCli(sourceRoot, log);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function runVerifyNordRelayCli(sourceRoot, log) {
|
|
428
|
+
if (fs.existsSync(SCRIPT_PATH)) {
|
|
429
|
+
await runLoggedStep(log, "Verify NordRelay CLI", process.execPath, [SCRIPT_PATH, "version"], { cwd: sourceRoot });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const nordrelay = resolveRequiredCommand("nordrelay");
|
|
433
|
+
await runLoggedStep(log, "Verify NordRelay CLI", nordrelay.command, ["version"], { cwd: os.homedir(), shell: nordrelay.shell });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function runLoggedStep(log, label, command, args, settings = {}) {
|
|
437
|
+
logUpdateLine(log, `${label}: ${formatCommand(command, args)}`);
|
|
438
|
+
console.log(`\n${label}`);
|
|
439
|
+
const useShell = Boolean(settings.shell);
|
|
440
|
+
const child = spawn(useShell ? formatShellCommand(command, args) : command, useShell ? [] : args, {
|
|
441
|
+
cwd: settings.cwd || RUNTIME_ROOT,
|
|
442
|
+
env: process.env,
|
|
443
|
+
shell: useShell,
|
|
444
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
445
|
+
windowsHide: false,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
child.stdout?.on("data", (chunk) => {
|
|
449
|
+
safeWrite(process.stdout, chunk);
|
|
450
|
+
safeWrite(log, chunk);
|
|
451
|
+
});
|
|
452
|
+
child.stderr?.on("data", (chunk) => {
|
|
453
|
+
safeWrite(process.stderr, chunk);
|
|
454
|
+
safeWrite(log, chunk);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const exit = await new Promise((resolve, reject) => {
|
|
458
|
+
child.once("error", reject);
|
|
459
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (exit.signal) {
|
|
463
|
+
throw new Error(`${label} stopped with signal ${exit.signal}`);
|
|
464
|
+
}
|
|
465
|
+
if (exit.code !== 0) {
|
|
466
|
+
throw new Error(`${label} failed with exit code ${exit.code ?? "unknown"}`);
|
|
467
|
+
}
|
|
468
|
+
logUpdateLine(log, `${label} completed`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function resolveRequiredCommand(command) {
|
|
472
|
+
const resolved = findExecutable(command);
|
|
473
|
+
if (!resolved) {
|
|
474
|
+
throw new Error(`${command} was not found on PATH.`);
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
command: resolved,
|
|
478
|
+
shell: isWindowsShellScript(resolved),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function resolveNpmSpawnCommand(env = process.env) {
|
|
483
|
+
const npmExecPath = env.npm_execpath?.trim();
|
|
484
|
+
if (npmExecPath && fs.existsSync(npmExecPath)) {
|
|
485
|
+
return {
|
|
486
|
+
command: process.execPath,
|
|
487
|
+
argsPrefix: [npmExecPath],
|
|
488
|
+
shell: false,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const pathMatch = findExecutable("npm", env.PATH);
|
|
493
|
+
if (pathMatch) {
|
|
494
|
+
return {
|
|
495
|
+
command: pathMatch,
|
|
496
|
+
argsPrefix: [],
|
|
497
|
+
shell: isWindowsShellScript(pathMatch),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
for (const candidate of commonNpmCandidates(env)) {
|
|
502
|
+
if (!fs.existsSync(candidate)) continue;
|
|
503
|
+
return {
|
|
504
|
+
command: candidate,
|
|
505
|
+
argsPrefix: [],
|
|
506
|
+
shell: isWindowsShellScript(candidate),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function commonNpmCandidates(env) {
|
|
513
|
+
const names = process.platform === "win32" ? ["npm.cmd", "npm.bat", "npm"] : ["npm"];
|
|
514
|
+
const directories = [
|
|
515
|
+
path.dirname(process.execPath),
|
|
516
|
+
env.APPDATA ? path.join(env.APPDATA, "npm") : undefined,
|
|
517
|
+
env.ProgramFiles ? path.join(env.ProgramFiles, "nodejs") : undefined,
|
|
518
|
+
env["ProgramFiles(x86)"] ? path.join(env["ProgramFiles(x86)"], "nodejs") : undefined,
|
|
519
|
+
].filter(Boolean);
|
|
520
|
+
return directories.flatMap((directory) => names.map((name) => path.join(directory, name)));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function logUpdateLine(log, message) {
|
|
524
|
+
safeWrite(log, `[${nowIso()}] ${message}\n`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function safeWrite(stream, chunk) {
|
|
528
|
+
try {
|
|
529
|
+
stream.write(chunk);
|
|
530
|
+
} catch {
|
|
531
|
+
// Logging must not break the updater if stdout/stderr disappears.
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function closeLogStream(log) {
|
|
536
|
+
return new Promise((resolve) => {
|
|
537
|
+
log.end(resolve);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function formatCommand(command, args) {
|
|
542
|
+
return [command, ...args].map((part) => {
|
|
543
|
+
const text = String(part);
|
|
544
|
+
return /[\s"'$`\\]/.test(text) ? JSON.stringify(text) : text;
|
|
545
|
+
}).join(" ");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function formatShellCommand(command, args) {
|
|
549
|
+
return [command, ...args].map(quoteShellArg).join(" ");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function quoteShellArg(value) {
|
|
553
|
+
if (process.platform === "win32") {
|
|
554
|
+
return quoteWindowsCmdArg(String(value));
|
|
555
|
+
}
|
|
556
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function quoteWindowsCmdArg(value) {
|
|
560
|
+
if (value.length === 0) {
|
|
561
|
+
return "\"\"";
|
|
562
|
+
}
|
|
563
|
+
if (!/[\s"&|<>()^%]/.test(value)) {
|
|
564
|
+
return value;
|
|
565
|
+
}
|
|
566
|
+
return `"${value
|
|
567
|
+
.replace(/%/g, "%%")
|
|
568
|
+
.replace(/(\\*)"/g, '$1$1\\"')
|
|
569
|
+
.replace(/(\\+)$/g, "$1$1")}"`;
|
|
570
|
+
}
|
|
571
|
+
|
|
329
572
|
async function commandInit(options) {
|
|
330
573
|
await mkdirp(options.home);
|
|
331
574
|
const envPath = path.join(options.home, "nordrelay.env");
|
|
@@ -858,17 +1101,26 @@ function check(name, ok, detail, status = "fail") {
|
|
|
858
1101
|
};
|
|
859
1102
|
}
|
|
860
1103
|
|
|
861
|
-
function findExecutable(command) {
|
|
1104
|
+
function findExecutable(command, pathValue = process.env.PATH, pathextValue = process.env.PATHEXT) {
|
|
862
1105
|
if (!command) return null;
|
|
863
1106
|
if (command.includes(path.sep) && fs.existsSync(command)) return command;
|
|
864
|
-
const paths = (
|
|
1107
|
+
const paths = (pathValue || "").split(path.delimiter);
|
|
1108
|
+
const extensions = process.platform === "win32"
|
|
1109
|
+
? ["", ...(pathextValue || ".COM;.EXE;.BAT;.CMD").split(";")]
|
|
1110
|
+
: [""];
|
|
865
1111
|
for (const dir of paths) {
|
|
866
|
-
const
|
|
867
|
-
|
|
1112
|
+
for (const extension of extensions) {
|
|
1113
|
+
const candidate = path.join(dir, `${command}${extension}`);
|
|
1114
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
1115
|
+
}
|
|
868
1116
|
}
|
|
869
1117
|
return null;
|
|
870
1118
|
}
|
|
871
1119
|
|
|
1120
|
+
function isWindowsShellScript(filePath) {
|
|
1121
|
+
return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(filePath);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
872
1124
|
function validateStateBackend() {
|
|
873
1125
|
const backend = process.env.NORDRELAY_STATE_BACKEND || "json";
|
|
874
1126
|
if (backend === "json") return { ok: true, detail: "NORDRELAY_STATE_BACKEND=json" };
|
|
@@ -920,6 +1172,7 @@ async function main() {
|
|
|
920
1172
|
if (options.command === "init") return commandInit(options);
|
|
921
1173
|
if (options.command === "user") return commandUser(options);
|
|
922
1174
|
if (options.command === "doctor") return commandDoctor(options);
|
|
1175
|
+
if (options.command === "update") return commandUpdate(options);
|
|
923
1176
|
if (options.command === "web" || options.command === "dashboard") return commandWeb(options);
|
|
924
1177
|
if (options.command === "restart") {
|
|
925
1178
|
await commandStop(options);
|
|
@@ -932,7 +1185,7 @@ async function main() {
|
|
|
932
1185
|
}
|
|
933
1186
|
|
|
934
1187
|
console.error(`Unknown command: ${options.command}`);
|
|
935
|
-
console.error("Usage: nordrelay [init|user|doctor|web|start|stop|restart|status|foreground|version]");
|
|
1188
|
+
console.error("Usage: nordrelay [init|user|doctor|web|start|stop|restart|status|update|foreground|version]");
|
|
936
1189
|
process.exitCode = 2;
|
|
937
1190
|
}
|
|
938
1191
|
|