@nordbyte/nordrelay 0.3.0 → 0.4.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.
- package/.env.example +45 -2
- package/README.md +227 -47
- package/dist/agent-activity.js +300 -0
- package/dist/agent-adapter.js +17 -30
- package/dist/agent-factory.js +27 -0
- package/dist/agent.js +123 -9
- package/dist/artifacts.js +1 -1
- package/dist/audit-log.js +1 -1
- package/dist/bot-ui.js +1 -1
- package/dist/bot.js +333 -161
- package/dist/claude-code-auth.js +121 -0
- package/dist/claude-code-cli.js +19 -0
- package/dist/claude-code-launch.js +73 -0
- package/dist/claude-code-session.js +660 -0
- package/dist/claude-code-state.js +590 -0
- package/dist/codex-session.js +15 -2
- package/dist/config.js +113 -9
- package/dist/context-key.js +23 -0
- package/dist/hermes-api.js +150 -0
- package/dist/hermes-auth.js +96 -0
- package/dist/hermes-cli.js +19 -0
- package/dist/hermes-launch.js +57 -0
- package/dist/hermes-session.js +477 -0
- package/dist/hermes-state.js +609 -0
- package/dist/index.js +51 -8
- package/dist/openclaw-auth.js +27 -0
- package/dist/openclaw-cli.js +19 -0
- package/dist/openclaw-gateway.js +285 -0
- package/dist/openclaw-launch.js +65 -0
- package/dist/openclaw-session.js +549 -0
- package/dist/openclaw-state.js +409 -0
- package/dist/operations.js +84 -3
- package/dist/pi-auth.js +59 -0
- package/dist/pi-launch.js +61 -0
- package/dist/pi-rpc.js +18 -0
- package/dist/pi-session.js +103 -15
- package/dist/pi-state.js +253 -0
- package/dist/relay-runtime.js +1073 -22
- package/dist/session-format.js +28 -18
- package/dist/session-registry.js +43 -18
- package/dist/settings-service.js +80 -26
- package/dist/state-backend.js +17 -8
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +463 -55
- package/dist/web-state.js +131 -0
- package/docker-compose.yml +1 -1
- package/package.json +8 -3
- package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
- package/plugins/nordrelay/commands/remote.md +2 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +173 -20
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -17
package/dist/session-format.js
CHANGED
|
@@ -17,8 +17,8 @@ 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),
|
|
20
|
+
...renderCodexUsagePlain(info, capabilities),
|
|
21
|
+
...renderAgentUsagePlain(info, capabilities),
|
|
22
22
|
info.sessionTokens ? formatSessionTokensPlain(info.sessionTokens) : undefined,
|
|
23
23
|
]
|
|
24
24
|
.filter((line) => Boolean(line))
|
|
@@ -42,8 +42,8 @@ export function renderSessionInfoHTML(info) {
|
|
|
42
42
|
capabilities.fastMode
|
|
43
43
|
? `<b>Reasoning/Fast:</b> <code>${escapeHTML(info.reasoningEffort ?? "(model default)")} / ${info.fastMode ? "on" : "off"}</code>`
|
|
44
44
|
: `<b>${escapeHTML(agentReasoningLabel(agentId))}:</b> <code>${escapeHTML(info.reasoningEffort ?? "(model default)")}</code>`,
|
|
45
|
-
...renderCodexUsageHTML(info),
|
|
46
|
-
...renderAgentUsageHTML(info),
|
|
45
|
+
...renderCodexUsageHTML(info, capabilities),
|
|
46
|
+
...renderAgentUsageHTML(info, capabilities),
|
|
47
47
|
info.sessionTokens ? `<b>Session tokens:</b> <code>${escapeHTML(formatSessionTokensValue(info.sessionTokens))}</code>` : undefined,
|
|
48
48
|
]
|
|
49
49
|
.filter((line) => Boolean(line))
|
|
@@ -67,16 +67,16 @@ export function formatFileSize(bytes) {
|
|
|
67
67
|
}
|
|
68
68
|
return `${(bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "")} MB`;
|
|
69
69
|
}
|
|
70
|
-
function renderCodexUsagePlain(info) {
|
|
70
|
+
function renderCodexUsagePlain(info, capabilities) {
|
|
71
71
|
const usage = info.codexUsage;
|
|
72
72
|
if (!usage) {
|
|
73
73
|
return [];
|
|
74
74
|
}
|
|
75
75
|
const lines = [];
|
|
76
|
-
if (usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
|
|
76
|
+
if (capabilities.usageStats && usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
|
|
77
77
|
lines.push(`Context used: ${formatPercent(usage.contextUsedPercent)} (${formatCompactTokenCount(usage.lastTokenUsage.totalTokens)} / ${formatCompactTokenCount(usage.contextWindow)})`);
|
|
78
78
|
}
|
|
79
|
-
if (usage.totalTokenUsage) {
|
|
79
|
+
if (capabilities.usageStats && usage.totalTokenUsage) {
|
|
80
80
|
lines.push([
|
|
81
81
|
`Tokens in: ${formatCompactTokenCount(usage.totalTokenUsage.inputTokens)}`,
|
|
82
82
|
`cached: ${formatCompactTokenCount(usage.totalTokenUsage.cachedInputTokens)}`,
|
|
@@ -84,22 +84,24 @@ function renderCodexUsagePlain(info) {
|
|
|
84
84
|
`reasoning out: ${formatCompactTokenCount(usage.totalTokenUsage.reasoningOutputTokens)}`,
|
|
85
85
|
].join(" · "));
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
if (capabilities.subscriptionLimits) {
|
|
88
|
+
const limits = formatLimitsLeft(usage);
|
|
89
|
+
if (limits) {
|
|
90
|
+
lines.push(`Limits left: ${limits}`);
|
|
91
|
+
}
|
|
90
92
|
}
|
|
91
93
|
return lines;
|
|
92
94
|
}
|
|
93
|
-
function renderCodexUsageHTML(info) {
|
|
95
|
+
function renderCodexUsageHTML(info, capabilities) {
|
|
94
96
|
const usage = info.codexUsage;
|
|
95
97
|
if (!usage) {
|
|
96
98
|
return [];
|
|
97
99
|
}
|
|
98
100
|
const lines = [];
|
|
99
|
-
if (usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
|
|
101
|
+
if (capabilities.usageStats && usage.contextUsedPercent !== null && usage.contextWindow !== null && usage.lastTokenUsage) {
|
|
100
102
|
lines.push(`<b>Context used:</b> <code>${escapeHTML(formatPercent(usage.contextUsedPercent))}</code> <i>(${escapeHTML(formatCompactTokenCount(usage.lastTokenUsage.totalTokens))} / ${escapeHTML(formatCompactTokenCount(usage.contextWindow))})</i>`);
|
|
101
103
|
}
|
|
102
|
-
if (usage.totalTokenUsage) {
|
|
104
|
+
if (capabilities.usageStats && usage.totalTokenUsage) {
|
|
103
105
|
lines.push(`<b>Tokens:</b> <code>${escapeHTML([
|
|
104
106
|
`in ${formatCompactTokenCount(usage.totalTokenUsage.inputTokens)}`,
|
|
105
107
|
`cached ${formatCompactTokenCount(usage.totalTokenUsage.cachedInputTokens)}`,
|
|
@@ -107,13 +109,18 @@ function renderCodexUsageHTML(info) {
|
|
|
107
109
|
`reasoning out ${formatCompactTokenCount(usage.totalTokenUsage.reasoningOutputTokens)}`,
|
|
108
110
|
].join(" · "))}</code>`);
|
|
109
111
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
if (capabilities.subscriptionLimits) {
|
|
113
|
+
const limits = formatLimitsLeft(usage);
|
|
114
|
+
if (limits) {
|
|
115
|
+
lines.push(`<b>Limits left:</b> <code>${escapeHTML(limits)}</code>`);
|
|
116
|
+
}
|
|
113
117
|
}
|
|
114
118
|
return lines;
|
|
115
119
|
}
|
|
116
|
-
function renderAgentUsagePlain(info) {
|
|
120
|
+
function renderAgentUsagePlain(info, capabilities) {
|
|
121
|
+
if (!capabilities.usageStats) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
117
124
|
const lines = [];
|
|
118
125
|
if (info.contextUsage?.percent !== undefined && info.contextUsage.percent !== null) {
|
|
119
126
|
const contextWindow = info.contextUsage.contextWindow !== null && info.contextUsage.contextWindow !== undefined
|
|
@@ -131,7 +138,10 @@ function renderAgentUsagePlain(info) {
|
|
|
131
138
|
}
|
|
132
139
|
return lines;
|
|
133
140
|
}
|
|
134
|
-
function renderAgentUsageHTML(info) {
|
|
141
|
+
function renderAgentUsageHTML(info, capabilities) {
|
|
142
|
+
if (!capabilities.usageStats) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
135
145
|
const lines = [];
|
|
136
146
|
if (info.contextUsage?.percent !== undefined && info.contextUsage.percent !== null) {
|
|
137
147
|
const contextWindow = info.contextUsage.contextWindow !== null && info.contextUsage.contextWindow !== undefined
|
package/dist/session-registry.js
CHANGED
|
@@ -8,12 +8,12 @@ export class SessionRegistry {
|
|
|
8
8
|
metadata = new Map();
|
|
9
9
|
store;
|
|
10
10
|
onRemoveCallback;
|
|
11
|
-
constructor(config) {
|
|
11
|
+
constructor(config, options = {}) {
|
|
12
12
|
this.config = config;
|
|
13
13
|
this.store = createDocumentStore({
|
|
14
14
|
workspace: config.workspace,
|
|
15
|
-
fileName: "contexts.json",
|
|
16
|
-
sqliteKey: "contexts",
|
|
15
|
+
fileName: options.fileName ?? "contexts.json",
|
|
16
|
+
sqliteKey: options.sqliteKey ?? "contexts",
|
|
17
17
|
backend: config.stateBackend,
|
|
18
18
|
});
|
|
19
19
|
this.loadPersistedMetadata();
|
|
@@ -64,7 +64,7 @@ export class SessionRegistry {
|
|
|
64
64
|
agentId,
|
|
65
65
|
threadId: null,
|
|
66
66
|
workspace: previous?.workspace ?? this.config.workspace,
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
117
|
+
return meta.pinnedThreadIdsByAgent[agentId] ?? [];
|
|
108
118
|
}
|
|
109
119
|
unpinThread(contextKey, threadId) {
|
|
110
120
|
const meta = this.metadata.get(contextKey) ?? this.createEmptyMetadata(contextKey);
|
|
111
|
-
|
|
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.
|
|
131
|
+
return meta.pinnedThreadIdsByAgent[agentId] ?? [];
|
|
116
132
|
}
|
|
117
133
|
listPinnedThreadIds(contextKey) {
|
|
118
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
}
|
package/dist/settings-service.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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([
|
|
5
4
|
"TELEGRAM_BOT_TOKEN",
|
|
6
5
|
"CODEX_API_KEY",
|
|
6
|
+
"HERMES_API_KEY",
|
|
7
|
+
"OPENCLAW_GATEWAY_TOKEN",
|
|
8
|
+
"OPENCLAW_GATEWAY_PASSWORD",
|
|
7
9
|
"OPENAI_API_KEY",
|
|
8
10
|
"TELEGRAM_WEBHOOK_SECRET",
|
|
9
11
|
"NORDRELAY_DASHBOARD_TOKEN",
|
|
@@ -17,7 +19,7 @@ export const SETTING_DEFINITIONS = [
|
|
|
17
19
|
setting("TELEGRAM_ALLOWED_CHAT_IDS", "Allowed chat IDs", "Telegram", "list", "Optional chat allowlist.", true),
|
|
18
20
|
setting("TELEGRAM_ALLOW_ANY_CHAT", "Allow any Telegram chat", "Telegram", "boolean", "Unsafe override; keep off for normal use.", true),
|
|
19
21
|
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),
|
|
22
|
+
setting("TELEGRAM_TRANSPORT", "Telegram transport", "Telegram", "string", "polling or webhook.", true, ["polling", "webhook"]),
|
|
21
23
|
setting("TELEGRAM_WEBHOOK_URL", "Webhook public URL", "Telegram", "string", "Public base URL for webhook mode.", true),
|
|
22
24
|
setting("TELEGRAM_WEBHOOK_HOST", "Webhook bind host", "Telegram", "string", "Local webhook bind host.", true),
|
|
23
25
|
setting("TELEGRAM_WEBHOOK_PORT", "Webhook bind port", "Telegram", "number", "Local webhook bind port.", true),
|
|
@@ -25,7 +27,10 @@ export const SETTING_DEFINITIONS = [
|
|
|
25
27
|
setting("TELEGRAM_WEBHOOK_SECRET", "Webhook secret", "Telegram", "secret", "Optional Telegram webhook secret token.", true),
|
|
26
28
|
setting("NORDRELAY_CODEX_ENABLED", "Enable Codex", "Agents", "boolean", "Allow Codex sessions.", true),
|
|
27
29
|
setting("NORDRELAY_PI_ENABLED", "Enable Pi", "Agents", "boolean", "Allow Pi sessions.", true),
|
|
28
|
-
setting("
|
|
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"]),
|
|
29
34
|
setting("CODEX_API_KEY", "Codex API key", "Codex", "secret", "Optional Codex SDK API key.", true),
|
|
30
35
|
setting("CODEX_CLI_PATH", "Codex CLI path", "Codex", "string", "Optional explicit Codex executable path.", true),
|
|
31
36
|
setting("CODEX_USE_BUNDLED_CLI", "Use bundled Codex CLI", "Codex", "boolean", "Force SDK-bundled CLI instead of host CLI.", true),
|
|
@@ -33,28 +38,53 @@ export const SETTING_DEFINITIONS = [
|
|
|
33
38
|
setting("CODEX_SYNC_INTERVAL_MS", "Codex sync interval", "Codex", "number", "Local state sync interval.", true),
|
|
34
39
|
setting("CODEX_EXTERNAL_BUSY_CHECK_MS", "External busy check", "Codex", "number", "External CLI busy polling interval.", true),
|
|
35
40
|
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),
|
|
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"]),
|
|
38
43
|
setting("CODEX_LAUNCH_PROFILES_JSON", "Launch profiles JSON", "Codex", "json", "Additional launch profile definitions.", true),
|
|
39
44
|
setting("CODEX_DEFAULT_LAUNCH_PROFILE", "Default launch profile", "Codex", "string", "Launch profile ID used by default.", true),
|
|
40
45
|
setting("ENABLE_UNSAFE_LAUNCH_PROFILES", "Enable unsafe profiles", "Codex", "boolean", "Expose danger-full-access profiles.", true),
|
|
41
46
|
setting("PI_CLI_PATH", "Pi CLI path", "Pi", "string", "Optional Pi executable path.", true),
|
|
42
47
|
setting("PI_SESSION_DIR", "Pi session dir", "Pi", "string", "Optional Pi session directory.", true),
|
|
43
48
|
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("
|
|
46
|
-
setting("
|
|
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"]),
|
|
47
77
|
setting("SHOW_TURN_TOKEN_USAGE", "Show turn token usage", "Operations", "boolean", "Append per-turn token usage.", false),
|
|
48
78
|
setting("ENABLE_TELEGRAM_LOGIN", "Enable Telegram login", "Operations", "boolean", "Allow /login and /logout.", true),
|
|
49
79
|
setting("ENABLE_TELEGRAM_REACTIONS", "Enable Telegram reactions", "Operations", "boolean", "Send Telegram reactions.", true),
|
|
50
80
|
setting("TELEGRAM_RATE_LIMIT_MIN_INTERVAL_MS", "Telegram send interval", "Operations", "number", "Minimum send interval.", true),
|
|
51
81
|
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),
|
|
82
|
+
setting("TELEGRAM_CLI_MIRROR_MODE", "CLI mirror mode", "Operations", "string", "off, status, final, or full.", false, ["off", "status", "final", "full"]),
|
|
53
83
|
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),
|
|
84
|
+
setting("TELEGRAM_NOTIFY_MODE", "Notify mode", "Operations", "string", "off, minimal, or all.", false, ["off", "minimal", "all"]),
|
|
55
85
|
setting("TELEGRAM_QUIET_HOURS", "Quiet hours", "Operations", "string", "HH-HH or blank.", false),
|
|
56
86
|
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),
|
|
87
|
+
setting("NORDRELAY_UPDATE_METHOD", "Update method", "Operations", "string", "auto, npm, or git.", true, ["auto", "npm", "git"]),
|
|
58
88
|
setting("MAX_FILE_SIZE", "Max file size", "Artifacts", "number", "Max inbound/outbound file size.", true),
|
|
59
89
|
setting("ARTIFACT_RETENTION_DAYS", "Artifact retention days", "Artifacts", "number", "Days before pruning.", true),
|
|
60
90
|
setting("ARTIFACT_MAX_TURNS", "Max artifact turns", "Artifacts", "number", "Maximum artifact turns retained.", true),
|
|
@@ -64,12 +94,12 @@ export const SETTING_DEFINITIONS = [
|
|
|
64
94
|
setting("TELEGRAM_AUTO_SEND_ARTIFACTS", "Auto-send artifacts", "Artifacts", "boolean", "Automatically send artifact files.", false),
|
|
65
95
|
setting("WORKSPACE_ALLOWED_ROOTS", "Workspace allowed roots", "Workspace", "list", "Restrict selectable workspaces.", true),
|
|
66
96
|
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),
|
|
97
|
+
setting("NORDRELAY_STATE_BACKEND", "State backend", "Workspace", "string", "json or sqlite.", true, ["json", "sqlite"]),
|
|
68
98
|
setting("NORDRELAY_AUDIT_MAX_EVENTS", "Audit max events", "Workspace", "number", "Retained audit events.", true),
|
|
69
99
|
setting("NORDRELAY_SESSION_LOCK_TTL_MS", "Session lock TTL", "Workspace", "number", "Write-lock TTL.", true),
|
|
70
100
|
setting("NORDRELAY_VERSION_CACHE_TTL_MS", "Version cache TTL", "Workspace", "number", "NPM version cache TTL.", true),
|
|
71
101
|
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),
|
|
102
|
+
setting("VOICE_PREFERRED_BACKEND", "Voice backend", "Voice", "string", "auto, parakeet, faster-whisper, or openai.", false, ["auto", "parakeet", "faster-whisper", "openai"]),
|
|
73
103
|
setting("VOICE_DEFAULT_LANGUAGE", "Voice language", "Voice", "string", "Default transcription language.", false),
|
|
74
104
|
setting("VOICE_TRANSCRIBE_ONLY", "Voice transcribe only", "Voice", "boolean", "Do not send voice transcripts as prompts.", false),
|
|
75
105
|
setting("FASTER_WHISPER_PYTHON", "faster-whisper Python", "Voice", "string", "Python executable.", true),
|
|
@@ -89,15 +119,15 @@ export class SettingsService {
|
|
|
89
119
|
constructor(envPath) {
|
|
90
120
|
this.envPath = envPath;
|
|
91
121
|
}
|
|
92
|
-
async snapshot(env = process.env) {
|
|
122
|
+
async snapshot(env = process.env, activeValues = {}) {
|
|
93
123
|
const parsed = await readEnvFile(this.envPath);
|
|
94
124
|
const settings = SETTING_DEFINITIONS.map((definition) => {
|
|
95
125
|
const configuredValue = parsed[definition.key];
|
|
96
|
-
const effectiveValue = configuredValue ?? env[definition.key] ?? "";
|
|
126
|
+
const effectiveValue = configuredValue ?? activeValues[definition.key] ?? env[definition.key] ?? "";
|
|
97
127
|
const masked = SECRET_KEYS.has(definition.key) && Boolean(effectiveValue);
|
|
98
128
|
return {
|
|
99
129
|
...definition,
|
|
100
|
-
value:
|
|
130
|
+
value: configuredValue === undefined ? "" : SECRET_KEYS.has(definition.key) && configuredValue ? maskSecret(configuredValue) : configuredValue,
|
|
101
131
|
effectiveValue: masked ? maskSecret(effectiveValue) : effectiveValue,
|
|
102
132
|
configured: configuredValue !== undefined,
|
|
103
133
|
masked,
|
|
@@ -108,6 +138,7 @@ export class SettingsService {
|
|
|
108
138
|
async update(patch) {
|
|
109
139
|
const current = await readEnvFile(this.envPath);
|
|
110
140
|
const changedKeys = [];
|
|
141
|
+
const errors = [];
|
|
111
142
|
const definitions = new Map(SETTING_DEFINITIONS.map((definition) => [definition.key, definition]));
|
|
112
143
|
for (const [key, rawValue] of Object.entries(patch)) {
|
|
113
144
|
const definition = definitions.get(key);
|
|
@@ -125,18 +156,24 @@ export class SettingsService {
|
|
|
125
156
|
}
|
|
126
157
|
continue;
|
|
127
158
|
}
|
|
159
|
+
const validationError = validateSettingValue(definition, value);
|
|
160
|
+
if (validationError) {
|
|
161
|
+
errors.push({ key, message: validationError });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
128
164
|
if (current[key] !== value) {
|
|
129
165
|
current[key] = value;
|
|
130
166
|
changedKeys.push(key);
|
|
131
167
|
}
|
|
132
168
|
}
|
|
133
|
-
if (changedKeys.length > 0) {
|
|
169
|
+
if (changedKeys.length > 0 && errors.length === 0) {
|
|
134
170
|
await writeEnvFile(this.envPath, current);
|
|
135
171
|
}
|
|
136
172
|
return {
|
|
137
173
|
envPath: this.envPath,
|
|
138
|
-
changedKeys,
|
|
139
|
-
restartRequired: changedKeys.some((key) => definitions.get(key)?.restartRequired),
|
|
174
|
+
changedKeys: errors.length === 0 ? changedKeys : [],
|
|
175
|
+
restartRequired: errors.length === 0 && changedKeys.some((key) => definitions.get(key)?.restartRequired),
|
|
176
|
+
errors,
|
|
140
177
|
};
|
|
141
178
|
}
|
|
142
179
|
}
|
|
@@ -144,11 +181,8 @@ export function resolveDashboardEnvPath(home, cwd = process.cwd()) {
|
|
|
144
181
|
if (process.env.NORDRELAY_ENV_FILE) {
|
|
145
182
|
return path.resolve(process.env.NORDRELAY_ENV_FILE);
|
|
146
183
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return homeEnv;
|
|
150
|
-
}
|
|
151
|
-
return path.join(cwd, ".env");
|
|
184
|
+
void cwd;
|
|
185
|
+
return path.join(home, "nordrelay.env");
|
|
152
186
|
}
|
|
153
187
|
export function maskSecret(value) {
|
|
154
188
|
if (!value) {
|
|
@@ -159,8 +193,28 @@ export function maskSecret(value) {
|
|
|
159
193
|
}
|
|
160
194
|
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
161
195
|
}
|
|
162
|
-
function setting(key, label, group, kind, description, restartRequired) {
|
|
163
|
-
return { key, label, group, kind, description, restartRequired };
|
|
196
|
+
function setting(key, label, group, kind, description, restartRequired, options) {
|
|
197
|
+
return { key, label, group, kind, description, restartRequired, options };
|
|
198
|
+
}
|
|
199
|
+
function validateSettingValue(definition, value) {
|
|
200
|
+
if (definition.kind === "number" && !Number.isFinite(Number(value))) {
|
|
201
|
+
return "Must be a number.";
|
|
202
|
+
}
|
|
203
|
+
if (definition.kind === "boolean" && !["true", "false", "1", "0", "yes", "no", "on", "off"].includes(value.toLowerCase())) {
|
|
204
|
+
return "Must be true or false.";
|
|
205
|
+
}
|
|
206
|
+
if (definition.kind === "json") {
|
|
207
|
+
try {
|
|
208
|
+
JSON.parse(value);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (definition.options && !definition.options.includes(value)) {
|
|
215
|
+
return `Must be one of: ${definition.options.join(", ")}.`;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
164
218
|
}
|
|
165
219
|
async function readEnvFile(filePath) {
|
|
166
220
|
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,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const DASHBOARD_PAGES = [
|
|
2
|
+
{ id: "overview", label: "Overview" },
|
|
3
|
+
{ id: "chat", label: "Chat" },
|
|
4
|
+
{ id: "sessions", label: "Sessions" },
|
|
5
|
+
{ id: "queue", label: "Queue" },
|
|
6
|
+
{ id: "tasks", label: "Tasks" },
|
|
7
|
+
{ id: "activity", label: "Activity" },
|
|
8
|
+
{ id: "artifacts", label: "Artifacts" },
|
|
9
|
+
{ id: "adapters", label: "Adapters" },
|
|
10
|
+
{ id: "access", label: "Access" },
|
|
11
|
+
{ id: "version", label: "Version" },
|
|
12
|
+
{ id: "settings", label: "Settings" },
|
|
13
|
+
{ id: "logs", label: "Logs" },
|
|
14
|
+
{ id: "diagnostics", label: "Diagnostics" },
|
|
15
|
+
];
|
|
16
|
+
export function renderDashboardNav(activePage = "overview") {
|
|
17
|
+
return DASHBOARD_PAGES.map((page) => `<button data-page="${page.id}"${page.id === activePage ? ' class="active"' : ""}>${page.label}</button>`).join("\n ");
|
|
18
|
+
}
|