@nordbyte/nordrelay 0.4.0 → 0.5.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.
@@ -0,0 +1,238 @@
1
+ export const SECRET_KEYS = new Set([
2
+ "TELEGRAM_BOT_TOKEN",
3
+ "CODEX_API_KEY",
4
+ "HERMES_API_KEY",
5
+ "OPENCLAW_GATEWAY_TOKEN",
6
+ "OPENCLAW_GATEWAY_PASSWORD",
7
+ "OPENAI_API_KEY",
8
+ "TELEGRAM_WEBHOOK_SECRET",
9
+ ]);
10
+ export const SETTING_DEFINITIONS = [
11
+ setting("TELEGRAM_BOT_TOKEN", "Telegram bot token", "Telegram", "secret", "BotFather token.", true),
12
+ setting("TELEGRAM_TRANSPORT", "Telegram transport", "Telegram", "string", "polling or webhook.", true, ["polling", "webhook"]),
13
+ setting("TELEGRAM_WEBHOOK_URL", "Webhook public URL", "Telegram", "string", "Public base URL for webhook mode.", true),
14
+ setting("TELEGRAM_WEBHOOK_HOST", "Webhook bind host", "Telegram", "string", "Local webhook bind host.", true),
15
+ setting("TELEGRAM_WEBHOOK_PORT", "Webhook bind port", "Telegram", "number", "Local webhook bind port.", true),
16
+ setting("TELEGRAM_WEBHOOK_PATH", "Webhook path", "Telegram", "string", "Webhook request path.", true),
17
+ setting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "Telegram", "secret", "Optional Telegram webhook secret token.", true),
18
+ setting("NORDRELAY_CODEX_ENABLED", "Enable Codex", "Agents", "boolean", "Allow Codex sessions.", true),
19
+ setting("NORDRELAY_PI_ENABLED", "Enable Pi", "Agents", "boolean", "Allow Pi sessions.", true),
20
+ setting("NORDRELAY_HERMES_ENABLED", "Enable Hermes", "Agents", "boolean", "Allow Hermes sessions through the Hermes API Server.", true),
21
+ setting("NORDRELAY_OPENCLAW_ENABLED", "Enable OpenClaw", "Agents", "boolean", "Allow OpenClaw sessions through the OpenClaw Gateway.", true),
22
+ setting("NORDRELAY_CLAUDE_CODE_ENABLED", "Enable Claude Code", "Agents", "boolean", "Allow Claude Code sessions through the Claude Agent SDK.", true),
23
+ setting("NORDRELAY_DEFAULT_AGENT", "Default agent", "Agents", "string", "codex, pi, hermes, openclaw, or claude-code.", true, ["codex", "pi", "hermes", "openclaw", "claude-code"]),
24
+ setting("CODEX_API_KEY", "Codex API key", "Codex", "secret", "Optional Codex SDK API key.", true),
25
+ setting("CODEX_CLI_PATH", "Codex CLI path", "Codex", "string", "Optional explicit Codex executable path.", true),
26
+ setting("CODEX_USE_BUNDLED_CLI", "Use bundled Codex CLI", "Codex", "boolean", "Force SDK-bundled CLI instead of host CLI.", true),
27
+ setting("CODEX_MODEL", "Default Codex model", "Codex", "string", "Default model for new Codex threads.", false),
28
+ setting("CODEX_SYNC_INTERVAL_MS", "Codex sync interval", "Codex", "number", "Local state sync interval.", true),
29
+ setting("CODEX_EXTERNAL_BUSY_CHECK_MS", "External busy check", "Codex", "number", "External CLI busy polling interval.", true),
30
+ setting("CODEX_EXTERNAL_BUSY_STALE_MS", "External busy stale timeout", "Codex", "number", "External CLI stale timeout.", true),
31
+ setting("CODEX_SANDBOX_MODE", "Codex sandbox mode", "Codex", "string", "read-only, workspace-write, or danger-full-access.", true, ["read-only", "workspace-write", "danger-full-access"]),
32
+ setting("CODEX_APPROVAL_POLICY", "Codex approval policy", "Codex", "string", "never, on-request, on-failure, or untrusted.", true, ["never", "on-request", "on-failure", "untrusted"]),
33
+ setting("CODEX_LAUNCH_PROFILES_JSON", "Launch profiles JSON", "Codex", "json", "Additional launch profile definitions.", true),
34
+ setting("CODEX_DEFAULT_LAUNCH_PROFILE", "Default launch profile", "Codex", "string", "Launch profile ID used by default.", true),
35
+ setting("ENABLE_UNSAFE_LAUNCH_PROFILES", "Enable unsafe profiles", "Codex", "boolean", "Expose danger-full-access profiles.", true),
36
+ setting("PI_CLI_PATH", "Pi CLI path", "Pi", "string", "Optional Pi executable path.", true),
37
+ setting("PI_SESSION_DIR", "Pi session dir", "Pi", "string", "Optional Pi session directory.", true),
38
+ setting("PI_DEFAULT_MODEL", "Default Pi model", "Pi", "string", "Default Pi model slug.", false),
39
+ setting("PI_DEFAULT_THINKING", "Default Pi thinking", "Pi", "string", "off, minimal, low, medium, high, or xhigh.", false, ["off", "minimal", "low", "medium", "high", "xhigh"]),
40
+ setting("PI_DEFAULT_PROFILE", "Default Pi profile", "Pi", "string", "default, readonly, no-tools, offline, or safe-offline.", true, ["default", "readonly", "no-tools", "offline", "safe-offline"]),
41
+ setting("HERMES_CLI_PATH", "Hermes CLI path", "Hermes", "string", "Optional Hermes executable path.", true),
42
+ setting("HERMES_HOME", "Hermes home", "Hermes", "string", "Optional Hermes home directory. Defaults to ~/.hermes.", true),
43
+ setting("HERMES_STATE_DB_PATH", "Hermes state DB path", "Hermes", "string", "Optional explicit Hermes state.db path.", true),
44
+ setting("HERMES_API_BASE_URL", "Hermes API base URL", "Hermes", "string", "Hermes API Server base URL.", true),
45
+ setting("HERMES_API_KEY", "Hermes API key", "Hermes", "secret", "Bearer token for the Hermes API Server.", true),
46
+ setting("HERMES_DEFAULT_MODEL", "Default Hermes model", "Hermes", "string", "Default model label sent to Hermes API runs.", false),
47
+ setting("HERMES_DEFAULT_REASONING", "Default Hermes reasoning", "Hermes", "string", "none, minimal, low, medium, high, or xhigh.", false, ["none", "minimal", "low", "medium", "high", "xhigh"]),
48
+ setting("HERMES_DEFAULT_PROFILE", "Default Hermes profile", "Hermes", "string", "default, safe, readonly, or yolo.", true, ["default", "safe", "readonly", "yolo"]),
49
+ setting("OPENCLAW_CLI_PATH", "OpenClaw CLI path", "OpenClaw", "string", "Optional OpenClaw executable path.", true),
50
+ setting("OPENCLAW_GATEWAY_URL", "OpenClaw Gateway URL", "OpenClaw", "string", "OpenClaw Gateway WebSocket URL.", true),
51
+ setting("OPENCLAW_GATEWAY_TOKEN", "OpenClaw Gateway token", "OpenClaw", "secret", "Shared-secret token for the OpenClaw Gateway.", true),
52
+ setting("OPENCLAW_GATEWAY_PASSWORD", "OpenClaw Gateway password", "OpenClaw", "secret", "Shared-secret password for the OpenClaw Gateway.", true),
53
+ setting("OPENCLAW_AGENT_ID", "OpenClaw agent ID", "OpenClaw", "string", "Configured OpenClaw agent id, for example main or work.", false),
54
+ setting("OPENCLAW_HOME", "OpenClaw home", "OpenClaw", "string", "Optional OpenClaw home directory. Defaults to ~/.openclaw.", true),
55
+ setting("OPENCLAW_STATE_DIR", "OpenClaw state dir", "OpenClaw", "string", "Optional OpenClaw state directory.", true),
56
+ setting("OPENCLAW_DEFAULT_MODEL", "Default OpenClaw model", "OpenClaw", "string", "Default OpenClaw model id.", false),
57
+ setting("OPENCLAW_DEFAULT_THINKING", "Default OpenClaw thinking", "OpenClaw", "string", "off, minimal, low, medium, high, or xhigh.", false, ["off", "minimal", "low", "medium", "high", "xhigh"]),
58
+ setting("OPENCLAW_DEFAULT_PROFILE", "Default OpenClaw profile", "OpenClaw", "string", "default, safe, readonly, local, or deliver.", true, ["default", "safe", "readonly", "local", "deliver"]),
59
+ setting("CLAUDE_CODE_CLI_PATH", "Claude Code CLI path", "Claude Code", "string", "Optional Claude Code executable path. Defaults to claude on PATH or the SDK bundled runtime.", true),
60
+ setting("CLAUDE_CONFIG_DIR", "Claude config dir", "Claude Code", "string", "Optional Claude config directory. Defaults to ~/.claude.", true),
61
+ setting("CLAUDE_CODE_DEFAULT_MODEL", "Default Claude Code model", "Claude Code", "string", "Default Claude Code model alias or model id.", false),
62
+ setting("CLAUDE_CODE_DEFAULT_EFFORT", "Default Claude Code effort", "Claude Code", "string", "off, low, medium, high, or xhigh.", false, ["off", "low", "medium", "high", "xhigh"]),
63
+ setting("CLAUDE_CODE_DEFAULT_PROFILE", "Default Claude Code profile", "Claude Code", "string", "default, accept-edits, plan, readonly, no-tools, or bypass-permissions.", true, ["default", "accept-edits", "plan", "readonly", "no-tools", "bypass-permissions"]),
64
+ setting("CLAUDE_CODE_MAX_TURNS", "Claude Code max turns", "Claude Code", "number", "Maximum agentic turns for each Claude Code prompt.", false),
65
+ setting("CONNECTOR_LOG_FORMAT", "Log format", "Operations", "string", "text or json.", true, ["text", "json"]),
66
+ setting("TOOL_VERBOSITY", "Tool verbosity", "Operations", "string", "all, summary, errors-only, or none.", false, ["all", "summary", "errors-only", "none"]),
67
+ setting("SHOW_TURN_TOKEN_USAGE", "Show turn token usage", "Operations", "boolean", "Append per-turn token usage.", false),
68
+ setting("ENABLE_TELEGRAM_LOGIN", "Enable Telegram login", "Operations", "boolean", "Allow /login and /logout.", true),
69
+ setting("ENABLE_TELEGRAM_REACTIONS", "Enable Telegram reactions", "Operations", "boolean", "Send Telegram reactions.", true),
70
+ setting("TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS", "Telegram send interval", "Operations", "number", "Minimum send interval.", true),
71
+ setting("TELEGRAM_EDIT_MIN_INTERVAL_MS", "Telegram edit interval", "Operations", "number", "Minimum edit interval.", true),
72
+ setting("TELEGRAM_CLI_MIRROR_MODE", "CLI mirror mode", "Operations", "string", "off, status, final, or full.", false, ["off", "status", "final", "full"]),
73
+ setting("TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS", "CLI mirror update interval", "Operations", "number", "Minimum mirrored edit interval.", true),
74
+ setting("TELEGRAM_NOTIFY_MODE", "Notify mode", "Operations", "string", "off, minimal, or all.", false, ["off", "minimal", "all"]),
75
+ setting("TELEGRAM_QUIET_HOURS", "Quiet hours", "Operations", "string", "HH-HH or blank.", false),
76
+ setting("TELEGRAM_REDACT_PATTERNS", "Redaction patterns", "Operations", "list", "Additional comma-separated regex patterns.", true),
77
+ setting("NORDRELAY_UPDATE_METHOD", "Update method", "Operations", "string", "auto, npm, or git.", true, ["auto", "npm", "git"]),
78
+ setting("MAX_FILE_SIZE", "Max file size", "Artifacts", "number", "Max inbound/outbound file size.", true),
79
+ setting("ARTIFACT_RETENTION_DAYS", "Artifact retention days", "Artifacts", "number", "Days before pruning.", true),
80
+ setting("ARTIFACT_MAX_TURNS", "Max artifact turns", "Artifacts", "number", "Maximum artifact turns retained.", true),
81
+ setting("ARTIFACT_MAX_INBOX_DIRS", "Max inbox dirs", "Artifacts", "number", "Maximum inbox dirs retained.", true),
82
+ setting("ARTIFACT_IGNORE_DIRS", "Artifact ignore dirs", "Artifacts", "list", "Extra ignored dirs or relative paths.", true),
83
+ setting("ARTIFACT_IGNORE_GLOBS", "Artifact ignore globs", "Artifacts", "list", "Extra ignored glob patterns.", true),
84
+ setting("TELEGRAM_AUTO_SEND_ARTIFACTS", "Auto-send artifacts", "Artifacts", "boolean", "Automatically send artifact files.", false),
85
+ setting("WORKSPACE_ALLOWED_ROOTS", "Workspace allowed roots", "Workspace", "list", "Restrict selectable workspaces.", true),
86
+ setting("WORKSPACE_WARN_ROOTS", "Workspace warn roots", "Workspace", "list", "Warn for broad workspace roots.", true),
87
+ setting("NORDRELAY_STATE_BACKEND", "State backend", "Workspace", "string", "json or sqlite.", true, ["json", "sqlite"]),
88
+ setting("NORDRELAY_AUDIT_MAX_EVENTS", "Audit max events", "Workspace", "number", "Retained audit events.", true),
89
+ setting("NORDRELAY_SESSION_LOCK_TTL_MS", "Session lock TTL", "Workspace", "number", "Write-lock TTL.", true),
90
+ setting("NORDRELAY_VERSION_CACHE_TTL_MS", "Version cache TTL", "Workspace", "number", "NPM version cache TTL.", true),
91
+ setting("OPENAI_API_KEY", "OpenAI API key", "Voice", "secret", "Whisper fallback API key.", true),
92
+ setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
93
+ setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
94
+ setting("VOICE_TRANSCRIBE_ONLY", "Voice transcribe only", "Voice", "boolean", "Do not send voice transcripts as prompts.", false),
95
+ setting("FASTER_WHISPER_PYTHON", "faster-whisper Python", "Voice", "string", "Python executable.", true),
96
+ setting("FASTER_WHISPER_MODEL", "faster-whisper model", "Voice", "string", "Model name.", true),
97
+ setting("FASTER_WHISPER_DEVICE", "faster-whisper device", "Voice", "string", "cpu, cuda, etc.", true),
98
+ setting("FASTER_WHISPER_COMPUTE_TYPE", "faster-whisper compute type", "Voice", "string", "int8, float16, etc.", true),
99
+ setting("FASTER_WHISPER_LANGUAGE", "faster-whisper language", "Voice", "string", "Fixed transcription language.", true),
100
+ setting("FASTER_WHISPER_TIMEOUT_MS", "faster-whisper timeout", "Voice", "number", "Transcription timeout.", true),
101
+ setting("NORDRELAY_DASHBOARD_HOST", "Dashboard host", "Dashboard", "string", "WebUI bind host.", true),
102
+ setting("NORDRELAY_DASHBOARD_PORT", "Dashboard port", "Dashboard", "number", "WebUI bind port.", true),
103
+ setting("NORDRELAY_ENV_FILE", "Env file path", "Dashboard", "string", "Optional explicit env-file path used by the CLI wrapper and dashboard.", true),
104
+ ];
105
+ const EXAMPLE_VALUES = {
106
+ "TELEGRAM_BOT_TOKEN": "123456789:replace-me",
107
+ "NORDRELAY_CODEX_ENABLED": "true",
108
+ "NORDRELAY_PI_ENABLED": "false",
109
+ "NORDRELAY_HERMES_ENABLED": "false",
110
+ "NORDRELAY_OPENCLAW_ENABLED": "false",
111
+ "NORDRELAY_CLAUDE_CODE_ENABLED": "false",
112
+ "NORDRELAY_DEFAULT_AGENT": "codex",
113
+ "CODEX_API_KEY": "",
114
+ "CODEX_CLI_PATH": "",
115
+ "CODEX_USE_BUNDLED_CLI": "false",
116
+ "CODEX_MODEL": "",
117
+ "CODEX_SYNC_INTERVAL_MS": "10000",
118
+ "CODEX_EXTERNAL_BUSY_CHECK_MS": "5000",
119
+ "CODEX_EXTERNAL_BUSY_STALE_MS": "300000",
120
+ "CODEX_SANDBOX_MODE": "workspace-write",
121
+ "CODEX_APPROVAL_POLICY": "never",
122
+ "CODEX_LAUNCH_PROFILES_JSON": "",
123
+ "CODEX_DEFAULT_LAUNCH_PROFILE": "default",
124
+ "ENABLE_UNSAFE_LAUNCH_PROFILES": "false",
125
+ "PI_CLI_PATH": "",
126
+ "PI_SESSION_DIR": "",
127
+ "PI_DEFAULT_MODEL": "",
128
+ "PI_DEFAULT_THINKING": "medium",
129
+ "PI_DEFAULT_PROFILE": "default",
130
+ "HERMES_CLI_PATH": "",
131
+ "HERMES_HOME": "",
132
+ "HERMES_STATE_DB_PATH": "",
133
+ "HERMES_API_BASE_URL": "http://127.0.0.1:8642",
134
+ "HERMES_API_KEY": "",
135
+ "HERMES_DEFAULT_MODEL": "",
136
+ "HERMES_DEFAULT_REASONING": "",
137
+ "HERMES_DEFAULT_PROFILE": "default",
138
+ "OPENCLAW_CLI_PATH": "",
139
+ "OPENCLAW_GATEWAY_URL": "ws://127.0.0.1:18789",
140
+ "OPENCLAW_GATEWAY_TOKEN": "",
141
+ "OPENCLAW_GATEWAY_PASSWORD": "",
142
+ "OPENCLAW_AGENT_ID": "main",
143
+ "OPENCLAW_HOME": "",
144
+ "OPENCLAW_STATE_DIR": "",
145
+ "OPENCLAW_DEFAULT_MODEL": "",
146
+ "OPENCLAW_DEFAULT_THINKING": "",
147
+ "OPENCLAW_DEFAULT_PROFILE": "default",
148
+ "CLAUDE_CODE_CLI_PATH": "",
149
+ "CLAUDE_CONFIG_DIR": "",
150
+ "CLAUDE_CODE_DEFAULT_MODEL": "",
151
+ "CLAUDE_CODE_DEFAULT_EFFORT": "",
152
+ "CLAUDE_CODE_DEFAULT_PROFILE": "default",
153
+ "CLAUDE_CODE_MAX_TURNS": "100",
154
+ "CONNECTOR_LOG_FORMAT": "text",
155
+ "TOOL_VERBOSITY": "summary",
156
+ "SHOW_TURN_TOKEN_USAGE": "false",
157
+ "ENABLE_TELEGRAM_LOGIN": "true",
158
+ "ENABLE_TELEGRAM_REACTIONS": "false",
159
+ "TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS": "80",
160
+ "TELEGRAM_EDIT_MIN_INTERVAL_MS": "1200",
161
+ "TELEGRAM_TRANSPORT": "polling",
162
+ "TELEGRAM_WEBHOOK_URL": "",
163
+ "TELEGRAM_WEBHOOK_HOST": "127.0.0.1",
164
+ "TELEGRAM_WEBHOOK_PORT": "8080",
165
+ "TELEGRAM_WEBHOOK_PATH": "/telegram/webhook",
166
+ "TELEGRAM_WEBHOOK_SECRET": "",
167
+ "TELEGRAM_CLI_MIRROR_MODE": "status",
168
+ "TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS": "4000",
169
+ "TELEGRAM_NOTIFY_MODE": "minimal",
170
+ "TELEGRAM_QUIET_HOURS": "",
171
+ "TELEGRAM_REDACT_PATTERNS": "",
172
+ "MAX_FILE_SIZE": "20971520",
173
+ "ARTIFACT_RETENTION_DAYS": "7",
174
+ "ARTIFACT_MAX_TURNS": "30",
175
+ "ARTIFACT_MAX_INBOX_DIRS": "30",
176
+ "ARTIFACT_IGNORE_DIRS": "",
177
+ "ARTIFACT_IGNORE_GLOBS": "",
178
+ "TELEGRAM_AUTO_SEND_ARTIFACTS": "false",
179
+ "NORDRELAY_STATE_BACKEND": "json",
180
+ "NORDRELAY_AUDIT_MAX_EVENTS": "1000",
181
+ "NORDRELAY_SESSION_LOCK_TTL_MS": "1800000",
182
+ "NORDRELAY_VERSION_CACHE_TTL_MS": "3600000",
183
+ "NORDRELAY_DASHBOARD_HOST": "127.0.0.1",
184
+ "NORDRELAY_DASHBOARD_PORT": "31878",
185
+ "NORDRELAY_ENV_FILE": "",
186
+ "WORKSPACE_ALLOWED_ROOTS": "",
187
+ "WORKSPACE_WARN_ROOTS": "",
188
+ "OPENAI_API_KEY": "",
189
+ "VOICE_PREFERRED_BACKEND": "auto",
190
+ "VOICE_DEFAULT_LANGUAGE": "",
191
+ "VOICE_TRANSCRIBE_ONLY": "false",
192
+ "FASTER_WHISPER_PYTHON": ".venv/bin/python",
193
+ "FASTER_WHISPER_MODEL": "base",
194
+ "FASTER_WHISPER_DEVICE": "cpu",
195
+ "FASTER_WHISPER_COMPUTE_TYPE": "int8",
196
+ "FASTER_WHISPER_LANGUAGE": "",
197
+ "FASTER_WHISPER_TIMEOUT_MS": "600000",
198
+ };
199
+ const GROUP_INTROS = {
200
+ Telegram: "Required Telegram bot and transport settings.",
201
+ Agents: "Agent access. Codex is enabled by default; Pi, Hermes, OpenClaw, and Claude Code are opt-in.",
202
+ Codex: "Codex defaults for newly created or reattached sessions.",
203
+ Pi: "Pi coding agent defaults.",
204
+ Hermes: "Hermes Agent defaults. Hermes uses the Hermes API Server.",
205
+ OpenClaw: "OpenClaw Agent defaults. OpenClaw uses the OpenClaw Gateway WebSocket RPC endpoint.",
206
+ "Claude Code": "Claude Code defaults. NordRelay uses the Claude Agent SDK and the host claude CLI when present.",
207
+ Operations: "Runtime output, logging, update, and Telegram behavior controls.",
208
+ Artifacts: "File, artifact, and retention controls.",
209
+ Workspace: "State and workspace guardrails.",
210
+ Voice: "Optional voice transcription settings.",
211
+ Dashboard: "Local WebUI dashboard. User login is required for every page, API route, SSE stream, artifact download, and health endpoint.",
212
+ };
213
+ export function envExampleValue(key) {
214
+ return EXAMPLE_VALUES[key] ?? "";
215
+ }
216
+ export function renderEnvExample() {
217
+ const lines = [
218
+ "# NordRelay runtime config example.",
219
+ "# Access is managed with NordRelay users, groups, linked Telegram identities, and enabled Telegram group chats.",
220
+ "# Create the first admin with `nordrelay init` or `nordrelay user create-admin`.",
221
+ ];
222
+ let currentGroup = "";
223
+ for (const definition of SETTING_DEFINITIONS) {
224
+ if (definition.group !== currentGroup) {
225
+ currentGroup = definition.group;
226
+ lines.push('', `# ${currentGroup}`, `# ${GROUP_INTROS[currentGroup] ?? definition.description}`);
227
+ }
228
+ lines.push(`# ${definition.description}`);
229
+ if (definition.options?.length) {
230
+ lines.push(`# Options: ${definition.options.join(", ")}`);
231
+ }
232
+ lines.push(`${definition.key}=${envExampleValue(definition.key)}`);
233
+ }
234
+ return `${lines.join('\n')}\n`;
235
+ }
236
+ function setting(key, label, group, kind, description, restartRequired, options) {
237
+ return { key, label, group, kind, description, restartRequired, options };
238
+ }
package/dist/config.js CHANGED
@@ -2,21 +2,10 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createBuiltinLaunchProfiles, createDefaultLaunchProfile, findLaunchProfile, isCodexApprovalPolicy, isCodexSandboxMode, parseLaunchProfilesJson, } from "./codex-launch.js";
4
4
  import { CLAUDE_CODE_EFFORT_LEVELS, HERMES_REASONING_EFFORTS, OPENCLAW_THINKING_LEVELS, isAgentId, PI_THINKING_LEVELS, } from "./agent.js";
