@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.
Files changed (44) hide show
  1. package/.env.example +2 -0
  2. package/README.md +23 -14
  3. package/dist/access-control.js +2 -0
  4. package/dist/agent-updates.js +61 -10
  5. package/dist/bot-ui.js +1 -0
  6. package/dist/bot.js +142 -1065
  7. package/dist/channel-actions.js +8 -8
  8. package/dist/codex-cli.js +1 -1
  9. package/dist/config-metadata.js +2 -0
  10. package/dist/operations.js +233 -122
  11. package/dist/relay-artifact-service.js +126 -0
  12. package/dist/relay-external-activity-monitor.js +216 -0
  13. package/dist/relay-queue-service.js +66 -0
  14. package/dist/relay-runtime-types.js +1 -0
  15. package/dist/relay-runtime.js +119 -371
  16. package/dist/state-backend.js +3 -0
  17. package/dist/support-bundle.js +221 -0
  18. package/dist/telegram-agent-commands.js +212 -0
  19. package/dist/telegram-artifact-commands.js +139 -0
  20. package/dist/telegram-command-menu.js +1 -0
  21. package/dist/telegram-command-types.js +1 -0
  22. package/dist/telegram-diagnostics-command.js +102 -0
  23. package/dist/telegram-general-commands.js +52 -0
  24. package/dist/telegram-operational-commands.js +153 -0
  25. package/dist/telegram-preference-commands.js +198 -0
  26. package/dist/telegram-queue-commands.js +278 -0
  27. package/dist/telegram-support-command.js +53 -0
  28. package/dist/telegram-update-commands.js +6 -1
  29. package/dist/web-api-contract.js +79 -31
  30. package/dist/web-api-types.js +1 -0
  31. package/dist/web-dashboard-access-routes.js +163 -0
  32. package/dist/web-dashboard-artifact-routes.js +65 -0
  33. package/dist/web-dashboard-assets.js +2 -0
  34. package/dist/web-dashboard-http.js +143 -0
  35. package/dist/web-dashboard-pages.js +257 -0
  36. package/dist/web-dashboard-runtime-routes.js +92 -0
  37. package/dist/web-dashboard-session-routes.js +209 -0
  38. package/dist/web-dashboard.js +44 -882
  39. package/dist/webui-assets/dashboard.css +74 -4
  40. package/dist/webui-assets/dashboard.js +163 -24
  41. package/dist/zip-writer.js +83 -0
  42. package/package.json +10 -4
  43. package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
  44. 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.0",
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
- "build": "node scripts/clean-dist.mjs && tsc && node scripts/build-web-assets.mjs",
43
- "check": "node --check plugins/nordrelay/scripts/nordrelay.mjs && tsc --noEmit && node scripts/build-web-assets.mjs --check && node --import tsx scripts/generate-env-example.mjs --check",
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.0",
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 = (process.env.PATH || "").split(path.delimiter);
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 candidate = path.join(dir, command);
867
- if (fs.existsSync(candidate)) return candidate;
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