@nordbyte/nordrelay 0.4.1 → 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,89 @@
1
+ export class ChannelCommandRouter {
2
+ handlers = new Map();
3
+ command(name, handler) {
4
+ const normalized = normalizeCommandName(name);
5
+ if (!normalized) {
6
+ throw new Error("Channel command name is required.");
7
+ }
8
+ this.handlers.set(normalized, handler);
9
+ return this;
10
+ }
11
+ async dispatch(message) {
12
+ const parsed = parseChannelCommand(message.text ?? "");
13
+ if (!parsed) {
14
+ return { matched: false };
15
+ }
16
+ const handler = this.handlers.get(parsed.command);
17
+ if (!handler) {
18
+ return { matched: false, command: parsed.command };
19
+ }
20
+ const response = await handler({
21
+ ...message,
22
+ text: parsed.argument,
23
+ });
24
+ return {
25
+ matched: true,
26
+ command: parsed.command,
27
+ response: response ?? undefined,
28
+ };
29
+ }
30
+ }
31
+ export async function deliverChannelAction(runtime, context, response) {
32
+ return runtime.sendMessage(context, {
33
+ text: response.html,
34
+ fallbackText: response.plain,
35
+ parseMode: "html",
36
+ buttons: response.buttons,
37
+ });
38
+ }
39
+ export function parseChannelCommand(text) {
40
+ const match = text.trimStart().match(/^\/([a-zA-Z0-9_-]+)(?:@\w+)?(?:\s+([\s\S]*))?$/);
41
+ if (!match?.[1]) {
42
+ return null;
43
+ }
44
+ return {
45
+ command: normalizeCommandName(match[1]),
46
+ argument: match[2]?.trim() ?? "",
47
+ };
48
+ }
49
+ export class InMemoryChannelRuntime {
50
+ descriptor;
51
+ capabilities;
52
+ id;
53
+ label;
54
+ sentMessages = [];
55
+ editedMessages = [];
56
+ typingContexts = [];
57
+ sentFiles = [];
58
+ constructor(descriptor) {
59
+ this.descriptor = descriptor;
60
+ this.id = descriptor.id;
61
+ this.label = descriptor.label;
62
+ this.capabilities = new Set(descriptor.capabilities);
63
+ }
64
+ describe() {
65
+ return {
66
+ ...this.descriptor,
67
+ capabilities: [...this.descriptor.capabilities],
68
+ };
69
+ }
70
+ async sendMessage(context, message) {
71
+ const messageId = `${this.id}-message-${this.sentMessages.length + 1}`;
72
+ this.sentMessages.push({ context, message, messageId });
73
+ return { messageId };
74
+ }
75
+ async editMessage(context, messageId, message) {
76
+ this.editedMessages.push({ context, messageId, message });
77
+ }
78
+ async sendTyping(context) {
79
+ this.typingContexts.push(context);
80
+ }
81
+ async sendFile(context, file) {
82
+ const messageId = `${this.id}-file-${this.sentFiles.length + 1}`;
83
+ this.sentFiles.push({ context, file, messageId });
84
+ return { messageId };
85
+ }
86
+ }
87
+ function normalizeCommandName(name) {
88
+ return name.trim().replace(/^\//, "").toLowerCase();
89
+ }
@@ -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) {
@@ -39,6 +39,7 @@ export class RelayRuntime {
39
39
  subscribers = new Set();
40
40
  externalMonitor;
41
41
  draining = false;
42
+ externalMonitorRunning = false;
42
43
  currentTurnId = null;
43
44
  accumulatedText = "";
44
45
  currentTurnStartedAt = 0;
@@ -60,7 +61,7 @@ export class RelayRuntime {
60
61
  });
61
62
  if (config.codexExternalBusyCheckMs > 0) {
62
63
  this.externalMonitor = setInterval(() => {
63
- void this.monitorExternalActivity().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
64
+ void this.monitorExternalActivitySafe();
64
65
  }, config.codexExternalBusyCheckMs);
65
66
  this.externalMonitor.unref?.();
66
67
  }
@@ -161,6 +162,18 @@ export class RelayRuntime {
161
162
  agentUpdateLog(id) {
162
163
  return this.agentUpdates.readLog(id);
163
164
  }
165
+ deleteAgentUpdateLog(id) {
166
+ const job = this.agentUpdates.deleteLog(id);
167
+ this.appendAudit({
168
+ action: "command",
169
+ status: "ok",
170
+ contextKey: WEB_CONTEXT_KEY,
171
+ agentId: job.agentId,
172
+ description: `delete update log ${id}`,
173
+ detail: job.logPath,
174
+ });
175
+ return job;
176
+ }
164
177
  sendAgentUpdateInput(id, input) {
165
178
  return this.agentUpdates.sendInput(id, input);
166
179
  }
@@ -224,12 +237,8 @@ export class RelayRuntime {
224
237
  }
225
238
  permissions() {
226
239
  return {
227
- telegramAllowAnyChat: this.config.telegramAllowAnyChat,
228
- telegramAdminUserIds: this.config.telegramAdminUserIds,
229
- telegramAllowedUserIds: this.config.telegramAllowedUserIds,
230
- telegramReadOnlyUserIds: this.config.telegramReadOnlyUserIds,
231
- telegramAllowedChatIds: this.config.telegramAllowedChatIds,
232
- telegramRolePolicies: this.config.telegramRolePolicies,
240
+ mode: "users",
241
+ message: "Access is managed by NordRelay users, groups, Telegram identities, and Telegram chat access records.",
233
242
  };
234
243
  }
235
244
  tasks() {
@@ -450,7 +459,8 @@ export class RelayRuntime {
450
459
  return { removed, messages };
451
460
  }
452
461
  activity(options = {}) {
453
- return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event));
462
+ const currentInfo = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
463
+ return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
454
464
  }
455
465
  async retry() {
456
466
  const cached = this.promptStore.getLastPrompt(WEB_CONTEXT_KEY);
@@ -1008,6 +1018,21 @@ export class RelayRuntime {
1008
1018
  }
1009
1019
  mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
1010
1020
  }
1021
+ async monitorExternalActivitySafe() {
1022
+ if (this.externalMonitorRunning) {
1023
+ return;
1024
+ }
1025
+ this.externalMonitorRunning = true;
1026
+ try {
1027
+ await this.monitorExternalActivity();
1028
+ }
1029
+ catch (error) {
1030
+ this.broadcastStatus(friendlyErrorText(error), "error");
1031
+ }
1032
+ finally {
1033
+ this.externalMonitorRunning = false;
1034
+ }
1035
+ }
1011
1036
  startExternalTurn(snapshot) {
1012
1037
  const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
1013
1038
  this.chatStore.append({
@@ -1346,11 +1371,10 @@ export class RelayRuntime {
1346
1371
  enrichActivityInput(input) {
1347
1372
  return this.enrichActivityFields(input);
1348
1373
  }
1349
- enrichActivityEvent(event) {
1350
- return this.enrichActivityFields(event);
1374
+ enrichActivityEvent(event, info) {
1375
+ return this.enrichActivityFields(event, info);
1351
1376
  }
1352
- enrichActivityFields(event) {
1353
- const info = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
1377
+ enrichActivityFields(event, info) {
1354
1378
  if (!info) {
1355
1379
  return !event.threadId && !event.workspace ? { ...event, workspace: this.config.workspace } : event;
1356
1380
  }