@nordbyte/nordrelay 0.3.1 → 0.4.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.
Files changed (55) hide show
  1. package/.env.example +45 -2
  2. package/README.md +221 -35
  3. package/dist/access-control.js +3 -0
  4. package/dist/agent-activity.js +300 -0
  5. package/dist/agent-adapter.js +17 -30
  6. package/dist/agent-factory.js +27 -0
  7. package/dist/agent-feature-matrix.js +42 -0
  8. package/dist/agent-updates.js +294 -0
  9. package/dist/agent.js +123 -9
  10. package/dist/artifacts.js +1 -1
  11. package/dist/audit-log.js +1 -1
  12. package/dist/bot-ui.js +1 -1
  13. package/dist/bot.js +483 -354
  14. package/dist/channel-actions.js +372 -0
  15. package/dist/claude-code-auth.js +121 -0
  16. package/dist/claude-code-cli.js +19 -0
  17. package/dist/claude-code-launch.js +73 -0
  18. package/dist/claude-code-session.js +660 -0
  19. package/dist/claude-code-state.js +590 -0
  20. package/dist/codex-session.js +12 -1
  21. package/dist/config.js +113 -9
  22. package/dist/hermes-api.js +150 -0
  23. package/dist/hermes-auth.js +96 -0
  24. package/dist/hermes-cli.js +19 -0
  25. package/dist/hermes-launch.js +57 -0
  26. package/dist/hermes-session.js +477 -0
  27. package/dist/hermes-state.js +609 -0
  28. package/dist/index.js +51 -8
  29. package/dist/openclaw-auth.js +27 -0
  30. package/dist/openclaw-cli.js +19 -0
  31. package/dist/openclaw-gateway.js +285 -0
  32. package/dist/openclaw-launch.js +65 -0
  33. package/dist/openclaw-session.js +549 -0
  34. package/dist/openclaw-state.js +409 -0
  35. package/dist/operations.js +115 -9
  36. package/dist/pi-auth.js +59 -0
  37. package/dist/pi-launch.js +61 -0
  38. package/dist/pi-rpc.js +18 -0
  39. package/dist/pi-session.js +103 -15
  40. package/dist/pi-state.js +253 -0
  41. package/dist/relay-runtime.js +798 -72
  42. package/dist/session-format.js +98 -19
  43. package/dist/session-registry.js +40 -15
  44. package/dist/settings-service.js +35 -4
  45. package/dist/web-dashboard-assets.js +2 -0
  46. package/dist/web-dashboard-client.js +275 -0
  47. package/dist/web-dashboard-style.js +9 -0
  48. package/dist/web-dashboard-ui.js +18 -0
  49. package/dist/web-dashboard.js +296 -196
  50. package/package.json +8 -3
  51. package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
  52. package/plugins/nordrelay/commands/remote.md +2 -2
  53. package/plugins/nordrelay/scripts/nordrelay.mjs +187 -12
  54. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
  55. package/CHANGELOG.md +0 -26
@@ -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),
21
- ...renderAgentUsagePlain(info),
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";
@@ -42,8 +56,8 @@ export function renderSessionInfoHTML(info) {
42
56
  capabilities.fastMode
43
57
  ? `<b>Reasoning/Fast:</b> <code>${escapeHTML(info.reasoningEffort ?? "(model default)")} / ${info.fastMode ? "on" : "off"}</code>`
44
58
  : `<b>${escapeHTML(agentReasoningLabel(agentId))}:</b> <code>${escapeHTML(info.reasoningEffort ?? "(model default)")}</code>`,
45
- ...renderCodexUsageHTML(info),
46
- ...renderAgentUsageHTML(info),
59
+ ...renderCodexUsageHTML(info, capabilities),
60
+ ...renderAgentUsageHTML(info, capabilities),
47
61
  info.sessionTokens ? `<b>Session tokens:</b> <code>${escapeHTML(formatSessionTokensValue(info.sessionTokens))}</code>` : undefined,
48
62
  ]
49
63
  .filter((line) => Boolean(line))
