@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.
@@ -6,6 +6,7 @@ import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js"
6
6
  import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
7
7
  import { getAgentDiagnostics, getExternalSnapshotForSession, } from "./agent-activity.js";
8
8
  import { listAgentAdapterDescriptors } from "./agent-adapter.js";
9
+ import { AgentUpdateManager } from "./agent-updates.js";
9
10
  import { createAgentSessionService, enabledAgents } from "./agent-factory.js";
10
11
  import { AuditLogStore } from "./audit-log.js";
11
12
  import { checkAuthStatus, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
@@ -13,10 +14,10 @@ import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout
13
14
  import { friendlyErrorText } from "./error-messages.js";
14
15
  import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
15
16
  import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
16
- import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
17
+ import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "./operations.js";
17
18
  import { checkPiAuthStatus } from "./pi-auth.js";
18
19
  import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
19
- import { renderSessionInfoPlain } from "./session-format.js";
20
+ import { renderSessionInfoPlain, renderSessionUsageRows } from "./session-format.js";
20
21
  import { SessionLockStore } from "./session-locks.js";
21
22
  import { SessionRegistry } from "./session-registry.js";
22
23
  import { transcribeAudio } from "./voice.js";
@@ -34,9 +35,11 @@ export class RelayRuntime {
34
35
  activityStore;
35
36
  auditStore;
36
37
  lockStore;
38
+ agentUpdates;
37
39
  subscribers = new Set();
38
40
  externalMonitor;
39
41
  draining = false;
42
+ externalMonitorRunning = false;
40
43
  currentTurnId = null;
41
44
  accumulatedText = "";
42
45
  currentTurnStartedAt = 0;
@@ -53,9 +56,12 @@ export class RelayRuntime {
53
56
  this.activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
54
57
  this.auditStore = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
55
58
  this.lockStore = new SessionLockStore(config.workspace, config.stateBackend);
59
+ this.agentUpdates = new AgentUpdateManager({
60
+ onUpdate: (job) => this.broadcast({ type: "agent_update", job }),
61
+ });
56
62
  if (config.codexExternalBusyCheckMs > 0) {
57
63
  this.externalMonitor = setInterval(() => {
58
- void this.monitorExternalActivity().catch((error) => this.broadcastStatus(friendlyErrorText(error), "error"));
64
+ void this.monitorExternalActivitySafe();
59
65
  }, config.codexExternalBusyCheckMs);
60
66
  this.externalMonitor.unref?.();
61
67
  }
@@ -87,6 +93,15 @@ export class RelayRuntime {
87
93
  snapshot: await this.snapshot(),
88
94
  };
89
95
  }
96
+ async bootstrapStatus() {
97
+ return {
98
+ health: {
99
+ version: await getPackageVersion(),
100
+ state: await readConnectorState(),
101
+ },
102
+ snapshot: await this.snapshot(),
103
+ };
104
+ }
90
105
  async version() {
91
106
  return {
92
107
  health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
@@ -114,6 +129,57 @@ export class RelayRuntime {
114
129
  });
115
130
  return update;
116
131
  }
132
+ agentUpdateJobs() {
133
+ return this.agentUpdates.list();
134
+ }
135
+ startAgentUpdate(agentId) {
136
+ const job = this.agentUpdates.start(agentId, {
137
+ piCliPath: this.config.piCliPath,
138
+ hermesCliPath: this.config.hermesCliPath,
139
+ openClawCliPath: this.config.openClawCliPath,
140
+ claudeCodeCliPath: this.config.claudeCodeCliPath,
141
+ });
142
+ this.broadcastStatus(`${job.agentLabel} update started. Log: ${job.logPath}`, "warn");
143
+ this.appendActivity({
144
+ source: "web",
145
+ status: "info",
146
+ type: "agent_update_started",
147
+ agentId,
148
+ threadId: null,
149
+ workspace: this.config.workspace,
150
+ detail: `${job.method}: ${job.summary}`,
151
+ });
152
+ this.appendAudit({
153
+ action: "command",
154
+ status: "ok",
155
+ contextKey: WEB_CONTEXT_KEY,
156
+ agentId,
157
+ description: `update ${agentId}`,
158
+ detail: job.summary,
159
+ });
160
+ return job;
161
+ }
162
+ agentUpdateLog(id) {
163
+ return this.agentUpdates.readLog(id);
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
+ }
177
+ sendAgentUpdateInput(id, input) {
178
+ return this.agentUpdates.sendInput(id, input);
179
+ }
180
+ cancelAgentUpdate(id) {
181
+ return this.agentUpdates.cancel(id);
182
+ }
117
183
  async diagnostics() {
118
184
  return {
119
185
  health: await getConnectorHealth({ piCliPath: this.config.piCliPath, hermesCliPath: this.config.hermesCliPath, openClawCliPath: this.config.openClawCliPath, claudeCodeCliPath: this.config.claudeCodeCliPath }),
@@ -171,12 +237,8 @@ export class RelayRuntime {
171
237
  }
172
238
  permissions() {
173
239
  return {
174
- telegramAllowAnyChat: this.config.telegramAllowAnyChat,
175
- telegramAdminUserIds: this.config.telegramAdminUserIds,
176
- telegramAllowedUserIds: this.config.telegramAllowedUserIds,
177
- telegramReadOnlyUserIds: this.config.telegramReadOnlyUserIds,
178
- telegramAllowedChatIds: this.config.telegramAllowedChatIds,
179
- telegramRolePolicies: this.config.telegramRolePolicies,
240
+ mode: "users",
241
+ message: "Access is managed by NordRelay users, groups, Telegram identities, and Telegram chat access records.",
180
242
  };
181
243
  }
182
244
  tasks() {
@@ -380,9 +442,11 @@ export class RelayRuntime {
380
442
  async sessionDetail(threadId) {
381
443
  const session = await this.getSession(true);
382
444
  const record = session.getSessionRecord(threadId);
445
+ const active = this.publicInfo(session);
383
446
  return {
384
447
  record,
385
- active: this.publicInfo(session),
448
+ active,
449
+ usageRows: active.threadId === threadId ? renderSessionUsageRows(active) : [],
386
450
  messages: this.chatStore.list(threadId, 100),
387
451
  activity: this.activity({ limit: 100 }).filter((event) => event.threadId === threadId),
388
452
  };
@@ -395,7 +459,8 @@ export class RelayRuntime {
395
459
  return { removed, messages };
396
460
  }
397
461
  activity(options = {}) {
398
- return this.activityStore.list(options);
462
+ const currentInfo = this.registry.get(WEB_CONTEXT_KEY)?.getInfo();
463
+ return this.activityStore.list(options).map((event) => this.enrichActivityEvent(event, currentInfo));
399
464
  }
400
465
  async retry() {
401
466
  const cached = this.promptStore.getLastPrompt(WEB_CONTEXT_KEY);
@@ -442,25 +507,40 @@ export class RelayRuntime {
442
507
  });
443
508
  return result;
444
509
  }
445
- async listSessions(limit = 80, query = "") {
446
- return this.filteredSessions(await this.getSession(true), query, Math.max(1, limit * 3)).slice(0, limit);
510
+ async listSessions(limit = 80, query = "", agentId) {
511
+ const { session, dispose } = await this.getControlSession(agentId);
512
+ try {
513
+ return this.filteredSessions(session, query, Math.max(1, limit * 3)).slice(0, limit);
514
+ }
515
+ finally {
516
+ if (dispose) {
517
+ session.dispose();
518
+ }
519
+ }
447
520
  }
448
- async listSessionsPage(page = 1, pageSize = MAX_WEB_SESSION_PAGE_SIZE, query = "") {
449
- const session = await this.getSession(true);
450
- const effectivePage = Math.max(1, Math.floor(page));
451
- const effectivePageSize = Math.min(MAX_WEB_SESSION_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
452
- const offset = (effectivePage - 1) * effectivePageSize;
453
- const requested = Math.min(5_000, Math.max(100, (offset + effectivePageSize + 1) * 3));
454
- const records = this.filteredSessions(session, query, requested);
455
- return {
456
- sessions: records.slice(offset, offset + effectivePageSize),
457
- pagination: {
458
- page: effectivePage,
459
- pageSize: effectivePageSize,
460
- hasPrevious: effectivePage > 1,
461
- hasNext: records.length > offset + effectivePageSize,
462
- },
463
- };
521
+ async listSessionsPage(page = 1, pageSize = MAX_WEB_SESSION_PAGE_SIZE, query = "", agentId) {
522
+ const { session, dispose } = await this.getControlSession(agentId);
523
+ try {
524
+ const effectivePage = Math.max(1, Math.floor(page));
525
+ const effectivePageSize = Math.min(MAX_WEB_SESSION_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
526
+ const offset = (effectivePage - 1) * effectivePageSize;
527
+ const requested = Math.min(5_000, Math.max(100, (offset + effectivePageSize + 1) * 3));
528
+ const records = this.filteredSessions(session, query, requested);
529
+ return {
530
+ sessions: records.slice(offset, offset + effectivePageSize),
531
+ pagination: {
532
+ page: effectivePage,
533
+ pageSize: effectivePageSize,
534
+ hasPrevious: effectivePage > 1,
535
+ hasNext: records.length > offset + effectivePageSize,
536
+ },
537
+ };
538
+ }
539
+ finally {
540
+ if (dispose) {
541
+ session.dispose();
542
+ }
543
+ }
464
544
  }
465
545
  filteredSessions(session, query, limit) {
466
546
  const normalized = query.trim().toLowerCase();
@@ -814,11 +894,25 @@ export class RelayRuntime {
814
894
  }
815
895
  async logs(target = "connector", lines = 100) {
816
896
  if (target === "update") {
817
- const { getUpdateLogPath } = await import("./operations.js");
818
897
  return readFormattedLogTail(lines, getUpdateLogPath());
819
898
  }
899
+ if (target === "agent-updates") {
900
+ return readFormattedLogTail(lines, getAgentUpdateLogPath());
901
+ }
820
902
  return readFormattedLogTail(lines);
821
903
  }
904
+ clearLogs(target = "connector") {
905
+ const result = clearLogFile(target === "update" ? getUpdateLogPath() : target === "agent-updates" ? getAgentUpdateLogPath() : getConnectorLogPath());
906
+ this.appendActivity({
907
+ source: "web",
908
+ status: "info",
909
+ type: "logs_cleared",
910
+ threadId: null,
911
+ workspace: this.config.workspace,
912
+ detail: `Cleared ${target} log.`,
913
+ });
914
+ return { ok: true, filePath: result.filePath, clearedAt: result.clearedAt.toISOString() };
915
+ }
822
916
  restartConnector() {
823
917
  spawnConnectorRestart();
824
918
  this.broadcastStatus("Restart requested. The dashboard may disconnect briefly.", "warn");
@@ -836,6 +930,7 @@ export class RelayRuntime {
836
930
  if (this.externalMonitor) {
837
931
  clearInterval(this.externalMonitor);
838
932
  }
933
+ this.agentUpdates.cancelAll();
839
934
  this.registry.disposeAll();
840
935
  this.subscribers.clear();
841
936
  }
@@ -923,6 +1018,21 @@ export class RelayRuntime {
923
1018
  }
924
1019
  mirror.lastLine = Math.max(mirror.lastLine, snapshot.lineCount);
925
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
+ }
926
1036
  startExternalTurn(snapshot) {
927
1037
  const prompt = snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`;
928
1038
  this.chatStore.append({
@@ -1254,10 +1364,28 @@ export class RelayRuntime {
1254
1364
  this.broadcast({ type: "session_update", session: this.publicInfo(session) });
1255
1365
  }
1256
1366
  appendActivity(input) {
1257
- const event = this.activityStore.append(input);
1367
+ const event = this.activityStore.append(this.enrichActivityInput(input));
1258
1368
  this.broadcast({ type: "activity_update", events: this.activity({ limit: 50 }) });
1259
1369
  return event;
1260
1370
  }
1371
+ enrichActivityInput(input) {
1372
+ return this.enrichActivityFields(input);
1373
+ }
1374
+ enrichActivityEvent(event, info) {
1375
+ return this.enrichActivityFields(event, info);
1376
+ }
1377
+ enrichActivityFields(event, info) {
1378
+ if (!info) {
1379
+ return !event.threadId && !event.workspace ? { ...event, workspace: this.config.workspace } : event;
1380
+ }
1381
+ if (event.threadId && info.threadId && event.threadId === info.threadId) {
1382
+ return { ...event, workspace: event.workspace ?? info.workspace, agentId: event.agentId ?? info.agentId };
1383
+ }
1384
+ if (!event.threadId && !event.workspace) {
1385
+ return { ...event, workspace: this.config.workspace };
1386
+ }
1387
+ return event;
1388
+ }
1261
1389
  appendAudit(input) {
1262
1390
  return this.auditStore.append({ ...input, channelId: "web" });
1263
1391
  }
@@ -17,13 +17,27 @@ export function renderSessionInfoPlain(info) {
17
17
  capabilities.fastMode
18
18
  ? `Reasoning/Fast: ${info.reasoningEffort ?? "(model default)"} / ${info.fastMode ? "on" : "off"}`
19
19
  : `${agentReasoningLabel(agentId)}: ${info.reasoningEffort ?? "(model default)"}`,
20
- ...renderCodexUsagePlain(info, capabilities),
21
- ...renderAgentUsagePlain(info, capabilities),
22
- info.sessionTokens ? formatSessionTokensPlain(info.sessionTokens) : undefined,
20
+ ...renderSessionUsageRowsPlain(info),
23
21
  ]
24
22
  .filter((line) => Boolean(line))
25
23
  .join("\n");
26
24
  }
25
+ export function renderSessionUsageRowsPlain(info) {
26
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
27
+ return [
28
+ ...renderCodexUsagePlain(info, capabilities),
29
+ ...renderAgentUsagePlain(info, capabilities),
30
+ info.sessionTokens ? formatSessionTokensPlain(info.sessionTokens) : undefined,
31
+ ].filter((line) => Boolean(line));
32
+ }
33
+ export function renderSessionUsageRows(info) {
34
+ const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
35
+ return [
36
+ ...renderCodexUsageRows(info, capabilities),
37
+ ...renderAgentUsageRows(info, capabilities),
38
+ info.sessionTokens ? ["Session tokens", formatSessionTokensValue(info.sessionTokens)] : undefined,
39
+ ].filter((row) => Boolean(row));
40
+ }
27
41
  export function renderSessionInfoHTML(info) {
28
42
  const capabilities = info.capabilities ?? CODEX_AGENT_CAPABILITIES;
29
43
  const agentId = info.agentId ?? "codex";
@@ -117,6 +131,37 @@ function renderCodexUsageHTML(info, capabilities) {
117
131
  }
118
132
  return lines;
119
133
  }
134
+ function renderCodexUsageRows(info, capabilities) {
135
+ const usage = info.codexUsage;
136
+ if (!usage) {
137
+ return [];
138
+ }
139
+ const rows = [];
140
+ if (capabilities.usageStats && usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
141
+ rows.push([
142
+ "Context used",
143
+ `${formatPercent(usage.contextUsedPercent)} (${formatCompactTokenCount(usage.lastTokenUsage.totalTokens)} / ${formatCompactTokenCount(usage.contextWindow)})`,
144
+ ]);
145
+ }
146
+ if (capabilities.usageStats && usage.totalTokenUsage) {
147
+ rows.push([
148
+ "Tokens",
149
+ [
150
+ `in ${formatCompactTokenCount(usage.totalTokenUsage.inputTokens)}`,
151
+ `cached ${formatCompactTokenCount(usage.totalTokenUsage.cachedInputTokens)}`,
152
+ `out ${formatCompactTokenCount(usage.totalTokenUsage.outputTokens)}`,
153
+ `reasoning out ${formatCompactTokenCount(usage.totalTokenUsage.reasoningOutputTokens)}`,
154
+ ].join(" · "),
155
+ ]);
156
+ }
157
+ if (capabilities.subscriptionLimits) {
158
+ const limits = formatLimitsLeft(usage);
159
+ if (limits) {
160
+ rows.push(["Limits left", limits]);
161
+ }
162
+ }
163
+ return rows;
164
+ }
120
165
  function renderAgentUsagePlain(info, capabilities) {
121
166
  if (!capabilities.usageStats) {
122
167
  return [];
@@ -159,6 +204,30 @@ function renderAgentUsageHTML(info, capabilities) {
159
204
  }
160
205
  return lines;
161
206
  }
207
+ function renderAgentUsageRows(info, capabilities) {
208
+ if (!capabilities.usageStats) {
209
+ return [];
210
+ }
211
+ const rows = [];
212
+ if (info.contextUsage?.percent !== undefined && info.contextUsage.percent !== null) {
213
+ const contextWindow = info.contextUsage.contextWindow !== null && info.contextUsage.contextWindow !== undefined
214
+ ? ` (${formatCompactTokenCount(info.contextUsage.tokens ?? 0)} / ${formatCompactTokenCount(info.contextUsage.contextWindow)})`
215
+ : "";
216
+ rows.push(["Context used", `${formatPercent(info.contextUsage.percent)}${contextWindow}`]);
217
+ }
218
+ if (info.sessionUsage) {
219
+ rows.push([
220
+ "Tokens",
221
+ [
222
+ `in ${formatCompactTokenCount(info.sessionUsage.input)}`,
223
+ `cache read ${formatCompactTokenCount(info.sessionUsage.cacheRead)}`,
224
+ `cache write ${formatCompactTokenCount(info.sessionUsage.cacheWrite)}`,
225
+ `out ${formatCompactTokenCount(info.sessionUsage.output)}`,
226
+ ].join(" · "),
227
+ ]);
228
+ }
229
+ return rows;
230
+ }
162
231
  function formatLimitsLeft(usage) {
163
232
  const parts = [];
164
233
  if (usage.rateLimits?.primary) {
@@ -1,119 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- const SECRET_KEYS = new Set([
4
- "TELEGRAM_BOT_TOKEN",
5
- "CODEX_API_KEY",
6
- "HERMES_API_KEY",
7
- "OPENCLAW_GATEWAY_TOKEN",
8
- "OPENCLAW_GATEWAY_PASSWORD",
9
- "OPENAI_API_KEY",
10
- "TELEGRAM_WEBHOOK_SECRET",
11
- "NORDRELAY_DASHBOARD_TOKEN",
12
- "NORDRELAY_DASHBOARD_PASSWORD",
13
- ]);
14
- export const SETTING_DEFINITIONS = [
15
- setting("TELEGRAM_BOT_TOKEN", "Telegram bot token", "Telegram", "secret", "BotFather token.", true),
16
- setting("TELEGRAM_ADMIN_USER_IDS", "Telegram admin user IDs", "Telegram", "list", "Comma-separated Telegram users allowed to administer and use the bot.", true),
17
- setting("TELEGRAM_ALLOWED_USER_IDS", "Allowed operator user IDs", "Telegram", "list", "Optional non-admin operators.", true),
18
- setting("TELEGRAM_READONLY_USER_IDS", "Readonly user IDs", "Telegram", "list", "Users allowed to inspect but not mutate.", true),
19
- setting("TELEGRAM_ALLOWED_CHAT_IDS", "Allowed chat IDs", "Telegram", "list", "Optional chat allowlist.", true),
20
- setting("TELEGRAM_ALLOW_ANY_CHAT", "Allow any Telegram chat", "Telegram", "boolean", "Unsafe override; keep off for normal use.", true),
21
- setting("TELEGRAM_ROLE_POLICIES_JSON", "Role policy JSON", "Telegram", "json", "Granular Telegram permission policy.", true),
22
- setting("TELEGRAM_TRANSPORT", "Telegram transport", "Telegram", "string", "polling or webhook.", true, ["polling", "webhook"]),
23
- setting("TELEGRAM_WEBHOOK_URL", "Webhook public URL", "Telegram", "string", "Public base URL for webhook mode.", true),
24
- setting("TELEGRAM_WEBHOOK_HOST", "Webhook bind host", "Telegram", "string", "Local webhook bind host.", true),
25
- setting("TELEGRAM_WEBHOOK_PORT", "Webhook bind port", "Telegram", "number", "Local webhook bind port.", true),
26
- setting("TELEGRAM_WEBHOOK_PATH", "Webhook path", "Telegram", "string", "Webhook request path.", true),
27
- setting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "Telegram", "secret", "Optional Telegram webhook secret token.", true),
28
- setting("NORDRELAY_CODEX_ENABLED", "Enable Codex", "Agents", "boolean", "Allow Codex sessions.", true),
29
- setting("NORDRELAY_PI_ENABLED", "Enable Pi", "Agents", "boolean", "Allow Pi sessions.", true),
30
- setting("NORDRELAY_HERMES_ENABLED", "Enable Hermes", "Agents", "boolean", "Allow Hermes sessions through the Hermes API Server.", true),
31
- setting("NORDRELAY_OPENCLAW_ENABLED", "Enable OpenClaw", "Agents", "boolean", "Allow OpenClaw sessions through the OpenClaw Gateway.", true),
32
- setting("NORDRELAY_CLAUDE_CODE_ENABLED", "Enable Claude Code", "Agents", "boolean", "Allow Claude Code sessions through the Claude Agent SDK.", true),
33
- setting("NORDRELAY_DEFAULT_AGENT", "Default agent", "Agents", "string", "codex, pi, hermes, openclaw, or claude-code.", true, ["codex", "pi", "hermes", "openclaw", "claude-code"]),
34
- setting("CODEX_API_KEY", "Codex API key", "Codex", "secret", "Optional Codex SDK API key.", true),
35
- setting("CODEX_CLI_PATH", "Codex CLI path", "Codex", "string", "Optional explicit Codex executable path.", true),
36
- setting("CODEX_USE_BUNDLED_CLI", "Use bundled Codex CLI", "Codex", "boolean", "Force SDK-bundled CLI instead of host CLI.", true),
37
- setting("CODEX_MODEL", "Default Codex model", "Codex", "string", "Default model for new Codex threads.", false),
38
- setting("CODEX_SYNC_INTERVAL_MS", "Codex sync interval", "Codex", "number", "Local state sync interval.", true),
39
- setting("CODEX_EXTERNAL_BUSY_CHECK_MS", "External busy check", "Codex", "number", "External CLI busy polling interval.", true),
40
- setting("CODEX_EXTERNAL_BUSY_STALE_MS", "External busy stale timeout", "Codex", "number", "External CLI stale timeout.", true),
41
- 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"]),
42
- setting("CODEX_APPROVAL_POLICY", "Codex approval policy", "Codex", "string", "never, on-request, on-failure, or untrusted.", true, ["never", "on-request", "on-failure", "untrusted"]),
43
- setting("CODEX_LAUNCH_PROFILES_JSON", "Launch profiles JSON", "Codex", "json", "Additional launch profile definitions.", true),
44
- setting("CODEX_DEFAULT_LAUNCH_PROFILE", "Default launch profile", "Codex", "string", "Launch profile ID used by default.", true),
45
- setting("ENABLE_UNSAFE_LAUNCH_PROFILES", "Enable unsafe profiles", "Codex", "boolean", "Expose danger-full-access profiles.", true),
46
- setting("PI_CLI_PATH", "Pi CLI path", "Pi", "string", "Optional Pi executable path.", true),
47
- setting("PI_SESSION_DIR", "Pi session dir", "Pi", "string", "Optional Pi session directory.", true),
48
- setting("PI_DEFAULT_MODEL", "Default Pi model", "Pi", "string", "Default Pi model slug.", false),
49
- setting("PI_DEFAULT_THINKING", "Default Pi thinking", "Pi", "string", "off, minimal, low, medium, high, or xhigh.", false, ["off", "minimal", "low", "medium", "high", "xhigh"]),
50
- 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"]),
51
- setting("HERMES_CLI_PATH", "Hermes CLI path", "Hermes", "string", "Optional Hermes executable path.", true),
52
- setting("HERMES_HOME", "Hermes home", "Hermes", "string", "Optional Hermes home directory. Defaults to ~/.hermes.", true),
53
- setting("HERMES_STATE_DB_PATH", "Hermes state DB path", "Hermes", "string", "Optional explicit Hermes state.db path.", true),
54
- setting("HERMES_API_BASE_URL", "Hermes API base URL", "Hermes", "string", "Hermes API Server base URL.", true),
55
- setting("HERMES_API_KEY", "Hermes API key", "Hermes", "secret", "Bearer token for the Hermes API Server.", true),
56
- setting("HERMES_DEFAULT_MODEL", "Default Hermes model", "Hermes", "string", "Default model label sent to Hermes API runs.", false),
57
- setting("HERMES_DEFAULT_REASONING", "Default Hermes reasoning", "Hermes", "string", "none, minimal, low, medium, high, or xhigh.", false, ["none", "minimal", "low", "medium", "high", "xhigh"]),
58
- setting("HERMES_DEFAULT_PROFILE", "Default Hermes profile", "Hermes", "string", "default, safe, readonly, or yolo.", true, ["default", "safe", "readonly", "yolo"]),
59
- setting("OPENCLAW_CLI_PATH", "OpenClaw CLI path", "OpenClaw", "string", "Optional OpenClaw executable path.", true),
60
- setting("OPENCLAW_GATEWAY_URL", "OpenClaw Gateway URL", "OpenClaw", "string", "OpenClaw Gateway WebSocket URL.", true),
61
- setting("OPENCLAW_GATEWAY_TOKEN", "OpenClaw Gateway token", "OpenClaw", "secret", "Shared-secret token for the OpenClaw Gateway.", true),
62
- setting("OPENCLAW_GATEWAY_PASSWORD", "OpenClaw Gateway password", "OpenClaw", "secret", "Shared-secret password for the OpenClaw Gateway.", true),
63
- setting("OPENCLAW_AGENT_ID", "OpenClaw agent ID", "OpenClaw", "string", "Configured OpenClaw agent id, for example main or work.", false),
64
- setting("OPENCLAW_HOME", "OpenClaw home", "OpenClaw", "string", "Optional OpenClaw home directory. Defaults to ~/.openclaw.", true),
65
- setting("OPENCLAW_STATE_DIR", "OpenClaw state dir", "OpenClaw", "string", "Optional OpenClaw state directory.", true),
66
- setting("OPENCLAW_DEFAULT_MODEL", "Default OpenClaw model", "OpenClaw", "string", "Default OpenClaw model id.", false),
67
- setting("OPENCLAW_DEFAULT_THINKING", "Default OpenClaw thinking", "OpenClaw", "string", "off, minimal, low, medium, high, or xhigh.", false, ["off", "minimal", "low", "medium", "high", "xhigh"]),
68
- setting("OPENCLAW_DEFAULT_PROFILE", "Default OpenClaw profile", "OpenClaw", "string", "default, safe, readonly, local, or deliver.", true, ["default", "safe", "readonly", "local", "deliver"]),
69
- 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),
70
- setting("CLAUDE_CONFIG_DIR", "Claude config dir", "Claude Code", "string", "Optional Claude config directory. Defaults to ~/.claude.", true),
71
- setting("CLAUDE_CODE_DEFAULT_MODEL", "Default Claude Code model", "Claude Code", "string", "Default Claude Code model alias or model id.", false),
72
- setting("CLAUDE_CODE_DEFAULT_EFFORT", "Default Claude Code effort", "Claude Code", "string", "off, low, medium, high, or xhigh.", false, ["off", "low", "medium", "high", "xhigh"]),
73
- 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"]),
74
- setting("CLAUDE_CODE_MAX_TURNS", "Claude Code max turns", "Claude Code", "number", "Maximum agentic turns for each Claude Code prompt.", false),
75
- setting("CONNECTOR_LOG_FORMAT", "Log format", "Operations", "string", "text or json.", true, ["text", "json"]),
76
- setting("TOOL_VERBOSITY", "Tool verbosity", "Operations", "string", "all, summary, errors-only, or none.", false, ["all", "summary", "errors-only", "none"]),
77
- setting("SHOW_TURN_TOKEN_USAGE", "Show turn token usage", "Operations", "boolean", "Append per-turn token usage.", false),
78
- setting("ENABLE_TELEGRAM_LOGIN", "Enable Telegram login", "Operations", "boolean", "Allow /login and /logout.", true),
79
- setting("ENABLE_TELEGRAM_REACTIONS", "Enable Telegram reactions", "Operations", "boolean", "Send Telegram reactions.", true),
80
- setting("TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS", "Telegram send interval", "Operations", "number", "Minimum send interval.", true),
81
- setting("TELEGRAM_EDIT_MIN_INTERVAL_MS", "Telegram edit interval", "Operations", "number", "Minimum edit interval.", true),
82
- setting("TELEGRAM_CLI_MIRROR_MODE", "CLI mirror mode", "Operations", "string", "off, status, final, or full.", false, ["off", "status", "final", "full"]),
83
- setting("TELEGRAM_CLI_MIRROR_MIN_UPDATE_MS", "CLI mirror update interval", "Operations", "number", "Minimum mirrored edit interval.", true),
84
- setting("TELEGRAM_NOTIFY_MODE", "Notify mode", "Operations", "string", "off, minimal, or all.", false, ["off", "minimal", "all"]),
85
- setting("TELEGRAM_QUIET_HOURS", "Quiet hours", "Operations", "string", "HH-HH or blank.", false),
86
- setting("TELEGRAM_REDACT_PATTERNS", "Redaction patterns", "Operations", "list", "Additional comma-separated regex patterns.", true),
87
- setting("NORDRELAY_UPDATE_METHOD", "Update method", "Operations", "string", "auto, npm, or git.", true, ["auto", "npm", "git"]),
88
- setting("MAX_FILE_SIZE", "Max file size", "Artifacts", "number", "Max inbound/outbound file size.", true),
89
- setting("ARTIFACT_RETENTION_DAYS", "Artifact retention days", "Artifacts", "number", "Days before pruning.", true),
90
- setting("ARTIFACT_MAX_TURNS", "Max artifact turns", "Artifacts", "number", "Maximum artifact turns retained.", true),
91
- setting("ARTIFACT_MAX_INBOX_DIRS", "Max inbox dirs", "Artifacts", "number", "Maximum inbox dirs retained.", true),
92
- setting("ARTIFACT_IGNORE_DIRS", "Artifact ignore dirs", "Artifacts", "list", "Extra ignored dirs or relative paths.", true),
93
- setting("ARTIFACT_IGNORE_GLOBS", "Artifact ignore globs", "Artifacts", "list", "Extra ignored glob patterns.", true),
94
- setting("TELEGRAM_AUTO_SEND_ARTIFACTS", "Auto-send artifacts", "Artifacts", "boolean", "Automatically send artifact files.", false),
95
- setting("WORKSPACE_ALLOWED_ROOTS", "Workspace allowed roots", "Workspace", "list", "Restrict selectable workspaces.", true),
96
- setting("WORKSPACE_WARN_ROOTS", "Workspace warn roots", "Workspace", "list", "Warn for broad workspace roots.", true),
97
- setting("NORDRELAY_STATE_BACKEND", "State backend", "Workspace", "string", "json or sqlite.", true, ["json", "sqlite"]),
98
- setting("NORDRELAY_AUDIT_MAX_EVENTS", "Audit max events", "Workspace", "number", "Retained audit events.", true),
99
- setting("NORDRELAY_SESSION_LOCK_TTL_MS", "Session lock TTL", "Workspace", "number", "Write-lock TTL.", true),
100
- setting("NORDRELAY_VERSION_CACHE_TTL_MS", "Version cache TTL", "Workspace", "number", "NPM version cache TTL.", true),
101
- setting("OPENAI_API_KEY", "OpenAI API key", "Voice", "secret", "Whisper fallback API key.", true),
102
- setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
103
- setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
104
- setting("VOICE_TRANSCRIBE_ONLY", "Voice transcribe only", "Voice", "boolean", "Do not send voice transcripts as prompts.", false),
105
- setting("FASTER_WHISPER_PYTHON", "faster-whisper Python", "Voice", "string", "Python executable.", true),
106
- setting("FASTER_WHISPER_MODEL", "faster-whisper model", "Voice", "string", "Model name.", true),
107
- setting("FASTER_WHISPER_DEVICE", "faster-whisper device", "Voice", "string", "cpu, cuda, etc.", true),
108
- setting("FASTER_WHISPER_COMPUTE_TYPE", "faster-whisper compute type", "Voice", "string", "int8, float16, etc.", true),
109
- setting("FASTER_WHISPER_LANGUAGE", "faster-whisper language", "Voice", "string", "Fixed transcription language.", true),
110
- setting("FASTER_WHISPER_TIMEOUT_MS", "faster-whisper timeout", "Voice", "number", "Transcription timeout.", true),
111
- setting("NORDRELAY_DASHBOARD_TOKEN", "Dashboard token", "Dashboard", "secret", "Bearer/login token for WebUI.", true),
112
- setting("NORDRELAY_DASHBOARD_USER", "Dashboard user", "Dashboard", "string", "Optional Basic Auth user.", true),
113
- setting("NORDRELAY_DASHBOARD_PASSWORD", "Dashboard password", "Dashboard", "secret", "Optional Basic Auth password.", true),
114
- setting("NORDRELAY_DASHBOARD_HOST", "Dashboard host", "Dashboard", "string", "WebUI bind host.", true),
115
- setting("NORDRELAY_DASHBOARD_PORT", "Dashboard port", "Dashboard", "number", "WebUI bind port.", true),
116
- ];
3
+ import { SECRET_KEYS, SETTING_DEFINITIONS } from "./config-metadata.js";
4
+ export { SETTING_DEFINITIONS } from "./config-metadata.js";
117
5
  export class SettingsService {
118
6
  envPath;
119
7
  constructor(envPath) {
@@ -193,9 +81,6 @@ export function maskSecret(value) {
193
81
  }
194
82
  return `${value.slice(0, 4)}...${value.slice(-4)}`;
195
83
  }
196
- function setting(key, label, group, kind, description, restartRequired, options) {
197
- return { key, label, group, kind, description, restartRequired, options };
198
- }
199
84
  function validateSettingValue(definition, value) {
200
85
  if (definition.kind === "number" && !Number.isFinite(Number(value))) {
201
86
  return "Must be a number.";