@nordbyte/nordrelay 0.3.1 → 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 +204 -30
- 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 +328 -159
- 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 +83 -2
- 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 +673 -51
- package/dist/session-format.js +28 -18
- package/dist/session-registry.js +40 -15
- package/dist/settings-service.js +35 -4
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +329 -47
- 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 +131 -3
- 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,18 @@ 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
15
|
const PI_PACKAGE_NAME = "@mariozechner/pi-coding-agent";
|
|
16
|
+
const HERMES_PACKAGE_NAME = "hermes-agent";
|
|
17
|
+
const OPENCLAW_PACKAGE_NAME = "openclaw";
|
|
18
|
+
const CLAUDE_CODE_PACKAGE_NAME = "@anthropic-ai/claude-code";
|
|
19
|
+
const CLAUDE_CODE_SDK_PACKAGE_NAME = "@anthropic-ai/claude-agent-sdk";
|
|
13
20
|
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
14
21
|
const SECRET_RE = /(bot|token|api[_-]?key|authorization|bearer|password|secret)(["'=: ]+)([^\s"',]+)/gi;
|
|
15
22
|
const DEFAULT_VERSION_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
@@ -80,10 +87,19 @@ export async function getVersionChecks(options = {}) {
|
|
|
80
87
|
const nordrelayVersion = await getPackageVersion();
|
|
81
88
|
const codexCli = resolveCodexCli();
|
|
82
89
|
const piCli = resolvePiCli(process.env, options.piCliPath);
|
|
90
|
+
const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
|
|
91
|
+
const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
|
|
92
|
+
const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
|
|
83
93
|
const codexVersionLabel = codexCli.path
|
|
84
94
|
? detectCliVersion(codexCli.path)
|
|
85
95
|
: readInstalledPackageVersion(CODEX_PACKAGE_NAME) ?? "not installed";
|
|
86
96
|
const piVersionLabel = piCli.path ? detectCliVersion(piCli.path) : "not installed";
|
|
97
|
+
const hermesVersionLabel = hermesCli.path ? detectCliVersion(hermesCli.path) : "not installed";
|
|
98
|
+
const openClawVersionLabel = openClawCli.path ? detectCliVersion(openClawCli.path) : "not installed";
|
|
99
|
+
const claudeCodeVersionLabel = claudeCodeCli.path
|
|
100
|
+
? detectCliVersion(claudeCodeCli.path)
|
|
101
|
+
: readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled";
|
|
102
|
+
const claudeCodePackageName = claudeCodeCli.path ? CLAUDE_CODE_PACKAGE_NAME : CLAUDE_CODE_SDK_PACKAGE_NAME;
|
|
87
103
|
return {
|
|
88
104
|
nordrelay: buildVersionCheck({
|
|
89
105
|
label: "NordRelay",
|
|
@@ -105,15 +121,33 @@ export async function getVersionChecks(options = {}) {
|
|
|
105
121
|
installedVersion: extractVersion(piVersionLabel),
|
|
106
122
|
notInstalled: piVersionLabel === "not installed",
|
|
107
123
|
}),
|
|
124
|
+
hermes: buildHermesVersionCheck(hermesVersionLabel),
|
|
125
|
+
openclaw: buildVersionCheck({
|
|
126
|
+
label: "OpenClaw",
|
|
127
|
+
packageName: OPENCLAW_PACKAGE_NAME,
|
|
128
|
+
installedLabel: openClawVersionLabel,
|
|
129
|
+
installedVersion: extractVersion(openClawVersionLabel),
|
|
130
|
+
notInstalled: openClawVersionLabel === "not installed",
|
|
131
|
+
}),
|
|
132
|
+
claudeCode: buildVersionCheck({
|
|
133
|
+
label: "Claude Code",
|
|
134
|
+
packageName: claudeCodePackageName,
|
|
135
|
+
installedLabel: claudeCodeVersionLabel,
|
|
136
|
+
installedVersion: extractVersion(claudeCodeVersionLabel),
|
|
137
|
+
notInstalled: claudeCodeVersionLabel === "not installed",
|
|
138
|
+
}),
|
|
108
139
|
};
|
|
109
140
|
}
|
|
110
|
-
export async function getConnectorHealth() {
|
|
141
|
+
export async function getConnectorHealth(options = {}) {
|
|
111
142
|
const state = await readConnectorState();
|
|
112
143
|
const version = await getPackageVersion();
|
|
113
144
|
const pidRunning = isProcessRunning(state.pid);
|
|
114
145
|
const appPidRunning = isProcessRunning(state.appPid);
|
|
115
146
|
const codexCli = resolveCodexCli();
|
|
116
|
-
const piCli = resolvePiCli();
|
|
147
|
+
const piCli = resolvePiCli(process.env, options.piCliPath);
|
|
148
|
+
const hermesCli = resolveHermesCli(process.env, options.hermesCliPath);
|
|
149
|
+
const openClawCli = resolveOpenClawCli(process.env, options.openClawCliPath);
|
|
150
|
+
const claudeCodeCli = resolveClaudeCodeCli(process.env, options.claudeCodeCliPath);
|
|
117
151
|
return {
|
|
118
152
|
version,
|
|
119
153
|
state,
|
|
@@ -125,6 +159,17 @@ export async function getConnectorHealth() {
|
|
|
125
159
|
piCli: describePiCli(piCli),
|
|
126
160
|
piCliPath: piCli.path ?? null,
|
|
127
161
|
piCliVersion: detectCliVersion(piCli.path),
|
|
162
|
+
hermesCli: describeHermesCli(hermesCli),
|
|
163
|
+
hermesCliPath: hermesCli.path ?? null,
|
|
164
|
+
hermesCliVersion: detectCliVersion(hermesCli.path),
|
|
165
|
+
openClawCli: describeOpenClawCli(openClawCli),
|
|
166
|
+
openClawCliPath: openClawCli.path ?? null,
|
|
167
|
+
openClawCliVersion: detectCliVersion(openClawCli.path),
|
|
168
|
+
claudeCodeCli: describeClaudeCodeCli(claudeCodeCli),
|
|
169
|
+
claudeCodeCliPath: claudeCodeCli.path ?? null,
|
|
170
|
+
claudeCodeCliVersion: claudeCodeCli.path
|
|
171
|
+
? detectCliVersion(claudeCodeCli.path)
|
|
172
|
+
: readInstalledPackageVersion(CLAUDE_CODE_SDK_PACKAGE_NAME) ?? "bundled",
|
|
128
173
|
stateFile: getConnectorStatePath(),
|
|
129
174
|
logFile: getConnectorLogPath(),
|
|
130
175
|
databasePath: findLatestDatabase(),
|
|
@@ -249,6 +294,31 @@ function detectCliVersion(commandPath) {
|
|
|
249
294
|
}
|
|
250
295
|
return output || "unknown";
|
|
251
296
|
}
|
|
297
|
+
function buildHermesVersionCheck(installedLabel) {
|
|
298
|
+
if (installedLabel === "not installed") {
|
|
299
|
+
return {
|
|
300
|
+
label: "Hermes",
|
|
301
|
+
packageName: HERMES_PACKAGE_NAME,
|
|
302
|
+
installedLabel: "not installed",
|
|
303
|
+
installedVersion: null,
|
|
304
|
+
latestVersion: null,
|
|
305
|
+
status: "not-installed",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const lines = installedLabel.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
309
|
+
const versionLine = lines[0] ?? installedLabel;
|
|
310
|
+
const updateLine = lines.find((line) => /^Update available:/i.test(line));
|
|
311
|
+
const installedVersion = extractVersion(versionLine);
|
|
312
|
+
return {
|
|
313
|
+
label: "Hermes",
|
|
314
|
+
packageName: HERMES_PACKAGE_NAME,
|
|
315
|
+
installedLabel: versionLine,
|
|
316
|
+
installedVersion,
|
|
317
|
+
latestVersion: updateLine?.replace(/^Update available:\s*/i, "") ?? null,
|
|
318
|
+
status: updateLine ? "outdated" : installedVersion ? "current" : "unknown",
|
|
319
|
+
detail: updateLine ?? (installedVersion ? undefined : "Could not parse Hermes version or update status"),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
252
322
|
function buildVersionCheck(options) {
|
|
253
323
|
if (options.notInstalled) {
|
|
254
324
|
return {
|
|
@@ -260,6 +330,17 @@ function buildVersionCheck(options) {
|
|
|
260
330
|
status: "not-installed",
|
|
261
331
|
};
|
|
262
332
|
}
|
|
333
|
+
if (options.skipLatest) {
|
|
334
|
+
return {
|
|
335
|
+
label: options.label,
|
|
336
|
+
packageName: options.packageName,
|
|
337
|
+
installedLabel: options.installedLabel,
|
|
338
|
+
installedVersion: options.installedVersion,
|
|
339
|
+
latestVersion: null,
|
|
340
|
+
status: options.installedVersion ? "unknown" : "unknown",
|
|
341
|
+
detail: "Latest-version lookup is not available for this package source",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
263
344
|
const latest = detectLatestNpmVersion(options.packageName);
|
|
264
345
|
if (!options.installedVersion || !latest.version) {
|
|
265
346
|
return {
|
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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createLaunchProfile } from "./codex-launch.js";
|
|
2
|
+
export const PI_LAUNCH_PROFILES = [
|
|
3
|
+
{
|
|
4
|
+
id: "default",
|
|
5
|
+
label: "Default",
|
|
6
|
+
behavior: "all tools / online / extensions",
|
|
7
|
+
unsafe: false,
|
|
8
|
+
cli: {},
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: "readonly",
|
|
12
|
+
label: "Read-only",
|
|
13
|
+
behavior: "read, grep, find, ls / online",
|
|
14
|
+
unsafe: false,
|
|
15
|
+
cli: { tools: "read,grep,find,ls" },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "no-tools",
|
|
19
|
+
label: "No tools",
|
|
20
|
+
behavior: "no tools / online",
|
|
21
|
+
unsafe: false,
|
|
22
|
+
cli: { noTools: true },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "offline",
|
|
26
|
+
label: "Offline",
|
|
27
|
+
behavior: "all tools / offline / extensions",
|
|
28
|
+
unsafe: false,
|
|
29
|
+
cli: { offline: true },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "safe-offline",
|
|
33
|
+
label: "Safe Offline",
|
|
34
|
+
behavior: "read-only tools / offline / no extensions",
|
|
35
|
+
unsafe: false,
|
|
36
|
+
cli: { tools: "read,grep,find,ls", offline: true, noExtensions: true },
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
export function listPiLaunchProfiles() {
|
|
40
|
+
return PI_LAUNCH_PROFILES.map((profile) => ({
|
|
41
|
+
id: profile.id,
|
|
42
|
+
label: profile.label,
|
|
43
|
+
behavior: profile.behavior,
|
|
44
|
+
unsafe: profile.unsafe,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
export function findPiLaunchProfile(profileId) {
|
|
48
|
+
const profile = PI_LAUNCH_PROFILES.find((candidate) => candidate.id === profileId);
|
|
49
|
+
if (profile) {
|
|
50
|
+
return profile;
|
|
51
|
+
}
|
|
52
|
+
return PI_LAUNCH_PROFILES[0];
|
|
53
|
+
}
|
|
54
|
+
export function piProfileAsLaunchProfile(profile) {
|
|
55
|
+
return createLaunchProfile({
|
|
56
|
+
id: profile.id,
|
|
57
|
+
label: profile.label,
|
|
58
|
+
sandboxMode: "workspace-write",
|
|
59
|
+
approvalPolicy: "never",
|
|
60
|
+
});
|
|
61
|
+
}
|
package/dist/pi-rpc.js
CHANGED
|
@@ -68,6 +68,24 @@ export class PiRpcClient {
|
|
|
68
68
|
if (this.options.thinking) {
|
|
69
69
|
args.push("--thinking", this.options.thinking);
|
|
70
70
|
}
|
|
71
|
+
if (this.options.tools) {
|
|
72
|
+
args.push("--tools", this.options.tools);
|
|
73
|
+
}
|
|
74
|
+
if (this.options.noTools) {
|
|
75
|
+
args.push("--no-tools");
|
|
76
|
+
}
|
|
77
|
+
if (this.options.noBuiltinTools) {
|
|
78
|
+
args.push("--no-builtin-tools");
|
|
79
|
+
}
|
|
80
|
+
if (this.options.offline) {
|
|
81
|
+
args.push("--offline");
|
|
82
|
+
}
|
|
83
|
+
if (this.options.noExtensions) {
|
|
84
|
+
args.push("--no-extensions");
|
|
85
|
+
}
|
|
86
|
+
if (this.options.noSkills) {
|
|
87
|
+
args.push("--no-skills");
|
|
88
|
+
}
|
|
71
89
|
const child = spawn(this.options.commandPath, args, {
|
|
72
90
|
cwd: this.options.cwd,
|
|
73
91
|
env: { ...process.env, ...this.options.env },
|