5
- import { parseRolePoliciesJson, } from "./access-control.js";
6
5
  import { parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
7
6
  export function loadConfig() {
8
7
  loadEnvFile(path.resolve(process.cwd(), ".env"));
9
8
  const telegramBotToken = requireEnv("TELEGRAM_BOT_TOKEN");
10
- const telegramAllowAnyChat = parseBooleanEnv(optionalString(process.env.TELEGRAM_ALLOW_ANY_CHAT), false);
11
- const configuredAllowedUserIds = parseOptionalIdList(optionalString(process.env.TELEGRAM_ALLOWED_USER_IDS), "TELEGRAM_ALLOWED_USER_IDS", { positiveOnly: true });
12
- const telegramAllowedChatIds = parseOptionalIdList(optionalString(process.env.TELEGRAM_ALLOWED_CHAT_IDS), "TELEGRAM_ALLOWED_CHAT_IDS", { positiveOnly: false });
13
- const configuredAdminUserIds = parseOptionalIdList(optionalString(process.env.TELEGRAM_ADMIN_USER_IDS), "TELEGRAM_ADMIN_USER_IDS", { positiveOnly: true });
14
- ensureTelegramAdminIds(configuredAdminUserIds);
15
- const telegramAllowedUserIds = mergeUniqueIds(configuredAllowedUserIds, configuredAdminUserIds);
16
- const telegramReadOnlyUserIds = parseOptionalIdList(optionalString(process.env.TELEGRAM_READONLY_USER_IDS), "TELEGRAM_READONLY_USER_IDS", { positiveOnly: true });
17
- const telegramRolePolicies = parseRolePoliciesJson(optionalString(process.env.TELEGRAM_ROLE_POLICIES_JSON));
18
- ensureTelegramAllowlist(telegramAllowedUserIds, telegramAllowedChatIds, telegramAllowAnyChat);
19
- const telegramAdminUserIds = configuredAdminUserIds;
20
9
  const telegramRateLimitMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS), 80, "TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS");
