@nordbyte/nordrelay 0.3.0 → 0.3.1
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/CHANGELOG.md +9 -0
- package/README.md +24 -18
- package/dist/bot.js +5 -2
- package/dist/codex-session.js +3 -1
- package/dist/context-key.js +23 -0
- package/dist/operations.js +1 -1
- package/dist/relay-runtime.js +436 -7
- package/dist/session-registry.js +3 -3
- package/dist/settings-service.js +46 -23
- package/dist/state-backend.js +17 -8
- package/dist/web-dashboard.js +159 -33
- package/dist/web-state.js +131 -0
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +42 -17
package/dist/settings-service.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
const SECRET_KEYS = new Set([
|
|
@@ -17,7 +16,7 @@ export const SETTING_DEFINITIONS = [
|
|
|
17
16
|
setting("TELEGRAM_ALLOWED_CHAT_IDS", "Allowed chat IDs", "Telegram", "list", "Optional chat allowlist.", true),
|
|
18
17
|
setting("TELEGRAM_ALLOW_ANY_CHAT", "Allow any Telegram chat", "Telegram", "boolean", "Unsafe override; keep off for normal use.", true),
|
|
19
18
|
setting("TELEGRAM_ROLE_POLICIES_JSON", "Role policy JSON", "Telegram", "json", "Granular Telegram permission policy.", true),
|
|
20
|
-
setting("TELEGRAM_TRANSPORT", "Telegram transport", "Telegram", "string", "polling or webhook.", true),
|
|
19
|
+
setting("TELEGRAM_TRANSPORT", "Telegram transport", "Telegram", "string", "polling or webhook.", true, ["polling", "webhook"]),
|
|
21
20
|
setting("TELEGRAM_WEBHOOK_URL", "Webhook public URL", "Telegram", "string", "Public base URL for webhook mode.", true),
|
|
22
21
|
setting("TELEGRAM_WEBHOOK_HOST", "Webhook bind host", "Telegram", "string", "Local webhook bind host.", true),
|
|
23
22
|
setting("TELEGRAM_WEBHOOK_PORT", "Webhook bind port", "Telegram", "number", "Local webhook bind port.", true),
|
|
@@ -25,7 +24,7 @@ export const SETTING_DEFINITIONS = [
|
|
|
25
24
|
setting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "Telegram", "secret", "Optional Telegram webhook secret token.", true),
|
|
26
25
|
setting("NORDRELAY_CODEX_ENABLED", "Enable Codex", "Agents", "boolean", "Allow Codex sessions.", true),
|
|
27
26
|
setting("NORDRELAY_PI_ENABLED", "Enable Pi", "Agents", "boolean", "Allow Pi sessions.", true),
|
|
28
|
-
setting("NORDRELAY_DEFAULT_AGENT", "Default agent", "Agents", "string", "codex or pi.", true),
|
|
27
|
+
setting("NORDRELAY_DEFAULT_AGENT", "Default agent", "Agents", "string", "codex or pi.", true, ["codex", "pi"]),
|
|
29
28
|
setting("CODEX_API_KEY", "Codex API key", "Codex", "secret", "Optional Codex SDK API key.", true),
|
|
30
29
|
setting("CODEX_CLI_PATH", "Codex CLI path", "Codex", "string", "Optional explicit Codex executable path.", true),
|
|
31
30
|
setting("CODEX_USE_BUNDLED_CLI", "Use bundled Codex CLI", "Codex", "boolean", "Force SDK-bundled CLI instead of host CLI.", true),
|
|
@@ -33,28 +32,28 @@ export const SETTING_DEFINITIONS = [
|
|
|
33
32
|
setting("CODEX_SYNC_INTERVAL_MS", "Codex sync interval", "Codex", "number", "Local state sync interval.", true),
|
|
34
33
|
setting("CODEX_EXTERNAL_BUSY_CHECK_MS", "External busy check", "Codex", "number", "External CLI busy polling interval.", true),
|
|
35
34
|
setting("CODEX_EXTERNAL_BUSY_STALE_MS", "External busy stale timeout", "Codex", "number", "External CLI stale timeout.", true),
|
|
36
|
-
setting("CODEX_SANDBOX_MODE", "Codex sandbox mode", "Codex", "string", "read-only, workspace-write, or danger-full-access.", true),
|
|
37
|
-
setting("CODEX_APPROVAL_POLICY", "Codex approval policy", "Codex", "string", "never, on-request, on-failure, or untrusted.", true),
|
|
35
|
+
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"]),
|
|
36
|
+
setting("CODEX_APPROVAL_POLICY", "Codex approval policy", "Codex", "string", "never, on-request, on-failure, or untrusted.", true, ["never", "on-request", "on-failure", "untrusted"]),
|
|
38
37
|
setting("CODEX_LAUNCH_PROFILES_JSON", "Launch profiles JSON", "Codex", "json", "Additional launch profile definitions.", true),
|
|
39
38
|
setting("CODEX_DEFAULT_LAUNCH_PROFILE", "Default launch profile", "Codex", "string", "Launch profile ID used by default.", true),
|
|
40
39
|
setting("ENABLE_UNSAFE_LAUNCH_PROFILES", "Enable unsafe profiles", "Codex", "boolean", "Expose danger-full-access profiles.", true),
|
|
41
40
|
setting("PI_CLI_PATH", "Pi CLI path", "Pi", "string", "Optional Pi executable path.", true),
|
|
42
41
|
setting("PI_SESSION_DIR", "Pi session dir", "Pi", "string", "Optional Pi session directory.", true),
|
|
43
42
|
setting("PI_DEFAULT_MODEL", "Default Pi model", "Pi", "string", "Default Pi model slug.", false),
|
|
44
|
-
setting("PI_DEFAULT_THINKING", "Default Pi thinking", "Pi", "string", "off, minimal, low, medium, high, or xhigh.", false),
|
|
45
|
-
setting("CONNECTOR_LOG_FORMAT", "Log format", "Operations", "string", "text or json.", true),
|
|
46
|
-
setting("TOOL_VERBOSITY", "Tool verbosity", "Operations", "string", "all, summary, errors-only, or none.", false),
|
|
43
|
+
setting("PI_DEFAULT_THINKING", "Default Pi thinking", "Pi", "string", "off, minimal, low, medium, high, or xhigh.", false, ["off", "minimal", "low", "medium", "high", "xhigh"]),
|
|
44
|
+
setting("CONNECTOR_LOG_FORMAT", "Log format", "Operations", "string", "text or json.", true, ["text", "json"]),
|
|
45
|
+
setting("TOOL_VERBOSITY", "Tool verbosity", "Operations", "string", "all, summary, errors-only, or none.", false, ["all", "summary", "errors-only", "none"]),
|
|
47
46
|
setting("SHOW_TURN_TOKEN_USAGE", "Show turn token usage", "Operations", "boolean", "Append per-turn token usage.", false),
|
|
48
47
|
setting("ENABLE_TELEGRAM_LOGIN", "Enable Telegram login", "Operations", "boolean", "Allow /login and /logout.", true),
|
|
49
48
|
setting("ENABLE_TELEGRAM_REACTIONS", "Enable Telegram reactions", "Operations", "boolean", "Send Telegram reactions.", true),
|
|
50
49
|
setting("TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS", "Telegram send interval", "Operations", "number", "Minimum send interval.", true),
|
|
51
50
|
setting("TELEGRAM_EDIT_MIN_INTERVAL_MS", "Telegram edit interval", "Operations", "number", "Minimum edit interval.", true),
|
|
52
|
-
setting("TELEGRAM_CLI_MIRROR_MODE", "CLI mirror mode", "Operations", "string", "off, status, final, or full.", false),
|
|
51
|
+
setting("TELEGRAM_CLI_MIRROR_MODE", "CLI mirror mode", "Operations", "string", "off, status, final, or full.", false, ["off", "status", "final", "full"]),
|
|
53
52
|
setting("TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS", "CLI mirror update interval", "Operations", "number", "Minimum mirrored edit interval.", true),
|
|
54
|
-
setting("TELEGRAM_NOTIFY_MODE", "Notify mode", "Operations", "string", "off, minimal, or all.", false),
|
|
53
|
+
setting("TELEGRAM_NOTIFY_MODE", "Notify mode", "Operations", "string", "off, minimal, or all.", false, ["off", "minimal", "all"]),
|
|
55
54
|
setting("TELEGRAM_QUIET_HOURS", "Quiet hours", "Operations", "string", "HH-HH or blank.", false),
|
|
56
55
|
setting("TELEGRAM_REDACT_PATTERNS", "Redaction patterns", "Operations", "list", "Additional comma-separated regex patterns.", true),
|
|
57
|
-
setting("NORDRELAY_UPDATE_METHOD", "Update method", "Operations", "string", "auto, npm, or git.", true),
|
|
56
|
+
setting("NORDRELAY_UPDATE_METHOD", "Update method", "Operations", "string", "auto, npm, or git.", true, ["auto", "npm", "git"]),
|
|
58
57
|
setting("MAX_FILE_SIZE", "Max file size", "Artifacts", "number", "Max inbound/outbound file size.", true),
|
|
59
58
|
setting("ARTIFACT_RETENTION_DAYS", "Artifact retention days", "Artifacts", "number", "Days before pruning.", true),
|
|
60
59
|
setting("ARTIFACT_MAX_TURNS", "Max artifact turns", "Artifacts", "number", "Maximum artifact turns retained.", true),
|
|
@@ -64,12 +63,12 @@ export const SETTING_DEFINITIONS = [
|
|
|
64
63
|
setting("TELEGRAM_AUTO_SEND_ARTIFACTS", "Auto-send artifacts", "Artifacts", "boolean", "Automatically send artifact files.", false),
|
|
65
64
|
setting("WORKSPACE_ALLOWED_ROOTS", "Workspace allowed roots", "Workspace", "list", "Restrict selectable workspaces.", true),
|
|
66
65
|
setting("WORKSPACE_WARN_ROOTS", "Workspace warn roots", "Workspace", "list", "Warn for broad workspace roots.", true),
|
|
67
|
-
setting("NORDRELAY_STATE_BACKEND", "State backend", "Workspace", "string", "json or sqlite.", true),
|
|
66
|
+
setting("NORDRELAY_STATE_BACKEND", "State backend", "Workspace", "string", "json or sqlite.", true, ["json", "sqlite"]),
|
|
68
67
|
setting("NORDRELAY_AUDIT_MAX_EVENTS", "Audit max events", "Workspace", "number", "Retained audit events.", true),
|
|
69
68
|
setting("NORDRELAY_SESSION_LOCK_TTL_MS", "Session lock TTL", "Workspace", "number", "Write-lock TTL.", true),
|
|
70
69
|
setting("NORDRELAY_VERSION_CACHE_TTL_MS", "Version cache TTL", "Workspace", "number", "NPM version cache TTL.", true),
|
|
71
70
|
setting("OPENAI_API_KEY", "OpenAI API key", "Voice", "secret", "Whisper fallback API key.", true),
|
|
72
|
-
setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false),
|
|
71
|
+
setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
|
|
73
72
|
setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
|
|
74
73
|
setting("VOICE_TRANSCRIBE_ONLY", "Voice transcribe only", "Voice", "boolean", "Do not send voice transcripts as prompts.", false),
|
|
75
74
|
setting("FASTER_WHISPER_PYTHON", "faster-whisper Python", "Voice", "string", "Python executable.", true),
|
|
@@ -108,6 +107,7 @@ export class SettingsService {
|
|
|
108
107
|
async update(patch) {
|
|
109
108
|
const current = await readEnvFile(this.envPath);
|
|
110
109
|
const changedKeys = [];
|
|
110
|
+
const errors = [];
|
|
111
111
|
const definitions = new Map(SETTING_DEFINITIONS.map((definition) => [definition.key, definition]));
|
|
112
112
|
for (const [key, rawValue] of Object.entries(patch)) {
|
|
113
113
|
const definition = definitions.get(key);
|
|
@@ -125,18 +125,24 @@ export class SettingsService {
|
|
|
125
125
|
}
|
|
126
126
|
continue;
|
|
127
127
|
}
|
|
128
|
+
const validationError = validateSettingValue(definition, value);
|
|
129
|
+
if (validationError) {
|
|
130
|
+
errors.push({ key, message: validationError });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
128
133
|
if (current[key] !== value) {
|
|
129
134
|
current[key] = value;
|
|
130
135
|
changedKeys.push(key);
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
|
-
if (changedKeys.length > 0) {
|
|
138
|
+
if (changedKeys.length > 0 && errors.length === 0) {
|
|
134
139
|
await writeEnvFile(this.envPath, current);
|
|
135
140
|
}
|
|
136
141
|
return {
|
|
137
142
|
envPath: this.envPath,
|
|
138
|
-
changedKeys,
|
|
139
|
-
restartRequired: changedKeys.some((key) => definitions.get(key)?.restartRequired),
|
|
143
|
+
changedKeys: errors.length === 0 ? changedKeys : [],
|
|
144
|
+
restartRequired: errors.length === 0 && changedKeys.some((key) => definitions.get(key)?.restartRequired),
|
|
145
|
+
errors,
|
|
140
146
|
};
|
|
141
147
|
}
|
|
142
148
|
}
|
|
@@ -144,11 +150,8 @@ export function resolveDashboardEnvPath(home, cwd = process.cwd()) {
|
|
|
144
150
|
if (process.env.NORDRELAY_ENV_FILE) {
|
|
145
151
|
return path.resolve(process.env.NORDRELAY_ENV_FILE);
|
|
146
152
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return homeEnv;
|
|
150
|
-
}
|
|
151
|
-
return path.join(cwd, ".env");
|
|
153
|
+
void cwd;
|
|
154
|
+
return path.join(home, "nordrelay.env");
|
|
152
155
|
}
|
|
153
156
|
export function maskSecret(value) {
|
|
154
157
|
if (!value) {
|
|
@@ -159,8 +162,28 @@ export function maskSecret(value) {
|
|
|
159
162
|
}
|
|
160
163
|
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
161
164
|
}
|
|
162
|
-
function setting(key, label, group, kind, description, restartRequired) {
|
|
163
|
-
return { key, label, group, kind, description, restartRequired };
|
|
165
|
+
function setting(key, label, group, kind, description, restartRequired, options) {
|
|
166
|
+
return { key, label, group, kind, description, restartRequired, options };
|
|
167
|
+
}
|
|
168
|
+
function validateSettingValue(definition, value) {
|
|
169
|
+
if (definition.kind === "number" && !Number.isFinite(Number(value))) {
|
|
170
|
+
return "Must be a number.";
|
|
171
|
+
}
|
|
172
|
+
if (definition.kind === "boolean" && !["true", "false", "1", "0", "yes", "no", "on", "off"].includes(value.toLowerCase())) {
|
|
173
|
+
return "Must be true or false.";
|
|
174
|
+
}
|
|
175
|
+
if (definition.kind === "json") {
|
|
176
|
+
try {
|
|
177
|
+
JSON.parse(value);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
return `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (definition.options && !definition.options.includes(value)) {
|
|
184
|
+
return `Must be one of: ${definition.options.join(", ")}.`;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
164
187
|
}
|
|
165
188
|
async function readEnvFile(filePath) {
|
|
166
189
|
try {
|
package/dist/state-backend.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { readJsonFileWithBackup, writeJsonFileAtomic } from "./persistence.js";
|
|
4
5
|
const require = createRequire(import.meta.url);
|
|
@@ -40,14 +41,22 @@ function tryCreateSqliteDocumentStore(options) {
|
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
const filePath = stateBackendPath(options.workspace, "sqlite");
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
let db;
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
47
|
+
db = new Database(filePath);
|
|
48
|
+
db.exec([
|
|
49
|
+
"CREATE TABLE IF NOT EXISTS documents (",
|
|
50
|
+
"key TEXT PRIMARY KEY,",
|
|
51
|
+
"json TEXT NOT NULL,",
|
|
52
|
+
"updated_at TEXT NOT NULL",
|
|
53
|
+
")",
|
|
54
|
+
].join(" "));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.warn(`SQLite state backend failed at ${filePath}:`, error instanceof Error ? error.message : String(error));
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
51
60
|
return {
|
|
52
61
|
kind: "sqlite",
|
|
53
62
|
filePath,
|