@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.
- package/.env.example +45 -2
- package/README.md +221 -35
- package/dist/access-control.js +3 -0
- package/dist/agent-activity.js +300 -0
- package/dist/agent-adapter.js +17 -30
- package/dist/agent-factory.js +27 -0
- package/dist/agent-feature-matrix.js +42 -0
- package/dist/agent-updates.js +294 -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 +483 -354
- package/dist/channel-actions.js +372 -0
- 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 +12 -1
- package/dist/config.js +113 -9
- 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 +115 -9
- 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 +798 -72
- package/dist/session-format.js +98 -19
- package/dist/session-registry.js +40 -15
- package/dist/settings-service.js +35 -4
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-client.js +275 -0
- package/dist/web-dashboard-style.js +9 -0
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +296 -196
- 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 +187 -12
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -26
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function getDefaultOpenClawHome() {
|
|
5
|
+
return path.join(os.homedir(), ".openclaw");
|
|
6
|
+
}
|
|
7
|
+
export function resolveOpenClawStateDir(options = {}) {
|
|
8
|
+
return options.stateDir
|
|
9
|
+
?? process.env.OPENCLAW_STATE_DIR
|
|
10
|
+
?? options.openClawHome
|
|
11
|
+
?? process.env.OPENCLAW_HOME
|
|
12
|
+
?? getDefaultOpenClawHome();
|
|
13
|
+
}
|
|
14
|
+
export function listOpenClawSessions(limit = 20, options = {}) {
|
|
15
|
+
const payload = options.sessionsJson ?? readOpenClawSessionsJson(limit, options);
|
|
16
|
+
return parseOpenClawSessionsPayload(payload, options)
|
|
17
|
+
.sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())
|
|
18
|
+
.slice(0, Math.max(1, limit));
|
|
19
|
+
}
|
|
20
|
+
export function getOpenClawSession(id, options = {}) {
|
|
21
|
+
const normalized = id.trim();
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const sessions = listOpenClawSessions(500, options);
|
|
26
|
+
return sessions.find((record) => record.id === normalized || record.sessionKey === normalized)
|
|
27
|
+
?? sessions.find((record) => record.id.startsWith(normalized) || record.sessionKey.startsWith(normalized))
|
|
28
|
+
?? null;
|
|
29
|
+
}
|
|
30
|
+
export function listOpenClawWorkspaces(options = {}) {
|
|
31
|
+
const workspaces = new Set();
|
|
32
|
+
for (const record of listOpenClawSessions(500, options)) {
|
|
33
|
+
if (record.cwd) {
|
|
34
|
+
workspaces.add(record.cwd);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (options.workspace) {
|
|
38
|
+
workspaces.add(options.workspace);
|
|
39
|
+
}
|
|
40
|
+
return [...workspaces].sort((left, right) => left.localeCompare(right));
|
|
41
|
+
}
|
|
42
|
+
export function getOpenClawSessionActivity(id, options = {}) {
|
|
43
|
+
return getOpenClawSessionSnapshot(id, { ...options, maxEvents: 0 })?.activity ?? null;
|
|
44
|
+
}
|
|
45
|
+
export function getOpenClawSessionActivityLog(id, limit = 50, options = {}) {
|
|
46
|
+
return getOpenClawSessionSnapshot(id, { ...options, maxEvents: Math.max(1, limit) })?.events ?? [];
|
|
47
|
+
}
|
|
48
|
+
export function getOpenClawSessionSnapshot(id, options = {}) {
|
|
49
|
+
const record = getOpenClawSession(id, options);
|
|
50
|
+
if (!record) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const events = parseOpenClawActivityEvents(record, options.afterLine ?? 0);
|
|
54
|
+
const latestUser = [...events].reverse().find((event) => event.kind === "user");
|
|
55
|
+
const latestAgent = [...events].reverse().find((event) => event.kind === "agent");
|
|
56
|
+
const latestTerminal = [...events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
|
|
57
|
+
const latestTool = [...events].reverse().find((event) => event.kind === "tool" && event.toolName);
|
|
58
|
+
const latestTimestamp = events.at(-1)?.timestamp ?? record.updatedAt;
|
|
59
|
+
const staleAfterMs = options.staleAfterMs ?? 5 * 60 * 1000;
|
|
60
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
61
|
+
const stale = Boolean(record.active && latestTimestamp && nowMs - latestTimestamp.getTime() > staleAfterMs);
|
|
62
|
+
const maxEvents = options.maxEvents ?? 50;
|
|
63
|
+
const lineCount = Math.max(events.length, record.active ? 1 : 0);
|
|
64
|
+
const returnedEvents = maxEvents <= 0 ? [] : events.slice(-maxEvents);
|
|
65
|
+
return {
|
|
66
|
+
agentId: "openclaw",
|
|
67
|
+
agentLabel: "OpenClaw",
|
|
68
|
+
threadId: record.id,
|
|
69
|
+
sourcePath: record.sessionPath ?? sourcePath(options),
|
|
70
|
+
sourceLabel: "OpenClaw sessions",
|
|
71
|
+
lineCount,
|
|
72
|
+
activity: {
|
|
73
|
+
agentId: "openclaw",
|
|
74
|
+
agentLabel: "OpenClaw",
|
|
75
|
+
threadId: record.id,
|
|
76
|
+
sourcePath: record.sessionPath ?? sourcePath(options),
|
|
77
|
+
sourceLabel: "OpenClaw sessions",
|
|
78
|
+
active: record.active && !stale,
|
|
79
|
+
stale,
|
|
80
|
+
turnId: latestUser?.turnId ?? latestTerminal?.turnId ?? record.id,
|
|
81
|
+
startedAt: latestUser?.timestamp ?? (record.active ? record.updatedAt : null),
|
|
82
|
+
updatedAt: latestTimestamp,
|
|
83
|
+
},
|
|
84
|
+
events: returnedEvents,
|
|
85
|
+
latestAgentMessage: latestAgent?.text ?? null,
|
|
86
|
+
latestUserMessage: latestUser?.text ?? record.firstUserMessage,
|
|
87
|
+
latestToolName: latestTool?.toolName ?? null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function getOpenClawSessionDiagnostics(id, options = {}) {
|
|
91
|
+
const source = sourcePath(options);
|
|
92
|
+
if (!id) {
|
|
93
|
+
return {
|
|
94
|
+
sourcePath: source,
|
|
95
|
+
status: "unavailable",
|
|
96
|
+
reason: "no active OpenClaw session",
|
|
97
|
+
lineCount: 0,
|
|
98
|
+
updatedAt: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const snapshot = getOpenClawSessionSnapshot(id, { ...options, maxEvents: 0 });
|
|
102
|
+
if (!snapshot) {
|
|
103
|
+
return {
|
|
104
|
+
sourcePath: source,
|
|
105
|
+
status: "unavailable",
|
|
106
|
+
reason: "OpenClaw session not found",
|
|
107
|
+
lineCount: 0,
|
|
108
|
+
updatedAt: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const status = snapshot.activity.active ? "active" : snapshot.activity.stale ? "stale" : "idle";
|
|
112
|
+
const reason = snapshot.activity.active
|
|
113
|
+
? "OpenClaw session reports an active run"
|
|
114
|
+
: snapshot.activity.stale
|
|
115
|
+
? "OpenClaw active run exceeded stale timeout"
|
|
116
|
+
: "OpenClaw session is idle";
|
|
117
|
+
return {
|
|
118
|
+
sourcePath: snapshot.sourcePath,
|
|
119
|
+
status,
|
|
120
|
+
reason,
|
|
121
|
+
lineCount: snapshot.lineCount,
|
|
122
|
+
updatedAt: snapshot.activity.updatedAt,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function parseOpenClawSessionsPayload(payload, options = {}) {
|
|
126
|
+
const root = objectValue(payload);
|
|
127
|
+
const storePaths = new Map();
|
|
128
|
+
for (const store of arrayValue(root?.stores)) {
|
|
129
|
+
const storeObject = objectValue(store);
|
|
130
|
+
const agentId = stringValue(storeObject?.agentId) ?? stringValue(storeObject?.agent_id);
|
|
131
|
+
const storePath = stringValue(storeObject?.path) ?? stringValue(storeObject?.storePath);
|
|
132
|
+
if (agentId && storePath) {
|
|
133
|
+
storePaths.set(agentId, storePath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const sessions = arrayValue(root?.sessions ?? payload);
|
|
137
|
+
return sessions
|
|
138
|
+
.map((entry) => mapOpenClawSession(entry, options, storePaths))
|
|
139
|
+
.filter((record) => Boolean(record));
|
|
140
|
+
}
|
|
141
|
+
function readOpenClawSessionsJson(limit, options) {
|
|
142
|
+
const cliPath = options.cliPath ?? process.env.OPENCLAW_CLI_PATH ?? "openclaw";
|
|
143
|
+
const args = ["sessions", "--all-agents", "--limit", String(Math.max(1, limit)), "--json"];
|
|
144
|
+
const result = spawnSync(cliPath, args, {
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
timeout: 5000,
|
|
147
|
+
windowsHide: true,
|
|
148
|
+
env: process.env,
|
|
149
|
+
});
|
|
150
|
+
if (result.error || result.status !== 0) {
|
|
151
|
+
return { sessions: [] };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(result.stdout.trim() || "{}");
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { sessions: [] };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function mapOpenClawSession(raw, options, storePaths) {
|
|
161
|
+
const object = objectValue(raw);
|
|
162
|
+
if (!object) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const key = stringValue(object.key)
|
|
166
|
+
?? stringValue(object.sessionKey)
|
|
167
|
+
?? stringValue(object.session_key)
|
|
168
|
+
?? stringValue(object.id)
|
|
169
|
+
?? stringValue(object.sessionId)
|
|
170
|
+
?? stringValue(object.session_id);
|
|
171
|
+
if (!key) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const id = stringValue(object.id)
|
|
175
|
+
?? stringValue(object.sessionId)
|
|
176
|
+
?? stringValue(object.session_id)
|
|
177
|
+
?? key;
|
|
178
|
+
const openClawAgentId = stringValue(object.agentId)
|
|
179
|
+
?? stringValue(object.agent_id)
|
|
180
|
+
?? parseAgentFromSessionKey(key)
|
|
181
|
+
?? options.openClawAgentId
|
|
182
|
+
?? null;
|
|
183
|
+
const sessionPath = stringValue(object.path)
|
|
184
|
+
?? stringValue(object.storePath)
|
|
185
|
+
?? stringValue(object.store_path)
|
|
186
|
+
?? (openClawAgentId ? storePaths.get(openClawAgentId) : undefined);
|
|
187
|
+
const workspace = stringValue(object.workspace)
|
|
188
|
+
?? stringValue(object.cwd)
|
|
189
|
+
?? workspaceFromSource(stringValue(object.source))
|
|
190
|
+
?? options.workspace
|
|
191
|
+
?? process.cwd();
|
|
192
|
+
const createdAt = dateValue(object.createdAt)
|
|
193
|
+
?? dateValue(object.created_at)
|
|
194
|
+
?? dateValue(object.startedAt)
|
|
195
|
+
?? dateValue(object.started_at)
|
|
196
|
+
?? new Date();
|
|
197
|
+
const updatedAt = dateValue(object.updatedAt)
|
|
198
|
+
?? dateValue(object.updated_at)
|
|
199
|
+
?? dateValue(object.lastActive)
|
|
200
|
+
?? dateValue(object.last_active)
|
|
201
|
+
?? dateValue(object.endedAt)
|
|
202
|
+
?? dateValue(object.ended_at)
|
|
203
|
+
?? createdAt;
|
|
204
|
+
const firstUserMessage = stringValue(object.firstUserMessage)
|
|
205
|
+
?? stringValue(object.first_user_message)
|
|
206
|
+
?? firstMessageText(object);
|
|
207
|
+
const status = stringValue(object.status);
|
|
208
|
+
const active = booleanValue(object.active)
|
|
209
|
+
?? booleanValue(object.running)
|
|
210
|
+
?? booleanValue(object.inProgress)
|
|
211
|
+
?? booleanValue(object.in_progress)
|
|
212
|
+
?? booleanValue(object.isRunning)
|
|
213
|
+
?? statusIsActive(status);
|
|
214
|
+
return {
|
|
215
|
+
id,
|
|
216
|
+
sessionKey: key,
|
|
217
|
+
title: stringValue(object.title),
|
|
218
|
+
cwd: workspace,
|
|
219
|
+
model: stringValue(object.model),
|
|
220
|
+
reasoningEffort: stringValue(object.thinking)
|
|
221
|
+
?? stringValue(object.thinkingLevel)
|
|
222
|
+
?? stringValue(object.thinking_level)
|
|
223
|
+
?? stringValue(object.reasoningEffort)
|
|
224
|
+
?? stringValue(object.reasoning_effort),
|
|
225
|
+
createdAt,
|
|
226
|
+
updatedAt,
|
|
227
|
+
firstUserMessage,
|
|
228
|
+
agentId: "openclaw",
|
|
229
|
+
sessionPath,
|
|
230
|
+
openClawAgentId,
|
|
231
|
+
status,
|
|
232
|
+
active,
|
|
233
|
+
usage: usageFromSession(object),
|
|
234
|
+
raw,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function parseOpenClawActivityEvents(record, afterLine) {
|
|
238
|
+
const raw = objectValue(record.raw);
|
|
239
|
+
const sourceEvents = arrayValue(raw?.events ?? raw?.messages ?? raw?.turns ?? raw?.transcript);
|
|
240
|
+
const events = [];
|
|
241
|
+
let lineNumber = 0;
|
|
242
|
+
if (record.active) {
|
|
243
|
+
lineNumber += 1;
|
|
244
|
+
events.push({
|
|
245
|
+
lineNumber,
|
|
246
|
+
kind: "task",
|
|
247
|
+
timestamp: record.updatedAt,
|
|
248
|
+
type: "run",
|
|
249
|
+
turnId: record.id,
|
|
250
|
+
status: "started",
|
|
251
|
+
text: record.status,
|
|
252
|
+
toolName: null,
|
|
253
|
+
phase: null,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
for (const event of sourceEvents) {
|
|
257
|
+
const object = objectValue(event);
|
|
258
|
+
if (!object)
|
|
259
|
+
continue;
|
|
260
|
+
lineNumber += 1;
|
|
261
|
+
const parsed = mapActivityObject(object, lineNumber, record.id);
|
|
262
|
+
if (parsed)
|
|
263
|
+
events.push(parsed);
|
|
264
|
+
}
|
|
265
|
+
if (!record.active && events.some((event) => event.kind === "user") && !events.some((event) => event.kind === "task" && event.status !== "started")) {
|
|
266
|
+
lineNumber += 1;
|
|
267
|
+
events.push({
|
|
268
|
+
lineNumber,
|
|
269
|
+
kind: "task",
|
|
270
|
+
timestamp: record.updatedAt,
|
|
271
|
+
type: "run",
|
|
272
|
+
turnId: record.id,
|
|
273
|
+
status: statusIsFailure(record.status) ? "failed" : "completed",
|
|
274
|
+
text: record.status,
|
|
275
|
+
toolName: null,
|
|
276
|
+
phase: null,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (events.length === 0 && record.firstUserMessage) {
|
|
280
|
+
events.push({
|
|
281
|
+
lineNumber: 1,
|
|
282
|
+
kind: "user",
|
|
283
|
+
timestamp: record.createdAt,
|
|
284
|
+
type: "message",
|
|
285
|
+
turnId: record.id,
|
|
286
|
+
status: null,
|
|
287
|
+
text: record.firstUserMessage,
|
|
288
|
+
toolName: null,
|
|
289
|
+
phase: null,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return events.filter((event) => event.lineNumber > afterLine);
|
|
293
|
+
}
|
|
294
|
+
function mapActivityObject(object, lineNumber, fallbackTurnId) {
|
|
295
|
+
const role = stringValue(object.role)?.toLowerCase();
|
|
296
|
+
const type = stringValue(object.type) ?? stringValue(object.event) ?? role ?? "message";
|
|
297
|
+
const status = stringValue(object.status);
|
|
298
|
+
const timestamp = dateValue(object.timestamp) ?? dateValue(object.createdAt) ?? dateValue(object.created_at);
|
|
299
|
+
const text = stringValue(object.text)
|
|
300
|
+
?? stringValue(object.content)
|
|
301
|
+
?? stringValue(object.message)
|
|
302
|
+
?? stringValue(object.summary);
|
|
303
|
+
const toolName = stringValue(object.toolName)
|
|
304
|
+
?? stringValue(object.tool_name)
|
|
305
|
+
?? stringValue(object.name)
|
|
306
|
+
?? stringValue(object.tool);
|
|
307
|
+
const turnId = stringValue(object.turnId) ?? stringValue(object.turn_id) ?? stringValue(object.runId) ?? fallbackTurnId;
|
|
308
|
+
if (role === "user") {
|
|
309
|
+
return { lineNumber, kind: "user", timestamp, type, turnId, status, text, toolName: null, phase: null };
|
|
310
|
+
}
|
|
311
|
+
if (role === "assistant" || role === "agent") {
|
|
312
|
+
return { lineNumber, kind: "agent", timestamp, type, turnId, status, text, toolName: null, phase: stringValue(object.phase) };
|
|
313
|
+
}
|
|
314
|
+
if (role === "tool" || toolName || type.includes("tool")) {
|
|
315
|
+
return {
|
|
316
|
+
lineNumber,
|
|
317
|
+
kind: "tool",
|
|
318
|
+
timestamp,
|
|
319
|
+
type,
|
|
320
|
+
turnId,
|
|
321
|
+
status: status === "completed" ? "finished" : status ?? (type.includes("start") ? "started" : "finished"),
|
|
322
|
+
text,
|
|
323
|
+
toolName: toolName ?? "tool",
|
|
324
|
+
phase: null,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (type.includes("run") || type.includes("task")) {
|
|
328
|
+
return { lineNumber, kind: "task", timestamp, type, turnId, status, text, toolName: null, phase: null };
|
|
329
|
+
}
|
|
330
|
+
return text ? { lineNumber, kind: "agent", timestamp, type, turnId, status, text, toolName: null, phase: null } : null;
|
|
331
|
+
}
|
|
332
|
+
function sourcePath(options) {
|
|
333
|
+
return path.join(resolveOpenClawStateDir(options), "sessions");
|
|
334
|
+
}
|
|
335
|
+
function parseAgentFromSessionKey(key) {
|
|
336
|
+
const match = key.match(/^agent:([^:]+):/);
|
|
337
|
+
return match?.[1] ?? null;
|
|
338
|
+
}
|
|
339
|
+
function workspaceFromSource(source) {
|
|
340
|
+
if (!source)
|
|
341
|
+
return null;
|
|
342
|
+
if (path.isAbsolute(source))
|
|
343
|
+
return source;
|
|
344
|
+
const match = source.match(/(?:cwd|workspace)=([^,;]+)/i);
|
|
345
|
+
return match && path.isAbsolute(match[1]) ? match[1] : null;
|
|
346
|
+
}
|
|
347
|
+
function firstMessageText(object) {
|
|
348
|
+
for (const entry of arrayValue(object.messages ?? object.transcript ?? object.events)) {
|
|
349
|
+
const row = objectValue(entry);
|
|
350
|
+
if (stringValue(row?.role)?.toLowerCase() === "user") {
|
|
351
|
+
return stringValue(row?.text) ?? stringValue(row?.content) ?? stringValue(row?.message);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
function usageFromSession(object) {
|
|
357
|
+
const usage = objectValue(object.usage) ?? object;
|
|
358
|
+
const input = numberValue(usage.input) ?? numberValue(usage.inputTokens) ?? numberValue(usage.input_tokens) ?? 0;
|
|
359
|
+
const output = numberValue(usage.output) ?? numberValue(usage.outputTokens) ?? numberValue(usage.output_tokens) ?? 0;
|
|
360
|
+
const cacheRead = numberValue(usage.cacheRead) ?? numberValue(usage.cache_read_tokens) ?? 0;
|
|
361
|
+
const cacheWrite = numberValue(usage.cacheWrite) ?? numberValue(usage.cache_write_tokens) ?? 0;
|
|
362
|
+
const total = input + output + cacheRead + cacheWrite;
|
|
363
|
+
if (total <= 0) {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
input,
|
|
368
|
+
output,
|
|
369
|
+
cacheRead,
|
|
370
|
+
cacheWrite,
|
|
371
|
+
total,
|
|
372
|
+
cost: numberValue(usage.cost) ?? numberValue(usage.estimated_cost_usd) ?? undefined,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function statusIsActive(status) {
|
|
376
|
+
return Boolean(status && /^(active|running|processing|queued|pending|accepted)$/i.test(status));
|
|
377
|
+
}
|
|
378
|
+
function statusIsFailure(status) {
|
|
379
|
+
return Boolean(status && /^(failed|error)$/i.test(status));
|
|
380
|
+
}
|
|
381
|
+
function arrayValue(value) {
|
|
382
|
+
return Array.isArray(value) ? value : [];
|
|
383
|
+
}
|
|
384
|
+
function objectValue(value) {
|
|
385
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
386
|
+
}
|
|
387
|
+
function stringValue(value) {
|
|
388
|
+
if (typeof value === "string" && value.trim())
|
|
389
|
+
return value;
|
|
390
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
391
|
+
return String(value);
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
function numberValue(value) {
|
|
395
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
396
|
+
}
|
|
397
|
+
function booleanValue(value) {
|
|
398
|
+
return typeof value === "boolean" ? value : null;
|
|
399
|
+
}
|
|
400
|
+
function dateValue(value) {
|
|
401
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
402
|
+
return new Date(value > 10_000_000_000 ? value : value * 1000);
|
|
403
|
+
}
|
|
404
|
+
if (typeof value === "string" && value.trim()) {
|
|
405
|
+
const parsed = new Date(value);
|
|
406
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
package/dist/operations.js
CHANGED
|
@@ -5,11 +5,19 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { describeCodexCli, resolveCodexCli } from "./codex-cli.js";
|
|
7
7
|
import { findLatestDatabase } from "./codex-state.js";
|
|
8
|
+
import { describeClaudeCodeCli, resolveClaudeCodeCli } from "./claude-code-cli.js";
|
|
9
|
+
import { describeHermesCli, resolveHermesCli } from "./hermes-cli.js";
|
|
10
|
+
import { describeOpenClawCli, resolveOpenClawCli } from "./openclaw-cli.js";
|
|
8
11
|
import { describePiCli, resolvePiCli } from "./pi-cli.js";
|
|
9
12
|
const APP_NAME = "nordrelay";
|
|
10
13
|
const PACKAGE_NAME = "@nordbyte/nordrelay";
|
|
11
14
|
const CODEX_PACKAGE_NAME = "@openai/codex";
|
|
12
|
-
const PI_PACKAGE_NAME = "@
|
|
15
|
+
const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
16
|
+
const LEGACY_PI_PACKAGE_NAME = "@mariozechner/pi-coding-agent";
|
|
17
|
+
const HERMES_PACKAGE_NAME = "hermes-agent";
|
|
18
|
+
const OPENCLAW_PACKAGE_NAME = "openclaw";
|
|
19
|
+
const CLAUDE_CODE_PACKAGE_NAME = "@anthropic-ai/claude-code";
|
|
20
|
+
const CLAUDE_CODE_SDK_PACKAGE_NAME = "@anthropic-ai/claude-agent-sdk";
|
|
13
21
|
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
14
22
|
const SECRET_RE = /(bot|token|api[_-]?key|authorization|bearer|password|secret)(["'=: ]+)([^\s"',]+)/gi;
|
|
15
23
|
const DEFAULT_VERSION_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
@@ -25,6 +33,9 @@ export function getConnectorLogPath() {
|
|
|
25
33
|
export function getUpdateLogPath() {
|
|
26
34
|
return path.join(getConnectorHome(), "update.log");
|
|
27
35
|
}
|
|
36
|
+
export function getAgentUpdateLogPath(home = getConnectorHome()) {
|
|
37
|
+
return path.join(home, "agent-updates.log");
|
|
38
|
+
}
|
|
28
39
|
export async function readConnectorState() {
|
|
29
40
|
try {
|
|
30
41
|
return JSON.parse(await readFile(getConnectorStatePath(), "utf8"));
|
|
@@ -67,6 +78,14 @@ export async function readFormattedLogTail(lines = 80, filePath = getConnectorLo
|
|
|
67
78
|
};
|
|
68
79
|
}
|
|
69
80
|
}
|
|
81
|
+
export function clearLogFile(filePath = getConnectorLogPath()) {
|
|
82
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
83
|
+
writeFileSync(filePath, "", "utf8");
|
|
84
|
+
return {
|
|
85
|
+
filePath,
|
|
86
|
+
clearedAt: new Date(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
70
89
|
export async function getPackageVersion() {
|
|
71
90
|
try {
|
|
72
91
|
const pkg = JSON.parse(await readFile(path.join(getSourceRoot(), "package.json"), "utf8"));
|
|
@@ -80,10 +99,22 @@ export async function getVersionChecks(options = {}) {
|
|
|
80
99
|
const nordrelayVersion = await getPackageVersion();
|
|
81
100
|
const codexCli = resolveCodexCli();
|
|
82
101
|
const piCli = resolvePiCli(process.env, options.piCliPath);
|
|
102
|
+
const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
|
|
103
|
+
const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
|
|
104
|
+
const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
|
|
83
105
|
const codexVersionLabel = codexCli.path
|
|
84
106
|
? detectCliVersion(codexCli.path)
|
|
85
107
|
: readInstalledPackageVersion(CODEX_PACKAGE_NAME) ?? "not installed";
|
|
86
|
-
const piVersionLabel = piCli.path
|
|
108
|
+
const piVersionLabel = piCli.path
|
|
109
|
+
? detectCliVersion(piCli.path)
|
|
110
|
+
: readInstalledPackageVersion(PI_PACKAGE_NAME) ?? readInstalledPackageVersion(LEGACY_PI_PACKAGE_NAME) ?? "not installed";
|
|
111
|
+
const legacyPiPackageVersion = readInstalledPackageVersion(LEGACY_PI_PACKAGE_NAME);
|
|
112
|
+
const hermesVersionLabel = hermesCli.path ? detectCliVersion(hermesCli.path) : "not installed";
|
|
113
|
+
const openClawVersionLabel = openClawCli.path ? detectCliVersion(openClawCli.path) : "not installed";
|
|
114
|
+
const claudeCodeVersionLabel = claudeCodeCli.path
|
|
115
|
+
? detectCliVersion(claudeCodeCli.path)
|
|
116
|
+
: readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled";
|
|
117
|
+
const claudeCodePackageName = claudeCodeCli.path ? CLAUDE_CODE_PACKAGE_NAME : CLAUDE_CODE_SDK_PACKAGE_NAME;
|
|
87
118
|
return {
|
|
88
119
|
nordrelay: buildVersionCheck({
|
|
89
120
|
label: "NordRelay",
|
|
@@ -104,16 +135,36 @@ export async function getVersionChecks(options = {}) {
|
|
|
104
135
|
installedLabel: piVersionLabel,
|
|
105
136
|
installedVersion: extractVersion(piVersionLabel),
|
|
106
137
|
notInstalled: piVersionLabel === "not installed",
|
|
138
|
+
detail: legacyPiPackageVersion ? `Legacy package ${LEGACY_PI_PACKAGE_NAME} is present; current package is ${PI_PACKAGE_NAME}.` : undefined,
|
|
139
|
+
}),
|
|
140
|
+
hermes: buildHermesVersionCheck(hermesVersionLabel),
|
|
141
|
+
openclaw: buildVersionCheck({
|
|
142
|
+
label: "OpenClaw",
|
|
143
|
+
packageName: OPENCLAW_PACKAGE_NAME,
|
|
144
|
+
installedLabel: openClawVersionLabel,
|
|
145
|
+
installedVersion: extractVersion(openClawVersionLabel),
|
|
146
|
+
notInstalled: openClawVersionLabel === "not installed",
|
|
147
|
+
}),
|
|
148
|
+
claudeCode: buildVersionCheck({
|
|
149
|
+
label: "Claude Code",
|
|
150
|
+
packageName: claudeCodePackageName,
|
|
151
|
+
installedLabel: claudeCodeVersionLabel,
|
|
152
|
+
installedVersion: extractVersion(claudeCodeVersionLabel),
|
|
153
|
+
notInstalled: claudeCodeVersionLabel === "not installed",
|
|
107
154
|
}),
|
|
108
155
|
};
|
|
109
156
|
}
|
|
110
|
-
export async function getConnectorHealth() {
|
|
111
|
-
const
|
|
157
|
+
export async function getConnectorHealth(options = {}) {
|
|
158
|
+
const rawState = await readConnectorState();
|
|
112
159
|
const version = await getPackageVersion();
|
|
113
|
-
const pidRunning = isProcessRunning(
|
|
114
|
-
const appPidRunning = isProcessRunning(
|
|
160
|
+
const pidRunning = isProcessRunning(rawState.pid);
|
|
161
|
+
const appPidRunning = isProcessRunning(rawState.appPid);
|
|
162
|
+
const state = normalizeConnectorState(rawState, pidRunning, appPidRunning);
|
|
115
163
|
const codexCli = resolveCodexCli();
|
|
116
|
-
const piCli = resolvePiCli();
|
|
164
|
+
const piCli = resolvePiCli(process.env, options.piCliPath);
|
|
165
|
+
const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
|
|
166
|
+
const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
|
|
167
|
+
const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
|
|
117
168
|
return {
|
|
118
169
|
version,
|
|
119
170
|
state,
|
|
@@ -125,6 +176,17 @@ export async function getConnectorHealth() {
|
|
|
125
176
|
piCli: describePiCli(piCli),
|
|
126
177
|
piCliPath: piCli.path ?? null,
|
|
127
178
|
piCliVersion: detectCliVersion(piCli.path),
|
|
179
|
+
hermesCli: describeHermesCli(hermesCli),
|
|
180
|
+
hermesCliPath: hermesCli.path ?? null,
|
|
181
|
+
hermesCliVersion: detectCliVersion(hermesCli.path),
|
|
182
|
+
openClawCli: describeOpenClawCli(openClawCli),
|
|
183
|
+
openClawCliPath: openClawCli.path ?? null,
|
|
184
|
+
openClawCliVersion: detectCliVersion(openClawCli.path),
|
|
185
|
+
claudeCodeCli: describeClaudeCodeCli(claudeCodeCli),
|
|
186
|
+
claudeCodeCliPath: claudeCodeCli.path ?? null,
|
|
187
|
+
claudeCodeCliVersion: claudeCodeCli.path
|
|
188
|
+
? detectCliVersion(claudeCodeCli.path)
|
|
189
|
+
: readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled",
|
|
128
190
|
stateFile: getConnectorStatePath(),
|
|
129
191
|
logFile: getConnectorLogPath(),
|
|
130
192
|
databasePath: findLatestDatabase(),
|
|
@@ -202,6 +264,13 @@ function isProcessRunning(pid) {
|
|
|
202
264
|
return false;
|
|
203
265
|
}
|
|
204
266
|
}
|
|
267
|
+
function normalizeConnectorState(state, pidRunning, appPidRunning) {
|
|
268
|
+
const stoppedSignal = state.signal === "SIGTERM" || state.signal === "SIGINT";
|
|
269
|
+
if (state.status === "error" && stoppedSignal && !state.error && !pidRunning && !appPidRunning) {
|
|
270
|
+
return { ...state, status: "stopped" };
|
|
271
|
+
}
|
|
272
|
+
return state;
|
|
273
|
+
}
|
|
205
274
|
function redactSecrets(text) {
|
|
206
275
|
return text.replace(SECRET_RE, "$1$2[redacted]");
|
|
207
276
|
}
|
|
@@ -249,6 +318,31 @@ function detectCliVersion(commandPath) {
|
|
|
249
318
|
}
|
|
250
319
|
return output || "unknown";
|
|
251
320
|
}
|
|
321
|
+
function buildHermesVersionCheck(installedLabel) {
|
|
322
|
+
if (installedLabel === "not installed") {
|
|
323
|
+
return {
|
|
324
|
+
label: "Hermes",
|
|
325
|
+
packageName: HERMES_PACKAGE_NAME,
|
|
326
|
+
installedLabel: "not installed",
|
|
327
|
+
installedVersion: null,
|
|
328
|
+
latestVersion: null,
|
|
329
|
+
status: "not-installed",
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const lines = installedLabel.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
333
|
+
const versionLine = lines[0] ?? installedLabel;
|
|
334
|
+
const updateLine = lines.find((line) => /^Update available:/i.test(line));
|
|
335
|
+
const installedVersion = extractVersion(versionLine);
|
|
336
|
+
return {
|
|
337
|
+
label: "Hermes",
|
|
338
|
+
packageName: HERMES_PACKAGE_NAME,
|
|
339
|
+
installedLabel: versionLine,
|
|
340
|
+
installedVersion,
|
|
341
|
+
latestVersion: updateLine?.replace(/^Update available:\s*/i, "") ?? null,
|
|
342
|
+
status: updateLine ? "outdated" : installedVersion ? "current" : "unknown",
|
|
343
|
+
detail: updateLine ?? (installedVersion ? undefined : "Could not parse Hermes version or update status"),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
252
346
|
function buildVersionCheck(options) {
|
|
253
347
|
if (options.notInstalled) {
|
|
254
348
|
return {
|
|
@@ -258,6 +352,18 @@ function buildVersionCheck(options) {
|
|
|
258
352
|
installedVersion: null,
|
|
259
353
|
latestVersion: null,
|
|
260
354
|
status: "not-installed",
|
|
355
|
+
detail: options.detail,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (options.skipLatest) {
|
|
359
|
+
return {
|
|
360
|
+
label: options.label,
|
|
361
|
+
packageName: options.packageName,
|
|
362
|
+
installedLabel: options.installedLabel,
|
|
363
|
+
installedVersion: options.installedVersion,
|
|
364
|
+
latestVersion: null,
|
|
365
|
+
status: options.installedVersion ? "unknown" : "unknown",
|
|
366
|
+
detail: options.detail ?? "Latest-version lookup is not available for this package source",
|
|
261
367
|
};
|
|
262
368
|
}
|
|
263
369
|
const latest = detectLatestNpmVersion(options.packageName);
|
|
@@ -269,7 +375,7 @@ function buildVersionCheck(options) {
|
|
|
269
375
|
installedVersion: options.installedVersion,
|
|
270
376
|
latestVersion: latest.version,
|
|
271
377
|
status: "unknown",
|
|
272
|
-
detail: latest.error ?? "Could not parse installed version",
|
|
378
|
+
detail: [options.detail, latest.error ?? "Could not parse installed version"].filter(Boolean).join(" "),
|
|
273
379
|
};
|
|
274
380
|
}
|
|
275
381
|
return {
|
|
@@ -279,7 +385,7 @@ function buildVersionCheck(options) {
|
|
|
279
385
|
installedVersion: options.installedVersion,
|
|
280
386
|
latestVersion: latest.version,
|
|
281
387
|
status: compareVersions(options.installedVersion, latest.version) < 0 ? "outdated" : "current",
|
|
282
|
-
detail: latest.error,
|
|
388
|
+
detail: [options.detail, latest.error].filter(Boolean).join(" ") || undefined,
|
|
283
389
|
};
|
|
284
390
|
}
|
|
285
391
|
function detectLatestNpmVersion(packageName) {
|
package/dist/pi-auth.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const PROVIDER_ENV_KEYS = {
|
|
2
|
+
anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
|
|
3
|
+
"aws-bedrock": ["AWS_BEARER_TOKEN_BEDROCK", "AWS_PROFILE", "AWS_ACCESS_KEY_ID"],
|
|
4
|
+
azure: ["AZURE_OPENAI_API_KEY"],
|
|
5
|
+
"azure-openai": ["AZURE_OPENAI_API_KEY"],
|
|
6
|
+
cerebras: ["CEREBRAS_API_KEY"],
|
|
7
|
+
cloudflare: ["CLOUDFLARE_API_KEY"],
|
|
8
|
+
deepseek: ["DEEPSEEK_API_KEY"],
|
|
9
|
+
fireworks: ["FIREWORKS_API_KEY"],
|
|
10
|
+
gemini: ["GEMINI_API_KEY"],
|
|
11
|
+
google: ["GEMINI_API_KEY"],
|
|
12
|
+
groq: ["GROQ_API_KEY"],
|
|
13
|
+
kimi: ["KIMI_API_KEY"],
|
|
14
|
+
minimax: ["MINIMAX_API_KEY"],
|
|
15
|
+
mistral: ["MISTRAL_API_KEY"],
|
|
16
|
+
moonshot: ["MOONSHOT_API_KEY"],
|
|
17
|
+
opencode: ["OPENCODE_API_KEY"],
|
|
18
|
+
openrouter: ["OPENROUTER_API_KEY"],
|
|
19
|
+
openai: ["OPENAI_API_KEY"],
|
|
20
|
+
"openai-codex": ["OPENAI_API_KEY"],
|
|
21
|
+
xai: ["XAI_API_KEY"],
|
|
22
|
+
xiaomi: ["XIAOMI_API_KEY", "XIAOMI_TOKEN_PLAN_CN_API_KEY", "XIAOMI_TOKEN_PLAN_AMS_API_KEY", "XIAOMI_TOKEN_PLAN_SGP_API_KEY"],
|
|
23
|
+
zai: ["ZAI_API_KEY"],
|
|
24
|
+
};
|
|
25
|
+
export function checkPiAuthStatus(model, env = process.env) {
|
|
26
|
+
const provider = providerFromModel(model);
|
|
27
|
+
const keys = PROVIDER_ENV_KEYS[provider];
|
|
28
|
+
if (!keys) {
|
|
29
|
+
return {
|
|
30
|
+
authenticated: true,
|
|
31
|
+
method: "cli",
|
|
32
|
+
detail: `Pi provider "${provider}" is not verifiable by NordRelay. Run "pi" on the host if auth fails.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const configured = keys.filter((key) => Boolean(env[key]?.trim()));
|
|
36
|
+
if (configured.length > 0) {
|
|
37
|
+
return {
|
|
38
|
+
authenticated: true,
|
|
39
|
+
method: "api-key",
|
|
40
|
+
detail: `Pi provider "${provider}" has ${configured.join(" or ")} configured.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
authenticated: false,
|
|
45
|
+
method: "none",
|
|
46
|
+
detail: `Pi provider "${provider}" needs one of: ${keys.join(", ")}.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function providerFromModel(model) {
|
|
50
|
+
const trimmed = model?.trim();
|
|
51
|
+
if (!trimmed) {
|
|
52
|
+
return "google";
|
|
53
|
+
}
|
|
54
|
+
const separator = trimmed.indexOf("/");
|
|
55
|
+
if (separator === -1) {
|
|
56
|
+
return "google";
|
|
57
|
+
}
|
|
58
|
+
return trimmed.slice(0, separator).toLowerCase();
|
|
59
|
+
}
|