@nordbyte/nordrelay 0.5.1 → 0.6.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.
Files changed (57) hide show
  1. package/.env.example +65 -11
  2. package/README.md +97 -23
  3. package/dist/access-control.js +1 -0
  4. package/dist/activity-events.js +44 -0
  5. package/dist/agent-updates.js +18 -2
  6. package/dist/audit-log.js +40 -2
  7. package/dist/bot-rendering.js +10 -7
  8. package/dist/bot.js +492 -7
  9. package/dist/channel-actions.js +7 -2
  10. package/dist/channel-adapter.js +34 -7
  11. package/dist/channel-command-service.js +156 -0
  12. package/dist/channel-turn-service.js +237 -0
  13. package/dist/codex-cli.js +1 -1
  14. package/dist/config-metadata.js +80 -13
  15. package/dist/config.js +77 -7
  16. package/dist/context-key.js +77 -5
  17. package/dist/discord-artifacts.js +165 -0
  18. package/dist/discord-bot.js +2014 -0
  19. package/dist/discord-channel-runtime.js +133 -0
  20. package/dist/discord-command-surface.js +119 -0
  21. package/dist/discord-rate-limit.js +141 -0
  22. package/dist/index.js +16 -5
  23. package/dist/job-store.js +127 -0
  24. package/dist/metrics.js +41 -0
  25. package/dist/operations.js +176 -119
  26. package/dist/relay-external-activity-monitor.js +47 -6
  27. package/dist/relay-runtime.js +1003 -268
  28. package/dist/runtime-cache.js +57 -0
  29. package/dist/session-locks.js +10 -7
  30. package/dist/state-backend.js +3 -0
  31. package/dist/support-bundle.js +18 -1
  32. package/dist/telegram-access-commands.js +15 -2
  33. package/dist/telegram-access-middleware.js +16 -3
  34. package/dist/telegram-agent-commands.js +25 -0
  35. package/dist/telegram-artifact-commands.js +46 -0
  36. package/dist/telegram-diagnostics-command.js +5 -50
  37. package/dist/telegram-general-commands.js +2 -6
  38. package/dist/telegram-operational-commands.js +14 -6
  39. package/dist/telegram-queue-commands.js +74 -4
  40. package/dist/telegram-support-command.js +7 -0
  41. package/dist/telegram-update-commands.js +27 -0
  42. package/dist/user-management.js +208 -0
  43. package/dist/web-api-contract.js +9 -0
  44. package/dist/web-dashboard-access-routes.js +74 -1
  45. package/dist/web-dashboard-artifact-routes.js +3 -3
  46. package/dist/web-dashboard-assets.js +2 -0
  47. package/dist/web-dashboard-pages.js +97 -13
  48. package/dist/web-dashboard-runtime-routes.js +53 -8
  49. package/dist/web-dashboard-session-routes.js +27 -20
  50. package/dist/web-dashboard-ui.js +1 -0
  51. package/dist/web-dashboard.js +149 -6
  52. package/dist/web-state.js +33 -2
  53. package/dist/webui-assets/dashboard.css +75 -1
  54. package/dist/webui-assets/dashboard.js +358 -47
  55. package/package.json +3 -1
  56. package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
  57. package/plugins/nordrelay/scripts/nordrelay.mjs +468 -22
@@ -1,5 +1,5 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { readFile, stat } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
@@ -21,6 +21,8 @@ const CLAUDE_CODE_SDK_PACKAGE_NAME = "@anthropic-ai/claude-agent-sdk";
21
21
  const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