@@ -67,16 +81,16 @@ export function formatFileSize(bytes) {
67
81
  }
68
82
  return `${(bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "")} MB`;
69
83
  }
70
- function renderCodexUsagePlain(info) {
84
+ function renderCodexUsagePlain(info, capabilities) {
71
85
  const usage = info.codexUsage;
72
86
  if (!usage) {
73
87
  return [];
74
88
  }
75
89
  const lines = [];
76
- if (usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
90
+ if (capabilities.usageStats && usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
77
91
  lines.push(`Context used: ${formatPercent(usage.contextUsedPercent)} (${formatCompactTokenCount(usage.lastTokenUsage.totalTokens)} / ${formatCompactTokenCount(usage.contextWindow)})`);
78
92
  }
79
- if (usage.totalTokenUsage) {
93
+ if (capabilities.usageStats && usage.totalTokenUsage) {
80
94
  lines.push([
81
95
  `Tokens in: ${formatCompactTokenCount(usage.totalTokenUsage.inputTokens)}`,
82
96
  `cached: ${formatCompactTokenCount(usage.totalTokenUsage.cachedInputTokens)}`,
@@ -84,22 +98,24 @@ function renderCodexUsagePlain(info) {
84
98
  `reasoning out: ${formatCompactTokenCount(usage.totalTokenUsage.reasoningOutputTokens)}`,
85
99
  ].join(" · "));
86
100
  }
87
- const limits = formatLimitsLeft(usage);
88
- if (limits) {
89
- lines.push(`Limits left: ${limits}`);
101
+ if (capabilities.subscriptionLimits) {
102
+ const limits = formatLimitsLeft(usage);
103
+ if (limits) {
104
+ lines.push(`Limits left: ${limits}`);
105
+ }
90
106
  }
91
107
  return lines;
92
108
  }
93
- function renderCodexUsageHTML(info) {
109
+ function renderCodexUsageHTML(info, capabilities) {
94
110
  const usage = info.codexUsage;
95
111
  if (!usage) {
96
112
  return [];
97
113
  }
98
114
  const lines = [];
99
- if (usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
115
+ if (capabilities.usageStats && usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
100
116
  lines.push(`<b>Context used:</b> <code>${escapeHTML(formatPercent(usage.contextUsedPercent))}</code> <i>(${escapeHTML(formatCompactTokenCount(usage.lastTokenUsage.totalTokens))} / ${escapeHTML(formatCompactTokenCount(usage.contextWindow))})</i>`);
101
117
  }
102
- if (usage.totalTokenUsage) {
118
+ if (capabilities.usageStats && usage.totalTokenUsage) {
103
119
  lines.push(`<b>Tokens:</b> <code>${escapeHTML([
104
120
  `in ${formatCompactTokenCount(usage.totalTokenUsage.inputTokens)}`,
105
121
  `cached ${formatCompactTokenCount(usage.totalTokenUsage.cachedInputTokens)}`,
@@ -107,13 +123,49 @@ function renderCodexUsageHTML(info) {
107
123
  `reasoning out ${formatCompactTokenCount(usage.totalTokenUsage.reasoningOutputTokens)}`,
108
124
  ].join(" · "))}</code>`);
109
125
  }
110
- const limits = formatLimitsLeft(usage);
111
- if (limits) {
112
- lines.push(`<b>Limits left:</b> <code>${escapeHTML(limits)}</code>`);
126
+ if (capabilities.subscriptionLimits) {
127
+ const limits = formatLimitsLeft(usage);
128
+ if (limits) {
129
+ lines.push(`<b>Limits left:</b> <code>${escapeHTML(limits)}</code>`);
130
+ }
113
131
  }
114
132
  return lines;
115
133
  }
116
- function renderAgentUsagePlain(info) {
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
+ }
165
+ function renderAgentUsagePlain(info, capabilities) {
166
+ if (!capabilities.usageStats) {
167
+ return [];
168
+ }
117
169
  const lines = [];
118
170
  if (info.contextUsage?.percent !== undefined && info.contextUsage.percent !== null) {
119
171
  const contextWindow = info.contextUsage.contextWindow !== null && info.contextUsage.contextWindow !== undefined
@@ -131,7 +183,10 @@ function renderAgentUsagePlain(info) {
131
183
  }
132
184
  return lines;
133
185
  }
134
- function renderAgentUsageHTML(info) {
186
+ function renderAgentUsageHTML(info, capabilities) {
187
+ if (!capabilities.usageStats) {
188
+ return [];
189
+ }
135
190
  const lines = [];
136
191
  if (info.contextUsage?.percent !== undefined && info.contextUsage.percent !== null) {
137
192
  const contextWindow = info.contextUsage.contextWindow !== null && info.contextUsage.contextWindow !== undefined
@@ -149,6 +204,30 @@ function renderAgentUsageHTML(info) {
149
204
  }
150
205
  return lines;
151
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
+ }
152
231
  function formatLimitsLeft(usage) {
153
232
  const parts = [];
154
233
  if (usage.rateLimits?.primary) {
@@ -64,7 +64,7 @@ export class SessionRegistry {
64
64
  agentId,
65
65
  threadId: null,
66
66
  workspace: previous?.workspace ?? this.config.workspace,
67
- pinnedThreadIds: previous?.pinnedThreadIds,
67
+ pinnedThreadIdsByAgent: previous?.pinnedThreadIdsByAgent,
68
68
  updatedAt: Date.now(),
69
69
  };
70
70
  this.metadata.set(contextKey, next);
@@ -74,9 +74,12 @@ export class SessionRegistry {
74
74
  updateMetadata(contextKey, session) {
75
75
  const info = session.getInfo();
76
76
  const previous = this.metadata.get(contextKey);
77
- const pinnedThreadIds = previous?.pinnedThreadIds ?? [];
77
+ const agentId = info.agentId ?? "codex";
78
+ const previousPinnedByAgent = previous?.pinnedThreadIdsByAgent ?? {};
79
+ const pinnedThreadIds = previousPinnedByAgent[agentId] ?? previous?.pinnedThreadIds ?? [];
78
80
  const next = {
79
81
  contextKey,
82
+ agentId,
80
83
  threadId: info.threadId,
81
84
  workspace: info.workspace,
82
85
  model: info.model,
@@ -84,49 +87,64 @@ export class SessionRegistry {
84
87
  launchProfileId: info.nextLaunchProfileId ?? info.launchProfileId,
85
88
  updatedAt: Date.now(),
86
89
  };
87
- if (info.agentId && info.agentId !== "codex") {
88
- next.agentId = info.agentId;
89
- }
90
90
  if (info.sessionPath) {
91
91
  next.sessionPath = info.sessionPath;
92
92
  }
93
+ const nextPinnedByAgent = { ...previousPinnedByAgent };
93
94
  if (pinnedThreadIds.length > 0) {
94
- next.pinnedThreadIds = pinnedThreadIds;
95
+ nextPinnedByAgent[agentId] = pinnedThreadIds;
96
+ }
97
+ else {
98
+ delete nextPinnedByAgent[agentId];
99
+ }
100
+ if (Object.keys(nextPinnedByAgent).length > 0) {
101
+ next.pinnedThreadIdsByAgent = nextPinnedByAgent;
95
102
  }
96
103
  this.metadata.set(contextKey, next);
97
104
  this.persistMetadata();
98
105
  }
99
106
  pinThread(contextKey, threadId) {
100
107
  const meta = this.metadata.get(contextKey) ?? this.createEmptyMetadata(contextKey);
101
- const pinned = new Set(meta.pinnedThreadIds ?? []);
108
+ const agentId = meta.agentId ?? this.config.defaultAgent ?? "codex";
109
+ const pinnedByAgent = meta.pinnedThreadIdsByAgent ?? {};
110
+ const pinned = new Set(pinnedByAgent[agentId] ?? meta.pinnedThreadIds ?? []);
102
111
  pinned.add(threadId);
103
- meta.pinnedThreadIds = [...pinned];
112
+ meta.pinnedThreadIdsByAgent = { ...pinnedByAgent, [agentId]: [...pinned] };
113
+ delete meta.pinnedThreadIds;
104
114
  meta.updatedAt = Date.now();
105
115
  this.metadata.set(contextKey, meta);
106
116
  this.persistMetadata();
107
- return meta.pinnedThreadIds;
117
+ return meta.pinnedThreadIdsByAgent[agentId] ?? [];
108
118
  }
109
119
  unpinThread(contextKey, threadId) {
110
120
  const meta = this.metadata.get(contextKey) ?? this.createEmptyMetadata(contextKey);
111
- meta.pinnedThreadIds = (meta.pinnedThreadIds ?? []).filter((id) => id !== threadId);
121
+ const agentId = meta.agentId ?? this.config.defaultAgent ?? "codex";
122
+ const pinnedByAgent = meta.pinnedThreadIdsByAgent ?? {};
123
+ meta.pinnedThreadIdsByAgent = {
124
+ ...pinnedByAgent,
125
+ [agentId]: (pinnedByAgent[agentId] ?? meta.pinnedThreadIds ?? []).filter((id) => id !== threadId),
126
+ };
127
+ delete meta.pinnedThreadIds;
112
128
  meta.updatedAt = Date.now();
113
129
  this.metadata.set(contextKey, meta);
114
130
  this.persistMetadata();
115
- return meta.pinnedThreadIds;
131
+ return meta.pinnedThreadIdsByAgent[agentId] ?? [];
116
132
  }
117
133
  listPinnedThreadIds(contextKey) {
118
- return [...(this.metadata.get(contextKey)?.pinnedThreadIds ?? [])];
134
+ const meta = this.metadata.get(contextKey);
135
+ const agentId = meta?.agentId ?? this.config.defaultAgent ?? "codex";
136
+ return [...(meta?.pinnedThreadIdsByAgent?.[agentId] ?? meta?.pinnedThreadIds ?? [])];
119
137
  }
120
138
  listContexts() {
121
139
  return [...this.metadata.values()].sort((left, right) => right.updatedAt - left.updatedAt);
122
140
  }
123
- syncAllFromCodexState(options = {}) {
141
+ syncAllFromAgentState(options = {}) {
124
142
  const results = [];
125
143
  for (const [contextKey, session] of this.sessions.entries()) {
126
144
  if (!(session.getInfo().capabilities ?? CODEX_AGENT_CAPABILITIES).externalActivity) {
127
145
  continue;
128
146
  }
129
- const result = session.syncFromCodexState(options);
147
+ const result = session.syncFromAgentState(options);
130
148
  if (result.changed) {
131
149
  this.updateMetadata(contextKey, session);
132
150
  }
@@ -168,7 +186,10 @@ export class SessionRegistry {
168
186
  }
169
187
  for (const entry of data) {
170
188
  if (entry.contextKey) {
171
- this.metadata.set(entry.contextKey, entry);
189
+ this.metadata.set(entry.contextKey, {
190
+ ...entry,
191
+ agentId: entry.agentId ?? "codex",
192
+ });
172
193
  }
173
194
  }
174
195
  }
@@ -179,6 +200,7 @@ export class SessionRegistry {
179
200
  createEmptyMetadata(contextKey) {
180
201
  return {
181
202
  contextKey,
203
+ agentId: this.config.defaultAgent ?? "codex",
182
204
  threadId: null,
183
205
  workspace: this.config.workspace,
184
206
  launchProfileId: this.config.defaultLaunchProfileId,
@@ -191,6 +213,9 @@ function resolveLaunchProfileId(config, meta) {
191
213
  if (!meta?.launchProfileId) {
192
214
  return undefined;
193
215
  }
216
+ if (meta.agentId === "pi" || meta.agentId === "hermes" || meta.agentId === "openclaw" || meta.agentId === "claude-code") {
217
+ return meta.launchProfileId;
218
+ }
194
219
  if (findLaunchProfile(config.launchProfiles, meta.launchProfileId)) {
195
220
  return meta.launchProfileId;
196
221
  }
@@ -3,6 +3,9 @@ import path from "node:path";
3
3
  const SECRET_KEYS = new Set([
4
4
  "TELEGRAM_BOT_TOKEN",
5
5
  "CODEX_API_KEY",
6
+ "HERMES_API_KEY",
7
+ "OPENCLAW_GATEWAY_TOKEN",
8
+ "OPENCLAW_GATEWAY_PASSWORD",
6
9
  "OPENAI_API_KEY",
7
10
  "TELEGRAM_WEBHOOK_SECRET",
8
11
  "NORDRELAY_DASHBOARD_TOKEN",
@@ -24,7 +27,10 @@ export const SETTING_DEFINITIONS = [
24
27
  setting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "Telegram", "secret", "Optional Telegram webhook secret token.", true),
25
28
  setting("NORDRELAY_CODEX_ENABLED", "Enable Codex", "Agents", "boolean", "Allow Codex sessions.", true),
26
29
  setting("NORDRELAY_PI_ENABLED", "Enable Pi", "Agents", "boolean", "Allow Pi sessions.", true),
27
- setting("NORDRELAY_DEFAULT_AGENT", "Default agent", "Agents", "string", "codex or pi.", true, ["codex", "pi"]),
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"]),
28
34
  setting("CODEX_API_KEY", "Codex API key", "Codex", "secret", "Optional Codex SDK API key.", true),
29
35
  setting("CODEX_CLI_PATH", "Codex CLI path", "Codex", "string", "Optional explicit Codex executable path.", true),
30
36
  setting("CODEX_USE_BUNDLED_CLI", "Use bundled Codex CLI", "Codex", "boolean", "Force SDK-bundled CLI instead of host CLI.", true),
@@ -41,6 +47,31 @@ export const SETTING_DEFINITIONS = [
41
47
  setting("PI_SESSION_DIR", "Pi session dir", "Pi", "string", "Optional Pi session directory.", true),
42
48
  setting("PI_DEFAULT_MODEL", "Default Pi model", "Pi", "string", "Default Pi model slug.", false),
43
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),
44
75
  setting("CONNECTOR_LOG_FORMAT", "Log format", "Operations", "string", "text or json.", true, ["text", "json"]),
45
76
  setting("TOOL_VERBOSITY", "Tool verbosity", "Operations", "string", "all, summary, errors-only, or none.", false, ["all", "summary", "errors-only", "none"]),
46
77
  setting("SHOW_TURN_TOKEN_USAGE", "Show turn token usage", "Operations", "boolean", "Append per-turn token usage.", false),
@@ -88,15 +119,15 @@ export class SettingsService {
88
119
  constructor(envPath) {
89
120
  this.envPath = envPath;
90
121
  }
91
- async snapshot(env = process.env) {
122
+ async snapshot(env = process.env, activeValues = {}) {
92
123
  const parsed = await readEnvFile(this.envPath);
93
124
  const settings = SETTING_DEFINITIONS.map((definition) => {
94
125
  const configuredValue = parsed[definition.key];
95
- const effectiveValue = configuredValue ?? env[definition.key] ?? "";
126
+ const effectiveValue = configuredValue ?? activeValues[definition.key] ?? env[definition.key] ?? "";
96
127
  const masked = SECRET_KEYS.has(definition.key) && Boolean(effectiveValue);
97
128
  return {
98
129
  ...definition,
99
- value: masked ? maskSecret(effectiveValue) : effectiveValue,
130
+ value: configuredValue === undefined ? "" : SECRET_KEYS.has(definition.key) && configuredValue ? maskSecret(configuredValue) : configuredValue,
100
131
  effectiveValue: masked ? maskSecret(effectiveValue) : effectiveValue,
101
132
  configured: configuredValue !== undefined,
102
133
  masked,
@@ -0,0 +1,2 @@
1
+ export { dashboardJs } from "./web-dashboard-client.js";
2
+ export { dashboardCss } from "./web-dashboard-style.js";