@nordbyte/nordrelay 0.5.1 → 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 +7 -4
- package/dist/agent-updates.js +18 -2
- package/dist/bot.js +34 -2
- package/dist/codex-cli.js +1 -1
- package/dist/config-metadata.js +2 -0
- package/dist/operations.js +176 -119
- package/dist/relay-runtime.js +42 -12
- package/dist/state-backend.js +3 -0
- package/dist/support-bundle.js +17 -1
- package/dist/web-dashboard.js +1 -0
- package/package.json +1 -1
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +258 -5
package/.env.example
CHANGED
|
@@ -209,6 +209,8 @@ NORDRELAY_AUDIT_MAX_EVENTS=1000
|
|
|
209
209
|
NORDRELAY_SESSION_LOCK_TTL_MS=1800000
|
|
210
210
|
# NPM version cache TTL.
|
|
211
211
|
NORDRELAY_VERSION_CACHE_TTL_MS=3600000
|
|
212
|
+
# Installed agent CLI version cache TTL.
|
|
213
|
+
NORDRELAY_CLI_VERSION_CACHE_TTL_MS=60000
|
|
212
214
|
|
|
213
215
|
# Voice
|
|
214
216
|
# Optional voice transcription settings.
|
package/README.md
CHANGED
|
@@ -183,9 +183,9 @@ Authentication and safety:
|
|
|
183
183
|
Operations:
|
|
184
184
|
|
|
185
185
|
- Plugin command/skill starts, stops, restarts, and inspects the connector process.
|
|
186
|
-
- Manual process commands support `start`, `stop`, `restart`, `status`, and `foreground`.
|
|
186
|
+
- Manual process commands support `start`, `stop`, `restart`, `status`, `update`, and `foreground`.
|
|
187
187
|
- Telegram admin commands support `/logs`, `/diagnostics`, `/support`, `/restart`, and `/update` for NordRelay and agent CLIs.
|
|
188
|
-
- `/update
|
|
188
|
+
- `nordrelay update`, `/update`, and the WebUI update button detect the install type: npm installs update with `npm install -g @nordbyte/nordrelay@latest`; source checkouts pull `origin/main`, install dependencies, run check, tests, and build, then restart if the connector is running.
|
|
189
189
|
- `/update agents`, `/update <agent>`, `/update install <agent>`, `/update jobs`, `/update log <id>`, `/update cancel <id>`, and `/update input <id> <text>` manage Codex, Pi, Hermes, OpenClaw, and Claude Code updater or installer jobs from Telegram.
|
|
190
190
|
- `/logs` renders redacted connector, NordRelay update, and agent update logs with local-time timestamps, levels, file path, last-modified time, and highlighted warnings/errors.
|
|
191
191
|
- Logs can be emitted as timestamped plain text or JSON records with `CONNECTOR_LOG_FORMAT`.
|
|
@@ -199,7 +199,7 @@ Operations:
|
|
|
199
199
|
- The WebUI exposes REST and SSE endpoints for chat streaming, sessions, settings, queue, artifacts, logs, health, diagnostics, and redacted diagnostics bundle export.
|
|
200
200
|
- The dashboard can bind to `127.0.0.1` or `0.0.0.0`; user login and session cookies are mandatory in both modes.
|
|
201
201
|
- Telegram can run with long polling or an HTTP webhook via `TELEGRAM_TRANSPORT=webhook`.
|
|
202
|
-
- Version freshness checks are cached with `NORDRELAY_VERSION_CACHE_TTL_MS` to keep `/version` responsive.
|
|
202
|
+
- Version freshness checks are cached with `NORDRELAY_VERSION_CACHE_TTL_MS`, and installed agent CLI version checks are cached with `NORDRELAY_CLI_VERSION_CACHE_TTL_MS`, to keep `/version` and adapter health responsive.
|
|
203
203
|
- CI runs on Ubuntu, Windows, and macOS with typecheck, Vitest, Playwright WebUI browser tests, package dry run, npm audit, and a separate secret-scan workflow.
|
|
204
204
|
- `npm run dev`, `npm run build`, `npm run check`, `npm test`, `npm run test:e2e`, `npm start`, `npm stop`, and `npm run status` are available.
|
|
205
205
|
- Dockerfile and `docker-compose.yml` are included for containerized operation.
|
|
@@ -356,6 +356,7 @@ nordrelay init
|
|
|
356
356
|
nordrelay doctor
|
|
357
357
|
nordrelay start
|
|
358
358
|
nordrelay status
|
|
359
|
+
nordrelay update
|
|
359
360
|
nordrelay restart
|
|
360
361
|
nordrelay stop
|
|
361
362
|
nordrelay foreground
|
|
@@ -367,6 +368,7 @@ Source checkout process commands:
|
|
|
367
368
|
```bash
|
|
368
369
|
node plugins/nordrelay/scripts/nordrelay.mjs start
|
|
369
370
|
node plugins/nordrelay/scripts/nordrelay.mjs status
|
|
371
|
+
node plugins/nordrelay/scripts/nordrelay.mjs update
|
|
370
372
|
node plugins/nordrelay/scripts/nordrelay.mjs restart
|
|
371
373
|
node plugins/nordrelay/scripts/nordrelay.mjs stop
|
|
372
374
|
node plugins/nordrelay/scripts/nordrelay.mjs foreground
|
|
@@ -737,6 +739,7 @@ Agent selection:
|
|
|
737
739
|
- `NORDRELAY_AUDIT_MAX_EVENTS`: maximum audit events retained. Defaults to `1000`.
|
|
738
740
|
- `NORDRELAY_SESSION_LOCK_TTL_MS`: session write-lock TTL. Defaults to `1800000`.
|
|
739
741
|
- `NORDRELAY_VERSION_CACHE_TTL_MS`: npm version freshness cache TTL. Defaults to `3600000`; set `0` to disable.
|
|
742
|
+
- `NORDRELAY_CLI_VERSION_CACHE_TTL_MS`: installed agent CLI version cache TTL. Defaults to `60000`; set `0` to disable.
|
|
740
743
|
|
|
741
744
|
Dashboard:
|
|
742
745
|
|
|
@@ -837,7 +840,7 @@ NordRelay wrapper:
|
|
|
837
840
|
|
|
838
841
|
- `NORDRELAY_HOME`: config/state/log directory override. Defaults to `~/.nordrelay`.
|
|
839
842
|
- `NORDRELAY_SOURCE_ROOT`: runtime source root override. Useful when the plugin is launched from Codex cache.
|
|
840
|
-
- `NORDRELAY_UPDATE_METHOD`: optional `auto`, `npm`, or `git` self-update method override. Auto uses git when the runtime root has a `.git` directory and npm otherwise.
|
|
843
|
+
- `NORDRELAY_UPDATE_METHOD`: optional `auto`, `npm`, or `git` self-update method override used by `nordrelay update`, `/update`, and the WebUI update button. Auto uses git when the runtime root has a `.git` directory and npm otherwise.
|
|
841
844
|
- Agent updates from the dashboard and Telegram use each agent's native updater where possible: `codex update`, `pi update pi`, `hermes update --yes`, `openclaw update --yes`, and `claude update`. Not-installed agents can be installed from the dashboard or with `/update install <agent>` using npm global installs.
|
|
842
845
|
- `NORDRELAY_KEEP_PENDING_UPDATES`: set true to avoid dropping pending Telegram updates on start.
|
|
843
846
|
- `NORDRELAY_FORWARD_TOOL_OUTPUT`: backward-compatible alias that sets `TOOL_VERBOSITY=all` when `TOOL_VERBOSITY` is unset.
|
package/dist/agent-updates.js
CHANGED
|
@@ -105,10 +105,11 @@ export class AgentUpdateManager {
|
|
|
105
105
|
`Working directory: ${job.cwd}`,
|
|
106
106
|
"",
|
|
107
107
|
].join("\n"));
|
|
108
|
-
const
|
|
108
|
+
const useShell = process.platform === "win32";
|
|
109
|
+
const child = spawn(useShell ? formatShellCommand(plan.command, plan.args) : plan.command, useShell ? [] : plan.args, {
|
|
109
110
|
cwd: plan.cwd,
|
|
110
111
|
env: { ...process.env, ...(this.options.env ?? {}), ...(context.env ?? {}) },
|
|
111
|
-
shell:
|
|
112
|
+
shell: useShell,
|
|
112
113
|
windowsHide: true,
|
|
113
114
|
stdio: "pipe",
|
|
114
115
|
});
|
|
@@ -342,6 +343,21 @@ function looksLikePrompt(text) {
|
|
|
342
343
|
const tail = text.split(/\r?\n/).slice(-4).join("\n");
|
|
343
344
|
return /\b(y\/n|yes\/no|continue|proceed|confirm|password|passphrase|token|api key|enter|select)\b|[?>]\s*$/i.test(tail);
|
|
344
345
|
}
|
|
346
|
+
function formatShellCommand(command, args) {
|
|
347
|
+
return [command, ...args].map(quoteWindowsCmdArg).join(" ");
|
|
348
|
+
}
|
|
349
|
+
function quoteWindowsCmdArg(value) {
|
|
350
|
+
if (value.length === 0) {
|
|
351
|
+
return "\"\"";
|
|
352
|
+
}
|
|
353
|
+
if (!/[\s"&|<>()^%]/.test(value)) {
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
return `"${value
|
|
357
|
+
.replace(/%/g, "%%")
|
|
358
|
+
.replace(/(\\*)"/g, '$1$1\\"')
|
|
359
|
+
.replace(/(\\+)$/g, "$1$1")}"`;
|
|
360
|
+
}
|
|
345
361
|
function capitalize(value) {
|
|
346
362
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
347
363
|
}
|
package/dist/bot.js
CHANGED
|
@@ -453,6 +453,30 @@ export function createBot(config, registry) {
|
|
|
453
453
|
await drainQueuedPrompts(createSystemContext(contextKey), contextKey, parsed.chatId, session);
|
|
454
454
|
}
|
|
455
455
|
};
|
|
456
|
+
const sendExternalMirrorTyping = async (chatId, messageThreadId, state) => {
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
if (state.lastTypingAt && now - state.lastTypingAt < TYPING_INTERVAL_MS) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
state.lastTypingAt = now;
|
|
462
|
+
await sendChatActionSafe(bot.api, chatId, "typing", messageThreadId).catch(() => { });
|
|
463
|
+
};
|
|
464
|
+
const sendExternalWorkingNotice = async (chatId, messageThreadId, state, snapshot) => {
|
|
465
|
+
const turnKey = snapshot.activity.turnId ?? snapshot.activity.startedAt?.toISOString() ?? "unknown";
|
|
466
|
+
if (state.workingNoticeTurnKey === turnKey) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const prompt = trimLine(snapshot.latestUserMessage ?? "", 250);
|
|
470
|
+
const fallbackText = prompt ? `Working on ${prompt}` : `Working on external ${snapshot.agentLabel} task...`;
|
|
471
|
+
const html = prompt
|
|
472
|
+
? `<b>Working on</b> ${escapeHTML(prompt)}`
|
|
473
|
+
: `<b>Working on</b> external ${escapeHTML(snapshot.agentLabel)} task...`;
|
|
474
|
+
await sendTextMessage(bot.api, chatId, html, {
|
|
475
|
+
fallbackText,
|
|
476
|
+
messageThreadId,
|
|
477
|
+
});
|
|
478
|
+
state.workingNoticeTurnKey = turnKey;
|
|
479
|
+
};
|
|
456
480
|
const mirrorExternalSnapshot = async (contextKey, chatId, session, snapshot) => {
|
|
457
481
|
const parsed = parseContextKey(contextKey);
|
|
458
482
|
const previous = externalMirrors.get(contextKey);
|
|
@@ -471,7 +495,15 @@ export function createBot(config, registry) {
|
|
|
471
495
|
if (snapshot.activity.active) {
|
|
472
496
|
state.turnId = snapshot.activity.turnId;
|
|
473
497
|
state.startedAt = snapshot.activity.startedAt;
|
|
474
|
-
if (mirrorMode
|
|
498
|
+
if (mirrorMode !== "off") {
|
|
499
|
+
await sendExternalMirrorTyping(chatId, parsed.messageThreadId, state);
|
|
500
|
+
}
|
|
501
|
+
if (mirrorMode === "final") {
|
|
502
|
+
await sendExternalWorkingNotice(chatId, parsed.messageThreadId, state, snapshot);
|
|
503
|
+
state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (mirrorMode === "off") {
|
|
475
507
|
state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
|
|
476
508
|
return;
|
|
477
509
|
}
|
|
@@ -510,7 +542,6 @@ export function createBot(config, registry) {
|
|
|
510
542
|
state.latestMirroredEventLine = event.lineNumber;
|
|
511
543
|
}
|
|
512
544
|
}
|
|
513
|
-
await sendChatActionSafe(bot.api, chatId, "typing", parsed.messageThreadId).catch(() => { });
|
|
514
545
|
state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
|
|
515
546
|
return;
|
|
516
547
|
}
|
|
@@ -551,6 +582,7 @@ export function createBot(config, registry) {
|
|
|
551
582
|
}
|
|
552
583
|
await deliverCliGeneratedArtifacts(contextKey, chatId, session, state.startedAt, terminalEvent.turnId, parsed.messageThreadId);
|
|
553
584
|
}
|
|
585
|
+
state.workingNoticeTurnKey = undefined;
|
|
554
586
|
state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
|
|
555
587
|
};
|
|
556
588
|
const canSendSystemMessagesToContext = (contextKey) => {
|
package/dist/codex-cli.js
CHANGED
|
@@ -26,7 +26,7 @@ export function findExecutableOnPath(command, pathValue) {
|
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
28
28
|
const extensions = process.platform === "win32"
|
|
29
|
-
? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";")
|
|
29
|
+
? ["", ...(process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";")]
|
|
30
30
|
: [""];
|
|
31
31
|
for (const rawDirectory of pathValue.split(path.delimiter)) {
|
|
32
32
|
const directory = rawDirectory.trim();
|
package/dist/config-metadata.js
CHANGED
|
@@ -88,6 +88,7 @@ export const SETTING_DEFINITIONS = [
|
|
|
88
88
|
setting("NORDRELAY_AUDIT_MAX_EVENTS", "Audit max events", "Workspace", "number", "Retained audit events.", true),
|
|
89
89
|
setting("NORDRELAY_SESSION_LOCK_TTL_MS", "Session lock TTL", "Workspace", "number", "Write-lock TTL.", true),
|
|
90
90
|
setting("NORDRELAY_VERSION_CACHE_TTL_MS", "Version cache TTL", "Workspace", "number", "NPM version cache TTL.", true),
|
|
91
|
+
setting("NORDRELAY_CLI_VERSION_CACHE_TTL_MS", "CLI version cache TTL", "Workspace", "number", "Installed agent CLI version cache TTL.", true),
|
|
91
92
|
setting("OPENAI_API_KEY", "OpenAI API key", "Voice", "secret", "Whisper fallback API key.", true),
|
|
92
93
|
setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
|
|
93
94
|
setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
|
|
@@ -180,6 +181,7 @@ const EXAMPLE_VALUES = {
|
|
|
180
181
|
"NORDRELAY_AUDIT_MAX_EVENTS": "1000",
|
|
181
182
|
"NORDRELAY_SESSION_LOCK_TTL_MS": "1800000",
|
|
182
183
|
"NORDRELAY_VERSION_CACHE_TTL_MS": "3600000",
|
|
184
|
+
"NORDRELAY_CLI_VERSION_CACHE_TTL_MS": "60000",
|
|
183
185
|
"NORDRELAY_DASHBOARD_HOST": "127.0.0.1",
|
|
184
186
|
"NORDRELAY_DASHBOARD_PORT": "31878",
|
|
185
187
|
"NORDRELAY_ENV_FILE": "",
|
package/dist/operations.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
159
|
-
|
|
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:
|
|
176
|
-
piCli: describePiCli(piCli),
|
|
177
|
-
piCliPath: piCli.path ?? null,
|
|
178
|
-
piCliVersion:
|
|
179
|
-
hermesCli: describeHermesCli(hermesCli),
|
|
180
|
-
hermesCliPath: hermesCli.path ?? null,
|
|
181
|
-
hermesCliVersion:
|
|
182
|
-
openClawCli: describeOpenClawCli(openClawCli),
|
|
183
|
-
openClawCliPath: openClawCli.path ?? null,
|
|
184
|
-
openClawCliVersion:
|
|
185
|
-
claudeCodeCli: describeClaudeCodeCli(claudeCodeCli),
|
|
186
|
-
claudeCodeCliPath: claudeCodeCli.path ?? null,
|
|
187
|
-
claudeCodeCliVersion:
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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:
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
308
|
-
|
|
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 =
|
|
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) {
|
package/dist/relay-runtime.js
CHANGED
|
@@ -105,10 +105,16 @@ export class RelayRuntime {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
async status() {
|
|
108
|
+
const cliOptions = this.cliPathOptions();
|
|
109
|
+
const [health, versionChecks, snapshot] = await Promise.all([
|
|
110
|
+
getConnectorHealth(cliOptions),
|
|
111
|
+
getVersionChecks(cliOptions),
|
|
112
|
+
this.snapshot(),
|
|
113
|
+
]);
|
|
108
114
|
return {
|
|
109
|
-
health
|
|
110
|
-
versionChecks
|
|
111
|
-
snapshot
|
|
115
|
+
health,
|
|
116
|
+
versionChecks,
|
|
117
|
+
snapshot,
|
|
112
118
|
};
|
|
113
119
|
}
|
|
114
120
|
async bootstrapStatus() {
|
|
@@ -121,10 +127,16 @@ export class RelayRuntime {
|
|
|
121
127
|
};
|
|
122
128
|
}
|
|
123
129
|
async version() {
|
|
130
|
+
const cliOptions = this.cliPathOptions();
|
|
131
|
+
const [health, state, versionChecks] = await Promise.all([
|
|
132
|
+
getConnectorHealth(cliOptions),
|
|
133
|
+
readConnectorState(),
|
|
134
|
+
getVersionChecks(cliOptions),
|
|
135
|
+
]);
|
|
124
136
|
return {
|
|
125
|
-
health
|
|
126
|
-
state
|
|
127
|
-
versionChecks
|
|
137
|
+
health,
|
|
138
|
+
state,
|
|
139
|
+
versionChecks,
|
|
128
140
|
};
|
|
129
141
|
}
|
|
130
142
|
updateConnector() {
|
|
@@ -199,22 +211,32 @@ export class RelayRuntime {
|
|
|
199
211
|
return this.agentUpdates.cancel(id);
|
|
200
212
|
}
|
|
201
213
|
async diagnostics() {
|
|
214
|
+
const cliOptions = this.cliPathOptions();
|
|
215
|
+
const [health, versionChecks, snapshot, session] = await Promise.all([
|
|
216
|
+
getConnectorHealth(cliOptions),
|
|
217
|
+
getVersionChecks(cliOptions),
|
|
218
|
+
this.snapshot(),
|
|
219
|
+
this.getSession(true),
|
|
220
|
+
]);
|
|
202
221
|
return {
|
|
203
|
-
health
|
|
204
|
-
versionChecks
|
|
205
|
-
snapshot
|
|
222
|
+
health,
|
|
223
|
+
versionChecks,
|
|
224
|
+
snapshot,
|
|
206
225
|
runtime: {
|
|
207
226
|
stateBackend: this.config.stateBackend,
|
|
208
227
|
sourceWorkspace: this.config.workspace,
|
|
209
228
|
queuePaused: this.queueService.isPaused(),
|
|
210
229
|
externalMirror: this.externalActivityMonitor.snapshot(),
|
|
211
|
-
agentDiagnostics: getAgentDiagnostics(
|
|
230
|
+
agentDiagnostics: getAgentDiagnostics(session, this.config),
|
|
212
231
|
},
|
|
213
232
|
};
|
|
214
233
|
}
|
|
215
234
|
async adapterHealth() {
|
|
216
|
-
const
|
|
217
|
-
const versions = await
|
|
235
|
+
const cliOptions = this.cliPathOptions();
|
|
236
|
+
const [health, versions] = await Promise.all([
|
|
237
|
+
getConnectorHealth(cliOptions),
|
|
238
|
+
getVersionChecks(cliOptions),
|
|
239
|
+
]);
|
|
218
240
|
return Promise.all(listAgentAdapterDescriptors().map(async (descriptor) => {
|
|
219
241
|
const enabled = enabledAgents(this.config).includes(descriptor.id);
|
|
220
242
|
const auth = descriptor.capabilities.auth && enabled
|
|
@@ -945,6 +967,14 @@ export class RelayRuntime {
|
|
|
945
967
|
});
|
|
946
968
|
return { session, dispose: true };
|
|
947
969
|
}
|
|
970
|
+
cliPathOptions() {
|
|
971
|
+
return {
|
|
972
|
+
piCliPath: this.config.piCliPath,
|
|
973
|
+
hermesCliPath: this.config.hermesCliPath,
|
|
974
|
+
openClawCliPath: this.config.openClawCliPath,
|
|
975
|
+
claudeCodeCliPath: this.config.claudeCodeCliPath,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
948
978
|
async ensureActiveThread(session) {
|
|
949
979
|
if (!session.hasActiveThread()) {
|
|
950
980
|
await session.newThread();
|
package/dist/state-backend.js
CHANGED
|
@@ -79,5 +79,8 @@ function tryCreateSqliteDocumentStore(options) {
|
|
|
79
79
|
"ON CONFLICT(key) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at",
|
|
80
80
|
].join(" ")).run(options.sqliteKey, JSON.stringify(value), new Date().toISOString());
|
|
81
81
|
},
|
|
82
|
+
close() {
|
|
83
|
+
db.close();
|
|
84
|
+
},
|
|
82
85
|
};
|
|
83
86
|
}
|
package/dist/support-bundle.js
CHANGED
|
@@ -178,7 +178,8 @@ function systemInfo() {
|
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
function detectNpmVersion(npm) {
|
|
181
|
-
const
|
|
181
|
+
const args = [...npm.argsPrefix, "--version"];
|
|
182
|
+
const result = spawnSync(npm.shell ? formatShellCommand(npm.command, args) : npm.command, npm.shell ? [] : args, {
|
|
182
183
|
encoding: "utf8",
|
|
183
184
|
shell: npm.shell,
|
|
184
185
|
timeout: 3000,
|
|
@@ -189,6 +190,21 @@ function detectNpmVersion(npm) {
|
|
|
189
190
|
}
|
|
190
191
|
return String(result.stdout || "").trim() || null;
|
|
191
192
|
}
|
|
193
|
+
function formatShellCommand(command, args) {
|
|
194
|
+
return [command, ...args].map(quoteWindowsCmdArg).join(" ");
|
|
195
|
+
}
|
|
196
|
+
function quoteWindowsCmdArg(value) {
|
|
197
|
+
if (value.length === 0) {
|
|
198
|
+
return "\"\"";
|
|
199
|
+
}
|
|
200
|
+
if (!/[\s"&|<>()^%]/.test(value)) {
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
return `"${value
|
|
204
|
+
.replace(/%/g, "%%")
|
|
205
|
+
.replace(/(\\*)"/g, '$1$1\\"')
|
|
206
|
+
.replace(/(\\+)$/g, "$1$1")}"`;
|
|
207
|
+
}
|
|
192
208
|
function formatTimestamp(date) {
|
|
193
209
|
return [
|
|
194
210
|
date.getFullYear(),
|
package/dist/web-dashboard.js
CHANGED
|
@@ -584,6 +584,7 @@ function activeSettingsValues(current) {
|
|
|
584
584
|
NORDRELAY_AUDIT_MAX_EVENTS: String(current.auditMaxEvents),
|
|
585
585
|
NORDRELAY_SESSION_LOCK_TTL_MS: String(current.sessionLockTtlMs),
|
|
586
586
|
NORDRELAY_VERSION_CACHE_TTL_MS: process.env.NORDRELAY_VERSION_CACHE_TTL_MS,
|
|
587
|
+
NORDRELAY_CLI_VERSION_CACHE_TTL_MS: process.env.NORDRELAY_CLI_VERSION_CACHE_TTL_MS,
|
|
587
588
|
VOICE_PREFERRED_BACKEND: current.voicePreferredBackend,
|
|
588
589
|
VOICE_DEFAULT_LANGUAGE: current.voiceDefaultLanguage,
|
|
589
590
|
VOICE_TRANSCRIBE_ONLY: boolValue(current.voiceTranscribeOnly),
|
package/package.json
CHANGED
|
@@ -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
|
|