21
10
  const telegramEditMinIntervalMs = parseNonNegativeIntegerEnv(optionalString(process.env.TELEGRAM_EDIT_MIN_INTERVAL_MS), 1_200, "TELEGRAM_EDIT_MIN_INTERVAL_MS");
22
11
  const telegramMirrorMode = parseMirrorMode(optionalString(process.env.TELEGRAM_CLI_MIRROR_MODE), "status");
@@ -102,16 +91,6 @@ export function loadConfig() {
102
91
  }
103
92
  return {
104
93
  telegramBotToken,
105
- telegramAllowedUserIds,
106
- telegramAllowedUserIdSet: new Set(telegramAllowedUserIds),
107
- telegramAllowedChatIds,
108
- telegramAllowedChatIdSet: new Set(telegramAllowedChatIds),
109
- telegramAdminUserIds,
110
- telegramAdminUserIdSet: new Set(telegramAdminUserIds),
111
- telegramReadOnlyUserIds,
112
- telegramReadOnlyUserIdSet: new Set(telegramReadOnlyUserIds),
113
- telegramRolePolicies,
114
- telegramAllowAnyChat,
115
94
  telegramRateLimitMinIntervalMs,
116
95
  telegramEditMinIntervalMs,
117
96
  telegramMirrorMode,
@@ -245,26 +224,6 @@ function optionalString(value) {
245
224
  const trimmed = value?.trim();
246
225
  return trimmed ? trimmed : undefined;
247
226
  }
248
- function parseOptionalIdList(raw, envName, options) {
249
- if (!raw) {
250
- return [];
251
- }
252
- const ids = raw
253
- .split(",")
254
- .map((value) => value.trim())
255
- .filter(Boolean)
256
- .map((value) => {
257
- const parsed = Number(value);
258
- if (!Number.isInteger(parsed) || (options.positiveOnly ? parsed <= 0 : parsed === 0)) {
259
- throw new Error(`Invalid Telegram id in ${envName}: ${value}`);
260
- }
261
- return parsed;
262
- });
263
- if (raw.trim() && ids.length === 0) {
264
- throw new Error(`${envName} must contain at least one id`);
265
- }
266
- return ids;
267
- }
268
227
  function parseOptionalStringList(raw) {
269
228
  if (!raw) {
270
229
  return [];
@@ -277,23 +236,6 @@ function parseOptionalStringList(raw) {
277
236
  function parsePathList(raw) {
278
237
  return parseOptionalStringList(raw).map((value) => path.resolve(value));
279
238
  }
280
- function ensureTelegramAllowlist(userIds, chatIds, allowAnyChat) {
281
- if (allowAnyChat) {
282
- return;
283
- }
284
- if (userIds.length > 0 || chatIds.length > 0) {
285
- return;
286
- }
287
- throw new Error("TELEGRAM_ALLOWED_USER_IDS or TELEGRAM_ALLOWED_CHAT_IDS must contain at least one id");
288
- }
289
- function ensureTelegramAdminIds(userIds) {
290
- if (userIds.length === 0) {
291
- throw new Error("TELEGRAM_ADMIN_USER_IDS must contain at least one id");
292
- }
293
- }
294
- function mergeUniqueIds(...groups) {
295
- return Array.from(new Set(groups.flat()));
296
- }
297
239
  function parseBooleanEnv(raw, defaultValue) {
298
240
  if (!raw) {
299
241
  return defaultValue;
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { checkPiAuthStatus } from "./pi-auth.js";
20
20
  import { describePiCli, resolvePiCli } from "./pi-cli.js";
21
21
  import { configureRedaction } from "./redaction.js";
22
22
  import { SessionRegistry } from "./session-registry.js";
23
+ import { UserStore } from "./user-management.js";
23
24
  let registry;
24
25
  let bot;
25
26
  let webhookServer;
@@ -33,6 +34,13 @@ try {
33
34
  bot = createBot(config, registry);
34
35
  await registerCommands(bot);
35
36
  console.log("NordRelay running");
37
+ const userStore = new UserStore();
38
+ if (userStore.hasAdminUser()) {
39
+ console.log("User management: admin user configured");
40
+ }
41
+ else {
42
+ console.warn("Warning: no NordRelay admin user exists. Run `nordrelay user create-admin` to enable WebUI and Telegram access.");
43
+ }
36
44
  const authStatus = await checkDefaultAgentAuth(config);
37
45
  console.log(`Auth (${agentLabel(config.defaultAgent)}): ${authStatus.authenticated ? "authenticated" : "not authenticated"} (${authStatus.method})`);
38
46
  if (!authStatus.authenticated) {
@@ -12,7 +12,8 @@ import { describePiCli, resolvePiCli } from "./pi-cli.js";
12
12
  const APP_NAME = "nordrelay";
13
13
  const PACKAGE_NAME = "@nordbyte/nordrelay";
14
14
  const CODEX_PACKAGE_NAME = "@openai/codex";
15
- const PI_PACKAGE_NAME = "@mariozechner/pi-coding-agent";
15
+ const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
16
+ const LEGACY_PI_PACKAGE_NAME = "@mariozechner/pi-coding-agent";
16
17
  const HERMES_PACKAGE_NAME = "hermes-agent";
17
18
  const OPENCLAW_PACKAGE_NAME = "openclaw";
18
19
  const CLAUDE_CODE_PACKAGE_NAME = "@anthropic-ai/claude-code";
@@ -32,6 +33,9 @@ export function getConnectorLogPath() {
32
33
  export function getUpdateLogPath() {
33
34
  return path.join(getConnectorHome(), "update.log");
34
35
  }
36
+ export function getAgentUpdateLogPath(home = getConnectorHome()) {
37
+ return path.join(home, "agent-updates.log");
38
+ }
35
39
  export async function readConnectorState() {
36
40
  try {
37
41
  return JSON.parse(await readFile(getConnectorStatePath(), "utf8"));
@@ -74,6 +78,14 @@ export async function readFormattedLogTail(lines = 80, filePath = getConnectorLo
74
78
  };
75
79
  }
76
80
  }
81
+ export function clearLogFile(filePath = getConnectorLogPath()) {
82
+ mkdirSync(path.dirname(filePath), { recursive: true });
83
+ writeFileSync(filePath, "", "utf8");
84
+ return {
85
+ filePath,
86
+ clearedAt: new Date(),
87
+ };
88
+ }
77
89
  export async function getPackageVersion() {
78
90
  try {
79
91
  const pkg = JSON.parse(await readFile(path.join(getSourceRoot(), "package.json"), "utf8"));
@@ -93,7 +105,10 @@ export async function getVersionChecks(options = {}) {
93
105
  const codexVersionLabel = codexCli.path
94
106
  ? detectCliVersion(codexCli.path)
95
107
  : readInstalledPackageVersion(CODEX_PACKAGE_NAME) ?? "not installed";
96
- const piVersionLabel = piCli.path ? detectCliVersion(piCli.path) : "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);
97
112
  const hermesVersionLabel = hermesCli.path ? detectCliVersion(hermesCli.path) : "not installed";
98
113
  const openClawVersionLabel = openClawCli.path ? detectCliVersion(openClawCli.path) : "not installed";
99
114
  const claudeCodeVersionLabel = claudeCodeCli.path
@@ -120,6 +135,7 @@ export async function getVersionChecks(options = {}) {
120
135
  installedLabel: piVersionLabel,
121
136
  installedVersion: extractVersion(piVersionLabel),
122
137
  notInstalled: piVersionLabel === "not installed",
138
+ detail: legacyPiPackageVersion ? `Legacy package ${LEGACY_PI_PACKAGE_NAME} is present; current package is ${PI_PACKAGE_NAME}.` : undefined,
123
139
  }),
124
140
  hermes: buildHermesVersionCheck(hermesVersionLabel),
125
141
  openclaw: buildVersionCheck({
@@ -139,10 +155,11 @@ export async function getVersionChecks(options = {}) {
139
155
  };
140
156
  }
141
157
  export async function getConnectorHealth(options = {}) {
142
- const state = await readConnectorState();
158
+ const rawState = await readConnectorState();
143
159
  const version = await getPackageVersion();
144
- const pidRunning = isProcessRunning(state.pid);
145
- const appPidRunning = isProcessRunning(state.appPid);
160
+ const pidRunning = isProcessRunning(rawState.pid);
161
+ const appPidRunning = isProcessRunning(rawState.appPid);
162
+ const state = normalizeConnectorState(rawState, pidRunning, appPidRunning);
146
163
  const codexCli = resolveCodexCli();
147
164
  const piCli = resolvePiCli(process.env, options.piCliPath);
148
165
  const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
@@ -247,6 +264,13 @@ function isProcessRunning(pid) {
247
264
  return false;
248
265
  }
249
266
  }
267
+ function normalizeConnectorState(state, pidRunning, appPidRunning) {
268
+ const stoppedSignal = state.signal === "SIGTERM" || state.signal === "SIGINT";
269
+ if (state.status === "error" && stoppedSignal && !state.error && !pidRunning && !appPidRunning) {
270
+ return { ...state, status: "stopped" };
271
+ }
272
+ return state;
273
+ }
250
274
  function redactSecrets(text) {
251
275
  return text.replace(SECRET_RE, "$1$2[redacted]");
252
276
  }
@@ -328,6 +352,7 @@ function buildVersionCheck(options) {
328
352
  installedVersion: null,
329
353
  latestVersion: null,
330
354
  status: "not-installed",
355
+ detail: options.detail,
331
356
  };
332
357
  }
333
358
  if (options.skipLatest) {
@@ -338,7 +363,7 @@ function buildVersionCheck(options) {
338
363
  installedVersion: options.installedVersion,
339
364
  latestVersion: null,
340
365
  status: options.installedVersion ? "unknown" : "unknown",
341
- detail: "Latest-version lookup is not available for this package source",
366
+ detail: options.detail ?? "Latest-version lookup is not available for this package source",
342
367
  };
343
368
  }
344
369
  const latest = detectLatestNpmVersion(options.packageName);
@@ -350,7 +375,7 @@ function buildVersionCheck(options) {
350
375
  installedVersion: options.installedVersion,
351
376
  latestVersion: latest.version,
352
377
  status: "unknown",
353
- detail: latest.error ?? "Could not parse installed version",
378
+ detail: [options.detail, latest.error ?? "Could not parse installed version"].filter(Boolean).join(" "),
354
379
  };
355
380
  }
356
381
  return {
@@ -360,7 +385,7 @@ function buildVersionCheck(options) {
360
385
  installedVersion: options.installedVersion,
361
386
  latestVersion: latest.version,
362
387
  status: compareVersions(options.installedVersion, latest.version) < 0 ? "outdated" : "current",
363
- detail: latest.error,
388
+ detail: [options.detail, latest.error].filter(Boolean).join(" ") || undefined,
364
389
  };
365
390
  }
366
391
  function detectLatestNpmVersion(packageName) {