22
22
  const SECRET_RE = /(bot|token|api[_-]?key|authorization|bearer|password|secret)(["'=: ]+)([^\s"',]+)/gi;
23
23
  const DEFAULT_VERSION_CACHE_TTL_MS = 60 * 60 * 1000;
24
+ const DEFAULT_CLI_VERSION_CACHE_TTL_MS = 60 * 1000;
25
+ const cliVersionCache = new Map();
24
26
  export function getConnectorHome() {
25
27
  return process.env.NORDRELAY_HOME || DEFAULT_HOME;
26
28
  }
@@ -96,97 +98,86 @@ export async function getPackageVersion() {
96
98
  }
97
99
  }
98
100
  export async function getVersionChecks(options = {}) {
99
- const nordrelayVersion = await getPackageVersion();
100
- const codexCli = resolveCodexCli();
101
- const piCli = resolvePiCli(process.env, options.piCliPath);
102
- const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
103
- const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
104
- const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
105
- const codexVersionLabel = codexCli.path
106
- ? detectCliVersion(codexCli.path)
107
- : readInstalledPackageVersion(CODEX_PACKAGE_NAME) ?? "not installed";
108
- const piVersionLabel = piCli.path
109
- ? detectCliVersion(piCli.path)
110
- : readInstalledPackageVersion(PI_PACKAGE_NAME) ?? readInstalledPackageVersion(LEGACY_PI_PACKAGE_NAME) ?? "not installed";
111
- const legacyPiPackageVersion = readInstalledPackageVersion(LEGACY_PI_PACKAGE_NAME);
112
- const hermesVersionLabel = hermesCli.path ? detectCliVersion(hermesCli.path) : "not installed";
113
- const openClawVersionLabel = openClawCli.path ? detectCliVersion(openClawCli.path) : "not installed";
114
- const claudeCodeVersionLabel = claudeCodeCli.path
115
- ? detectCliVersion(claudeCodeCli.path)
116
- : readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled";
117
- const claudeCodePackageName = claudeCodeCli.path ? CLAUDE_CODE_PACKAGE_NAME : CLAUDE_CODE_SDK_PACKAGE_NAME;
118
- return {
119
- nordrelay: buildVersionCheck({
101
+ const [nordrelayVersion, cliVersions] = await Promise.all([
102
+ getPackageVersion(),
103
+ resolveAgentCliVersions(options),
104
+ ]);
105
+ const [nordrelay, codex, pi, hermes, openclaw, claudeCode,] = await Promise.all([
106
+ buildVersionCheck({
120
107
  label: "NordRelay",
121
108
  packageName: PACKAGE_NAME,
122
109
  installedLabel: nordrelayVersion,
123
110
  installedVersion: extractVersion(nordrelayVersion),
124
111
  }),
125
- codex: buildVersionCheck({
112
+ buildVersionCheck({
126
113
  label: "Codex",
127
114
  packageName: CODEX_PACKAGE_NAME,
128
- installedLabel: codexVersionLabel,
129
- installedVersion: extractVersion(codexVersionLabel),
130
- notInstalled: codexVersionLabel === "not installed",
115
+ installedLabel: cliVersions.codexVersionLabel,
116
+ installedVersion: extractVersion(cliVersions.codexVersionLabel),
117
+ notInstalled: cliVersions.codexVersionLabel === "not installed",
131
118
  }),
132
- pi: buildVersionCheck({
119
+ buildVersionCheck({
133
120
  label: "Pi",
134
121
  packageName: PI_PACKAGE_NAME,
135
- installedLabel: piVersionLabel,
136
- installedVersion: extractVersion(piVersionLabel),
137
- notInstalled: piVersionLabel === "not installed",
138
- detail: legacyPiPackageVersion ? `Legacy package ${LEGACY_PI_PACKAGE_NAME} is present; current package is ${PI_PACKAGE_NAME}.` : undefined,
122
+ installedLabel: cliVersions.piVersionLabel,
123
+ installedVersion: extractVersion(cliVersions.piVersionLabel),
124
+ notInstalled: cliVersions.piVersionLabel === "not installed",
125
+ detail: cliVersions.legacyPiPackageVersion ? `Legacy package ${LEGACY_PI_PACKAGE_NAME} is present; current package is ${PI_PACKAGE_NAME}.` : undefined,
139
126
  }),
140
- hermes: buildHermesVersionCheck(hermesVersionLabel),
141
- openclaw: buildVersionCheck({
127
+ buildHermesVersionCheck(cliVersions.hermesVersionLabel),
128
+ buildVersionCheck({
142
129
  label: "OpenClaw",
143
130
  packageName: OPENCLAW_PACKAGE_NAME,
144
- installedLabel: openClawVersionLabel,
145
- installedVersion: extractVersion(openClawVersionLabel),
146
- notInstalled: openClawVersionLabel === "not installed",
131
+ installedLabel: cliVersions.openClawVersionLabel,
132
+ installedVersion: extractVersion(cliVersions.openClawVersionLabel),
133
+ notInstalled: cliVersions.openClawVersionLabel === "not installed",
147
134
  }),
148
- claudeCode: buildVersionCheck({
135
+ buildVersionCheck({
149
136
  label: "Claude Code",
150
- packageName: claudeCodePackageName,
151
- installedLabel: claudeCodeVersionLabel,
152
- installedVersion: extractVersion(claudeCodeVersionLabel),
153
- notInstalled: claudeCodeVersionLabel === "not installed",
137
+ packageName: cliVersions.claudeCodePackageName,
138
+ installedLabel: cliVersions.claudeCodeVersionLabel,
139
+ installedVersion: extractVersion(cliVersions.claudeCodeVersionLabel),
140
+ notInstalled: cliVersions.claudeCodeVersionLabel === "not installed",
154
141
  }),
142
+ ]);
143
+ return {
144
+ nordrelay,
145
+ codex,
146
+ pi,
147
+ hermes,
148
+ openclaw,
149
+ claudeCode,
155
150
  };
156
151
  }
157
152
  export async function getConnectorHealth(options = {}) {
158
- const rawState = await readConnectorState();
159
- const version = await getPackageVersion();
153
+ const [rawState, version, cliVersions] = await Promise.all([
154
+ readConnectorState(),
155
+ getPackageVersion(),
156
+ resolveAgentCliVersions(options),
157
+ ]);
160
158
  const pidRunning = isProcessRunning(rawState.pid);
161
159
  const appPidRunning = isProcessRunning(rawState.appPid);
162
160
  const state = normalizeConnectorState(rawState, pidRunning, appPidRunning);
163
- const codexCli = resolveCodexCli();
164
- const piCli = resolvePiCli(process.env, options.piCliPath);
165
- const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
166
- const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
167
- const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
168
161
  return {
169
162
  version,
170
163
  state,
171
164
  pidRunning,
172
165
  appPidRunning,
173
- codexCli: describeCodexCli(codexCli),
174
- codexCliPath: codexCli.path ?? null,
175
- codexCliVersion: detectCliVersion(codexCli.path),
176
- piCli: describePiCli(piCli),
177
- piCliPath: piCli.path ?? null,
178
- piCliVersion: detectCliVersion(piCli.path),
179
- hermesCli: describeHermesCli(hermesCli),
180
- hermesCliPath: hermesCli.path ?? null,
181
- hermesCliVersion: detectCliVersion(hermesCli.path),
182
- openClawCli: describeOpenClawCli(openClawCli),
183
- openClawCliPath: openClawCli.path ?? null,
184
- openClawCliVersion: detectCliVersion(openClawCli.path),
185
- claudeCodeCli: describeClaudeCodeCli(claudeCodeCli),
186
- claudeCodeCliPath: claudeCodeCli.path ?? null,
187
- claudeCodeCliVersion: claudeCodeCli.path
188
- ? detectCliVersion(claudeCodeCli.path)
189
- : readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled",
166
+ codexCli: describeCodexCli(cliVersions.codexCli),
167
+ codexCliPath: cliVersions.codexCli.path ?? null,
168
+ codexCliVersion: cliVersions.codexVersionLabel,
169
+ piCli: describePiCli(cliVersions.piCli),
170
+ piCliPath: cliVersions.piCli.path ?? null,
171
+ piCliVersion: cliVersions.piVersionLabel,
172
+ hermesCli: describeHermesCli(cliVersions.hermesCli),
173
+ hermesCliPath: cliVersions.hermesCli.path ?? null,
174
+ hermesCliVersion: cliVersions.hermesVersionLabel,
175
+ openClawCli: describeOpenClawCli(cliVersions.openClawCli),
176
+ openClawCliPath: cliVersions.openClawCli.path ?? null,
177
+ openClawCliVersion: cliVersions.openClawVersionLabel,
178
+ claudeCodeCli: describeClaudeCodeCli(cliVersions.claudeCodeCli),
179
+ claudeCodeCliPath: cliVersions.claudeCodeCli.path ?? null,
180
+ claudeCodeCliVersion: cliVersions.claudeCodeVersionLabel,
190
181
  stateFile: getConnectorStatePath(),
191
182
  logFile: getConnectorLogPath(),
192
183
  databasePath: findLatestDatabase(),
@@ -208,23 +199,22 @@ export function spawnSelfUpdate() {
208
199
  const script = getWrapperScriptPath();
209
200
  const updateLog = getUpdateLogPath();
210
201
  const method = detectSelfUpdateMethod(sourceRoot);
211
- const commands = method === "npm"
212
- ? buildNpmSelfUpdateCommands()
213
- : buildGitSelfUpdateCommands(script);
214
- const logFd = openSync(updateLog, "a");
215
- const command = [
216
- "set -e",
217
- `printf '\\n[%s] Starting ${method} connector self-update\\n' "$(date -Is)"`,
218
- ...commands,
219
- ].join(" && ");
220
- const child = spawn("sh", ["-lc", command], {
202
+ mkdirSync(path.dirname(updateLog), { recursive: true });
203
+ const child = spawn(process.execPath, [
204
+ script,
205
+ "update",
206
+ "--method",
207
+ method,
208
+ "--home",
209
+ getConnectorHome(),
210
+ "--keep-pending-updates",
211
+ ], {
221
212
  cwd: sourceRoot,
222
213
  detached: true,
223
214
  env: process.env,
224
- stdio: ["ignore", logFd, logFd],
215
+ stdio: "ignore",
225
216
  });
226
217
  child.unref();
227
- closeSync(logFd);
228
218
  return {
229
219
  logPath: updateLog,
230
220
  method,
@@ -274,41 +264,62 @@ function normalizeConnectorState(state, pidRunning, appPidRunning) {
274
264
  function redactSecrets(text) {
275
265
  return text.replace(SECRET_RE, "$1$2[redacted]");
276
266
  }
277
- function buildGitSelfUpdateCommands(script) {
278
- return [
279
- "git pull --ff-only origin main",
280
- "npm install",
281
- "npm run check",
282
- "npm test",
283
- "npm run build",
284
- `printf '[%s] Checks passed; restarting connector\\n' "$(date -Is)"`,
285
- `${shellQuote(process.execPath)} ${shellQuote(script)} restart --keep-pending-updates`,
286
- ];
287
- }
288
- function buildNpmSelfUpdateCommands() {
289
- return [
290
- `${resolveNpmCommand()} install -g ${PACKAGE_NAME}@latest`,
291
- "nordrelay version",
292
- `printf '[%s] npm update finished; restarting connector\\n' "$(date -Is)"`,
293
- "nordrelay restart --keep-pending-updates",
294
- ];
295
- }
296
- function resolveNpmCommand() {
297
- const npm = resolveNpmSpawnCommand();
298
- if (npm) {
299
- return [npm.command, ...npm.argsPrefix].map(shellQuote).join(" ");
300
- }
301
- return "npm";
267
+ async function resolveAgentCliVersions(options = {}) {
268
+ const codexCli = resolveCodexCli();
269
+ const piCli = resolvePiCli(process.env, options.piCliPath);
270
+ const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
271
+ const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
272
+ const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
273
+ const legacyPiPackageVersion = readInstalledPackageVersion(LEGACY_PI_PACKAGE_NAME);
274
+ const [codexVersionLabel, piVersionLabel, hermesVersionLabel, openClawVersionLabel, claudeCodeVersionLabel,] = await Promise.all([
275
+ codexCli.path ? detectCliVersion(codexCli.path) : Promise.resolve(readInstalledPackageVersion(CODEX_PACKAGE_NAME) ?? "not installed"),
276
+ piCli.path ? detectCliVersion(piCli.path) : Promise.resolve(readInstalledPackageVersion(PI_PACKAGE_NAME) ?? legacyPiPackageVersion ?? "not installed"),
277
+ hermesCli.path ? detectCliVersion(hermesCli.path) : Promise.resolve("not installed"),
278
+ openClawCli.path ? detectCliVersion(openClawCli.path) : Promise.resolve("not installed"),
279
+ claudeCodeCli.path ? detectCliVersion(claudeCodeCli.path) : Promise.resolve(readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled"),
280
+ ]);
281
+ return {
282
+ codexCli,
283
+ piCli,
284
+ hermesCli,
285
+ openClawCli,
286
+ claudeCodeCli,
287
+ codexVersionLabel,
288
+ piVersionLabel,
289
+ hermesVersionLabel,
290
+ openClawVersionLabel,
291
+ claudeCodeVersionLabel,
292
+ claudeCodePackageName: claudeCodeCli.path ? CLAUDE_CODE_PACKAGE_NAME : CLAUDE_CODE_SDK_PACKAGE_NAME,
293
+ legacyPiPackageVersion,
294
+ };
302
295
  }
303
- function detectCliVersion(commandPath) {
296
+ async function detectCliVersion(commandPath) {
304
297
  if (!commandPath) {
305
298
  return "not installed";
306
299
  }
307
- const result = spawnSync(commandPath, ["--version"], {
308
- encoding: "utf8",
300
+ const ttlMs = parseCliVersionCacheTtlMs();
301
+ if (ttlMs > 0) {
302
+ const cached = cliVersionCache.get(commandPath);
303
+ if (cached && Date.now() < cached.expiresAt) {
304
+ if (cached.value !== undefined) {
305
+ return cached.value;
306
+ }
307
+ if (cached.promise) {
308
+ return cached.promise;
309
+ }
310
+ }
311
+ const promise = detectCliVersionUncached(commandPath);
312
+ cliVersionCache.set(commandPath, { promise, expiresAt: Date.now() + ttlMs });
313
+ const value = await promise;
314
+ cliVersionCache.set(commandPath, { value, expiresAt: Date.now() + ttlMs });
315
+ return value;
316
+ }
317
+ return detectCliVersionUncached(commandPath);
318
+ }
319
+ async function detectCliVersionUncached(commandPath) {
320
+ const result = await runCommand(commandPath, ["--version"], {
309
321
  shell: isWindowsShellScript(commandPath),
310
322
  timeout: 3000,
311
- windowsHide: true,
312
323
  });
313
324
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
314
325
  if (result.error) {
@@ -319,9 +330,9 @@ function detectCliVersion(commandPath) {
319
330
  }
320
331
  return output || "unknown";
321
332
  }
322
- function buildHermesVersionCheck(installedLabel) {
333
+ async function buildHermesVersionCheck(installedLabel) {
323
334
  if (installedLabel === "not installed") {
324
- const latest = detectLatestNpmVersion(HERMES_PACKAGE_NAME);
335
+ const latest = await detectLatestNpmVersion(HERMES_PACKAGE_NAME);
325
336
  return {
326
337
  label: "Hermes",
327
338
  packageName: HERMES_PACKAGE_NAME,
@@ -346,9 +357,9 @@ function buildHermesVersionCheck(installedLabel) {
346
357
  detail: updateLine ?? (installedVersion ? undefined : "Could not parse Hermes version or update status"),
347
358
  };
348
359
  }
349
- function buildVersionCheck(options) {
360
+ async function buildVersionCheck(options) {
350
361
  if (options.notInstalled) {
351
- const latest = options.skipLatest ? { version: null, error: undefined } : detectLatestNpmVersion(options.packageName);
362
+ const latest = options.skipLatest ? { version: null, error: undefined } : await detectLatestNpmVersion(options.packageName);
352
363
  return {
353
364
  label: options.label,
354
365
  packageName: options.packageName,
@@ -370,7 +381,7 @@ function buildVersionCheck(options) {
370
381
  detail: options.detail ?? "Latest-version lookup is not available for this package source",
371
382
  };
372
383
  }
373
- const latest = detectLatestNpmVersion(options.packageName);
384
+ const latest = await detectLatestNpmVersion(options.packageName);
374
385
  if (!options.installedVersion || !latest.version) {
375
386
  return {
376
387
  label: options.label,
@@ -392,7 +403,7 @@ function buildVersionCheck(options) {
392
403
  detail: [options.detail, latest.error].filter(Boolean).join(" ") || undefined,
393
404
  };
394
405
  }
395
- function detectLatestNpmVersion(packageName) {
406
+ async function detectLatestNpmVersion(packageName) {
396
407
  const cached = readVersionCache(packageName);
397
408
  if (cached) {
398
409
  return cached;
@@ -401,11 +412,9 @@ function detectLatestNpmVersion(packageName) {
401
412
  if (!npm) {
402
413
  return { version: null, error: "npm was not found on PATH; latest-version lookup is unavailable" };
403
414
  }
404
- const result = spawnSync(npm.command, [...npm.argsPrefix, "view", packageName, "version", "--registry=https://registry.npmjs.org"], {
405
- encoding: "utf8",
415
+ const result = await runCommand(npm.command, [...npm.argsPrefix, "view", packageName, "version", "--registry=https://registry.npmjs.org"], {
406
416
  shell: npm.shell,
407
417
  timeout: 5000,
408
- windowsHide: true,
409
418
  });
410
419
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
411
420
  if (result.error) {
@@ -418,6 +427,49 @@ function detectLatestNpmVersion(packageName) {
418
427
  writeVersionCache(packageName, resolved.version);
419
428
  return resolved;
420
429
  }
430
+ function runCommand(command, args, options) {
431
+ return new Promise((resolve) => {
432
+ const useShell = Boolean(options.shell);
433
+ execFile(useShell ? formatShellCommand(command, args) : command, useShell ? [] : args, {
434
+ encoding: "utf8",
435
+ shell: useShell,
436
+ timeout: options.timeout,
437
+ windowsHide: true,
438
+ env: process.env,
439
+ maxBuffer: 1024 * 1024,
440
+ }, (error, stdout, stderr) => {
441
+ const enriched = error;
442
+ resolve({
443
+ stdout: typeof stdout === "string" ? stdout : "",
444
+ stderr: typeof stderr === "string" ? stderr : "",
445
+ status: typeof enriched?.code === "number" ? enriched.code : error ? 1 : 0,
446
+ signal: enriched?.signal,
447
+ error: enriched ?? undefined,
448
+ });
449
+ });
450
+ });
451
+ }
452
+ function formatShellCommand(command, args) {
453
+ return [command, ...args].map(quoteShellArg).join(" ");
454
+ }
455
+ function quoteShellArg(value) {
456
+ if (process.platform === "win32") {
457
+ return quoteWindowsCmdArg(value);
458
+ }
459
+ return `'${value.replace(/'/g, `'\\''`)}'`;
460
+ }
461
+ function quoteWindowsCmdArg(value) {
462
+ if (value.length === 0) {
463
+ return "\"\"";
464
+ }
465
+ if (!/[\s"&|<>()^%]/.test(value)) {
466
+ return value;
467
+ }
468
+ return `"${value
469
+ .replace(/%/g, "%%")
470
+ .replace(/(\\*)"/g, '$1$1\\"')
471
+ .replace(/(\\+)$/g, "$1$1")}"`;
472
+ }
421
473
  export function resolveNpmSpawnCommand(env = process.env) {
422
474
  const npmExecPath = env.npm_execpath?.trim();
423
475
  if (npmExecPath && existsSync(npmExecPath)) {
@@ -512,6 +564,14 @@ function parseVersionCacheTtlMs() {
512
564
  const parsed = Number(raw);
513
565
  return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : DEFAULT_VERSION_CACHE_TTL_MS;
514
566
  }
567
+ function parseCliVersionCacheTtlMs() {
568
+ const raw = process.env.NORDRELAY_CLI_VERSION_CACHE_TTL_MS;
569
+ if (!raw) {
570
+ return DEFAULT_CLI_VERSION_CACHE_TTL_MS;
571
+ }
572
+ const parsed = Number(raw);
573
+ return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : DEFAULT_CLI_VERSION_CACHE_TTL_MS;
574
+ }
515
575
  function readInstalledPackageVersion(packageName) {
516
576
  try {
517
577
  const packagePath = path.join(getSourceRoot(), "node_modules", ...packageName.split("/"), "package.json");
@@ -543,9 +603,6 @@ function compareVersions(left, right) {
543
603
  function parseVersionParts(value) {
544
604
  return value.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
545
605
  }
546
- function shellQuote(value) {
547
- return `'${value.replace(/'/g, `'\\''`)}'`;
548
- }
549
606
  function formatLogLine(line) {
550
607
  const trimmed = line.trim();
551
608
  if (!trimmed) {
@@ -2,6 +2,10 @@ import {} from "./agent.js";
2
2
  import { getExternalSnapshotForSession } from "./agent-activity.js";
3
3
  import { friendlyErrorText } from "./error-messages.js";
4
4
  import {} from "./web-state.js";
5
+ const CLI_ACTIVITY_ACTOR = {
6
+ channel: "cli",
7
+ label: "CLI",
8
+ };
5
9
  export class RelayExternalActivityMonitor {
6
10
  options;
7
11
  mirror = null;
@@ -16,7 +20,9 @@ export class RelayExternalActivityMonitor {
16
20
  if (!this.mirror) {
17
21
  return null;
18
22
  }
19
- const startedAt = this.mirror.startedAt ?? new Date().toISOString();
23
+ const startedAt = this.mirror.startedAt instanceof Date
24
+ ? this.mirror.startedAt.toISOString()
25
+ : this.mirror.startedAt ?? new Date().toISOString();
20
26
  const startedMs = new Date(startedAt).getTime();
21
27
  return {
22
28
  id: this.mirror.turnId ?? "cli",
@@ -75,7 +81,7 @@ export class RelayExternalActivityMonitor {
75
81
  startedAt: snapshot.activity.startedAt?.toISOString() ?? null,
76
82
  };
77
83
  if (snapshot.activity.active) {
78
- this.startExternalTurn(snapshot);
84
+ this.startExternalTurn(snapshot, info);
79
85
  }
80
86
  return;
81
87
  }
@@ -85,9 +91,9 @@ export class RelayExternalActivityMonitor {
85
91
  mirror.turnId = snapshot.activity.turnId;
86
92
  mirror.startedAt = snapshot.activity.startedAt?.toISOString() ?? null;
87
93
  mirror.latestAgentLine = undefined;
88
- this.startExternalTurn(snapshot);
94
+ this.startExternalTurn(snapshot, info);
89
95
  }
90
- this.broadcastExternalEvents(snapshot, snapshot.events.filter((event) => event.lineNumber > mirror.lastLine));
96
+ this.broadcastExternalEvents(snapshot, snapshot.events.filter((event) => event.lineNumber > mirror.lastLine), info);
91
97
  mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
92
98
  mirror.latestStatus = externalStatusLine(snapshot, this.options.queueLength());
93
99
  this.options.broadcastStatus(mirror.latestStatus, "info");
@@ -122,6 +128,7 @@ export class RelayExternalActivityMonitor {
122
128
  threadId: snapshot.threadId,
123
129
  workspace: info.workspace,
124
130
  agentId: info.agentId,
131
+ actor: CLI_ACTIVITY_ACTOR,
125
132
  prompt: snapshot.latestUserMessage ?? undefined,
126
133
  detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
127
134
  durationMs: durationFromDates(externalStartedAt, terminalEvent.timestamp),
@@ -136,7 +143,7 @@ export class RelayExternalActivityMonitor {
136
143
  }
137
144
  mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
138
145
  }
139
- startExternalTurn(snapshot) {
146
+ startExternalTurn(snapshot, info) {
140
147
  const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
141
148
  this.options.chatStore.append({
142
149
  threadId: snapshot.threadId,
@@ -158,11 +165,14 @@ export class RelayExternalActivityMonitor {
158
165
  status: "running",
159
166
  type: "cli_turn_started",
160
167
  threadId: snapshot.threadId,
168
+ workspace: info.workspace,
169
+ agentId: info.agentId,
170
+ actor: CLI_ACTIVITY_ACTOR,
161
171
  prompt,
162
172
  detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
163
173
  });
164
174
  }
165
- broadcastExternalEvents(snapshot, events) {
175
+ broadcastExternalEvents(snapshot, events, info) {
166
176
  for (const event of events) {
167
177
  if (event.kind === "tool" && event.status === "started") {
168
178
  this.options.broadcast({
@@ -176,6 +186,9 @@ export class RelayExternalActivityMonitor {
176
186
  status: "running",
177
187
  type: "cli_tool_started",
178
188
  threadId: snapshot.threadId,
189
+ workspace: info.workspace,
190
+ agentId: info.agentId,
191
+ actor: CLI_ACTIVITY_ACTOR,
179
192
  detail: event.toolName ?? "tool",
180
193
  });
181
194
  }
@@ -186,6 +199,34 @@ export class RelayExternalActivityMonitor {
186
199
  toolCallId: `cli-${event.lineNumber}`,
187
200
  isError: false,
188
201
  });
202
+ this.options.appendActivity({
203
+ source: "cli",
204
+ status: "completed",
205
+ type: "cli_tool_completed",
206
+ threadId: snapshot.threadId,
207
+ workspace: info.workspace,
208
+ agentId: info.agentId,
209
+ actor: CLI_ACTIVITY_ACTOR,
210
+ detail: event.toolName ?? "tool",
211
+ });
212
+ }
213
+ if (event.kind === "tool" && event.status === "failed") {
214
+ this.options.broadcast({
215
+ type: "tool_end",
216
+ id: snapshot.activity.turnId ?? "cli",
217
+ toolCallId: `cli-${event.lineNumber}`,
218
+ isError: true,
219
+ });
220
+ this.options.appendActivity({
221
+ source: "cli",
222
+ status: "failed",
223
+ type: "cli_tool_failed",
224
+ threadId: snapshot.threadId,
225
+ workspace: info.workspace,
226
+ agentId: info.agentId,
227
+ actor: CLI_ACTIVITY_ACTOR,
228
+ detail: event.toolName ?? "tool",
229
+ });
189
230
  }
190
231
  }
191
232
  }