@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,609 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const betterSqlite3Module = await import("better-sqlite3").catch(() => null);
|
|
5
|
+
const BetterSqlite3 = (betterSqlite3Module?.default ??
|
|
6
|
+
betterSqlite3Module);
|
|
7
|
+
export function getDefaultHermesHome() {
|
|
8
|
+
return path.join(os.homedir(), ".hermes");
|
|
9
|
+
}
|
|
10
|
+
export function resolveHermesStateDbPath(options = {}) {
|
|
11
|
+
if (options.stateDbPath?.trim()) {
|
|
12
|
+
return options.stateDbPath;
|
|
13
|
+
}
|
|
14
|
+
const home = options.hermesHome ?? process.env.HERMES_HOME ?? getDefaultHermesHome();
|
|
15
|
+
return path.join(home, "state.db");
|
|
16
|
+
}
|
|
17
|
+
export function listHermesSessions(limit = 20, options = {}) {
|
|
18
|
+
return withHermesDatabase(options, (db, stateDbPath) => {
|
|
19
|
+
const rows = db.prepare(`
|
|
20
|
+
SELECT
|
|
21
|
+
s.id,
|
|
22
|
+
s.source,
|
|
23
|
+
s.title,
|
|
24
|
+
s.model,
|
|
25
|
+
s.model_config,
|
|
26
|
+
s.started_at,
|
|
27
|
+
s.ended_at,
|
|
28
|
+
s.message_count,
|
|
29
|
+
s.input_tokens,
|
|
30
|
+
s.output_tokens,
|
|
31
|
+
s.cache_read_tokens,
|
|
32
|
+
s.cache_write_tokens,
|
|
33
|
+
s.reasoning_tokens,
|
|
34
|
+
s.estimated_cost_usd,
|
|
35
|
+
s.actual_cost_usd,
|
|
36
|
+
COALESCE((
|
|
37
|
+
SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 500)
|
|
38
|
+
FROM messages m
|
|
39
|
+
WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
|
|
40
|
+
ORDER BY m.timestamp ASC, m.id ASC
|
|
41
|
+
LIMIT 1
|
|
42
|
+
), '') AS first_user_message,
|
|
43
|
+
COALESCE((
|
|
44
|
+
SELECT MAX(m2.timestamp)
|
|
45
|
+
FROM messages m2
|
|
46
|
+
WHERE m2.session_id = s.id
|
|
47
|
+
), s.started_at) AS last_active
|
|
48
|
+
FROM sessions s
|
|
49
|
+
ORDER BY last_active DESC, s.started_at DESC
|
|
50
|
+
LIMIT ?
|
|
51
|
+
`).all(limit);
|
|
52
|
+
return rows.map((row) => mapHermesSessionRow(row, stateDbPath, options.workspace));
|
|
53
|
+
}) ?? [];
|
|
54
|
+
}
|
|
55
|
+
export function getHermesSession(id, options = {}) {
|
|
56
|
+
const normalized = id.trim();
|
|
57
|
+
if (!normalized) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return withHermesDatabase(options, (db, stateDbPath) => {
|
|
61
|
+
const row = db.prepare(`
|
|
62
|
+
SELECT
|
|
63
|
+
s.id,
|
|
64
|
+
s.source,
|
|
65
|
+
s.title,
|
|
66
|
+
s.model,
|
|
67
|
+
s.model_config,
|
|
68
|
+
s.started_at,
|
|
69
|
+
s.ended_at,
|
|
70
|
+
s.message_count,
|
|
71
|
+
s.input_tokens,
|
|
72
|
+
s.output_tokens,
|
|
73
|
+
s.cache_read_tokens,
|
|
74
|
+
s.cache_write_tokens,
|
|
75
|
+
s.reasoning_tokens,
|
|
76
|
+
s.estimated_cost_usd,
|
|
77
|
+
s.actual_cost_usd,
|
|
78
|
+
COALESCE((
|
|
79
|
+
SELECT SUBSTR(REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' '), 1, 500)
|
|
80
|
+
FROM messages m
|
|
81
|
+
WHERE m.session_id = s.id AND m.role = 'user' AND m.content IS NOT NULL
|
|
82
|
+
ORDER BY m.timestamp ASC, m.id ASC
|
|
83
|
+
LIMIT 1
|
|
84
|
+
), '') AS first_user_message,
|
|
85
|
+
COALESCE((
|
|
86
|
+
SELECT MAX(m2.timestamp)
|
|
87
|
+
FROM messages m2
|
|
88
|
+
WHERE m2.session_id = s.id
|
|
89
|
+
), s.started_at) AS last_active
|
|
90
|
+
FROM sessions s
|
|
91
|
+
WHERE s.id = ? OR s.id LIKE ?
|
|
92
|
+
ORDER BY LENGTH(s.id) ASC, last_active DESC
|
|
93
|
+
LIMIT 1
|
|
94
|
+
`).get(normalized, `${escapeLikePrefix(normalized)}%`);
|
|
95
|
+
return row ? mapHermesSessionRow(row, stateDbPath, options.workspace) : null;
|
|
96
|
+
}) ?? null;
|
|
97
|
+
}
|
|
98
|
+
export function getHermesSessionActivity(id, options = {}) {
|
|
99
|
+
return getHermesSessionSnapshot(id, { ...options, maxEvents: 0 })?.activity ?? null;
|
|
100
|
+
}
|
|
101
|
+
export function getHermesSessionActivityLog(id, limit = 50, options = {}) {
|
|
102
|
+
const snapshot = getHermesSessionSnapshot(id, { ...options, maxEvents: Math.max(1, limit) });
|
|
103
|
+
return snapshot?.events.slice(-Math.max(1, limit)) ?? [];
|
|
104
|
+
}
|
|
105
|
+
export function getHermesSessionSnapshot(id, options = {}) {
|
|
106
|
+
const record = getHermesSession(id, options);
|
|
107
|
+
if (!record) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const messages = listHermesMessages(record.id, options);
|
|
111
|
+
const parsed = parseHermesActivityEvents(messages, record.id, options.afterLine ?? 0);
|
|
112
|
+
const latestUser = [...parsed.events].reverse().find((event) => event.kind === "user");
|
|
113
|
+
const latestAgent = [...parsed.events].reverse().find((event) => event.kind === "agent");
|
|
114
|
+
const latestTerminal = [...parsed.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
|
|
115
|
+
const latestTool = [...parsed.events].reverse().find((event) => event.kind === "tool" && event.toolName);
|
|
116
|
+
const latestTimestamp = parsed.latestTimestamp ?? record.updatedAt;
|
|
117
|
+
const hasAssistantAfterUser = Boolean(latestUser && latestAgent && latestAgent.lineNumber > latestUser.lineNumber);
|
|
118
|
+
const terminalAfterUser = Boolean(latestUser && latestTerminal && latestTerminal.lineNumber > latestUser.lineNumber);
|
|
119
|
+
const openTurn = Boolean(latestUser && !hasAssistantAfterUser && !terminalAfterUser);
|
|
120
|
+
const staleAfterMs = options.staleAfterMs ?? 5 * 60 * 1000;
|
|
121
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
122
|
+
const stale = Boolean(openTurn && latestTimestamp && nowMs - latestTimestamp.getTime() > staleAfterMs);
|
|
123
|
+
const active = openTurn && !stale;
|
|
124
|
+
const maxEvents = options.maxEvents ?? 50;
|
|
125
|
+
const afterLine = options.afterLine ?? 0;
|
|
126
|
+
const events = maxEvents <= 0
|
|
127
|
+
? []
|
|
128
|
+
: parsed.events.filter((event) => event.lineNumber > afterLine).slice(-maxEvents);
|
|
129
|
+
return {
|
|
130
|
+
agentId: "hermes",
|
|
131
|
+
agentLabel: "Hermes",
|
|
132
|
+
threadId: record.id,
|
|
133
|
+
sourcePath: record.sessionPath,
|
|
134
|
+
sourceLabel: "Hermes state DB",
|
|
135
|
+
lineCount: messages.length,
|
|
136
|
+
activity: {
|
|
137
|
+
agentId: "hermes",
|
|
138
|
+
agentLabel: "Hermes",
|
|
139
|
+
threadId: record.id,
|
|
140
|
+
sourcePath: record.sessionPath,
|
|
141
|
+
sourceLabel: "Hermes state DB",
|
|
142
|
+
active,
|
|
143
|
+
stale,
|
|
144
|
+
turnId: latestUser?.turnId ?? latestTerminal?.turnId ?? null,
|
|
145
|
+
startedAt: latestUser?.timestamp ?? null,
|
|
146
|
+
updatedAt: latestTimestamp,
|
|
147
|
+
},
|
|
148
|
+
events,
|
|
149
|
+
latestAgentMessage: latestAgent?.text ?? null,
|
|
150
|
+
latestUserMessage: latestUser?.text ?? null,
|
|
151
|
+
latestToolName: latestTool?.toolName ?? null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function getHermesSessionDiagnostics(id, options = {}) {
|
|
155
|
+
const stateDbPath = resolveHermesStateDbPath(options);
|
|
156
|
+
if (!BetterSqlite3) {
|
|
157
|
+
return {
|
|
158
|
+
stateDbPath,
|
|
159
|
+
status: "unavailable",
|
|
160
|
+
reason: "better-sqlite3 is not available",
|
|
161
|
+
lineCount: 0,
|
|
162
|
+
updatedAt: null,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (!existsSync(stateDbPath)) {
|
|
166
|
+
return {
|
|
167
|
+
stateDbPath,
|
|
168
|
+
status: "unavailable",
|
|
169
|
+
reason: "Hermes state.db not found",
|
|
170
|
+
lineCount: 0,
|
|
171
|
+
updatedAt: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!id) {
|
|
175
|
+
return {
|
|
176
|
+
stateDbPath,
|
|
177
|
+
status: "unavailable",
|
|
178
|
+
reason: "no active Hermes session",
|
|
179
|
+
lineCount: 0,
|
|
180
|
+
updatedAt: null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const snapshot = getHermesSessionSnapshot(id, { ...options, maxEvents: 0 });
|
|
184
|
+
if (!snapshot) {
|
|
185
|
+
return {
|
|
186
|
+
stateDbPath,
|
|
187
|
+
status: "unavailable",
|
|
188
|
+
reason: "Hermes session not found",
|
|
189
|
+
lineCount: 0,
|
|
190
|
+
updatedAt: null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const status = snapshot.activity.active ? "active" : snapshot.activity.stale ? "stale" : "idle";
|
|
194
|
+
return {
|
|
195
|
+
stateDbPath,
|
|
196
|
+
status,
|
|
197
|
+
reason: snapshot.activity.active
|
|
198
|
+
? "latest Hermes user turn has no assistant response yet"
|
|
199
|
+
: snapshot.activity.stale
|
|
200
|
+
? "open Hermes turn exceeded stale timeout"
|
|
201
|
+
: "latest Hermes turn has a terminal response",
|
|
202
|
+
lineCount: snapshot.lineCount,
|
|
203
|
+
updatedAt: snapshot.activity.updatedAt,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export function listHermesWorkspaces(options = {}) {
|
|
207
|
+
const workspaces = new Set();
|
|
208
|
+
if (options.workspace?.trim()) {
|
|
209
|
+
workspaces.add(options.workspace);
|
|
210
|
+
}
|
|
211
|
+
const rows = withHermesDatabase(options, (db) => db.prepare(`
|
|
212
|
+
SELECT source, model_config
|
|
213
|
+
FROM sessions
|
|
214
|
+
ORDER BY COALESCE(ended_at, started_at) DESC, started_at DESC
|
|
215
|
+
LIMIT 500
|
|
216
|
+
`).all()) ?? [];
|
|
217
|
+
for (const row of rows) {
|
|
218
|
+
const workspace = extractHermesWorkspace(row);
|
|
219
|
+
if (workspace) {
|
|
220
|
+
workspaces.add(workspace);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return [...workspaces].sort((left, right) => left.localeCompare(right));
|
|
224
|
+
}
|
|
225
|
+
export function listHermesMessages(id, options = {}) {
|
|
226
|
+
return withHermesDatabase(options, (db) => db.prepare(`
|
|
227
|
+
SELECT id, role, content, tool_name, timestamp, token_count, reasoning, reasoning_content
|
|
228
|
+
FROM messages
|
|
229
|
+
WHERE session_id = ?
|
|
230
|
+
ORDER BY timestamp ASC, id ASC
|
|
231
|
+
`).all(id)) ?? [];
|
|
232
|
+
}
|
|
233
|
+
function parseHermesActivityEvents(messages, sessionId, afterLine) {
|
|
234
|
+
const events = [];
|
|
235
|
+
let latestTimestamp = null;
|
|
236
|
+
let currentTurnId = null;
|
|
237
|
+
for (const [index, message] of messages.entries()) {
|
|
238
|
+
const lineNumber = index + 1;
|
|
239
|
+
const timestamp = unixSecondsToDate(message.timestamp);
|
|
240
|
+
if (timestamp && (!latestTimestamp || timestamp > latestTimestamp)) {
|
|
241
|
+
latestTimestamp = timestamp;
|
|
242
|
+
}
|
|
243
|
+
const role = stringValue(message.role);
|
|
244
|
+
const text = extractHermesMessageText(message);
|
|
245
|
+
const toolName = extractHermesToolName(message);
|
|
246
|
+
const reasoningText = extractHermesReasoningText(message);
|
|
247
|
+
if (role === "user") {
|
|
248
|
+
currentTurnId = `hermes-${sessionId}-${lineNumber}`;
|
|
249
|
+
pushEvent(events, afterLine, {
|
|
250
|
+
lineNumber,
|
|
251
|
+
kind: "task",
|
|
252
|
+
timestamp,
|
|
253
|
+
type: "turn",
|
|
254
|
+
turnId: currentTurnId,
|
|
255
|
+
status: "started",
|
|
256
|
+
text,
|
|
257
|
+
toolName: null,
|
|
258
|
+
phase: null,
|
|
259
|
+
});
|
|
260
|
+
pushEvent(events, afterLine, {
|
|
261
|
+
lineNumber,
|
|
262
|
+
kind: "user",
|
|
263
|
+
timestamp,
|
|
264
|
+
type: "message",
|
|
265
|
+
turnId: currentTurnId,
|
|
266
|
+
status: null,
|
|
267
|
+
text,
|
|
268
|
+
toolName: null,
|
|
269
|
+
phase: null,
|
|
270
|
+
});
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (role === "assistant") {
|
|
274
|
+
if (reasoningText) {
|
|
275
|
+
pushEvent(events, afterLine, {
|
|
276
|
+
lineNumber,
|
|
277
|
+
kind: "tool",
|
|
278
|
+
timestamp,
|
|
279
|
+
type: "reasoning",
|
|
280
|
+
turnId: currentTurnId,
|
|
281
|
+
status: "finished",
|
|
282
|
+
text: reasoningText,
|
|
283
|
+
toolName: "reasoning",
|
|
284
|
+
phase: null,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (toolName && !text) {
|
|
288
|
+
pushEvent(events, afterLine, {
|
|
289
|
+
lineNumber,
|
|
290
|
+
kind: "tool",
|
|
291
|
+
timestamp,
|
|
292
|
+
type: "tool_call",
|
|
293
|
+
turnId: currentTurnId,
|
|
294
|
+
status: "started",
|
|
295
|
+
text: null,
|
|
296
|
+
toolName,
|
|
297
|
+
phase: null,
|
|
298
|
+
});
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (reasoningText && !text) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
pushEvent(events, afterLine, {
|
|
305
|
+
lineNumber,
|
|
306
|
+
kind: "agent",
|
|
307
|
+
timestamp,
|
|
308
|
+
type: "message",
|
|
309
|
+
turnId: currentTurnId,
|
|
310
|
+
status: "completed",
|
|
311
|
+
text,
|
|
312
|
+
toolName: null,
|
|
313
|
+
phase: null,
|
|
314
|
+
});
|
|
315
|
+
pushEvent(events, afterLine, {
|
|
316
|
+
lineNumber,
|
|
317
|
+
kind: "task",
|
|
318
|
+
timestamp,
|
|
319
|
+
type: "turn",
|
|
320
|
+
turnId: currentTurnId,
|
|
321
|
+
status: "completed",
|
|
322
|
+
text: null,
|
|
323
|
+
toolName: null,
|
|
324
|
+
phase: null,
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (role === "tool") {
|
|
329
|
+
pushEvent(events, afterLine, {
|
|
330
|
+
lineNumber,
|
|
331
|
+
kind: "tool",
|
|
332
|
+
timestamp,
|
|
333
|
+
type: "tool",
|
|
334
|
+
turnId: currentTurnId,
|
|
335
|
+
status: "started",
|
|
336
|
+
text: null,
|
|
337
|
+
toolName: toolName ?? "tool",
|
|
338
|
+
phase: null,
|
|
339
|
+
});
|
|
340
|
+
pushEvent(events, afterLine, {
|
|
341
|
+
lineNumber,
|
|
342
|
+
kind: "tool",
|
|
343
|
+
timestamp,
|
|
344
|
+
type: "tool",
|
|
345
|
+
turnId: currentTurnId,
|
|
346
|
+
status: "finished",
|
|
347
|
+
text,
|
|
348
|
+
toolName: toolName ?? "tool",
|
|
349
|
+
phase: null,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { events, latestTimestamp };
|
|
354
|
+
}
|
|
355
|
+
function mapHermesSessionRow(row, stateDbPath, workspace) {
|
|
356
|
+
const usage = mapHermesUsage(row);
|
|
357
|
+
const firstUserMessage = stringValue(row.first_user_message);
|
|
358
|
+
const title = stringValue(row.title) ?? summarizeTitle(firstUserMessage);
|
|
359
|
+
return {
|
|
360
|
+
id: String(row.id ?? ""),
|
|
361
|
+
title,
|
|
362
|
+
cwd: extractHermesWorkspace(row, workspace) ?? process.cwd(),
|
|
363
|
+
model: stringValue(row.model),
|
|
364
|
+
reasoningEffort: parseReasoningFromModelConfig(row.model_config),
|
|
365
|
+
createdAt: unixSecondsToDate(row.started_at) ?? new Date(0),
|
|
366
|
+
updatedAt: unixSecondsToDate(row.last_active) ?? unixSecondsToDate(row.started_at) ?? new Date(0),
|
|
367
|
+
firstUserMessage,
|
|
368
|
+
agentId: "hermes",
|
|
369
|
+
sessionPath: stateDbPath,
|
|
370
|
+
source: stringValue(row.source) ?? "unknown",
|
|
371
|
+
messageCount: numberValue(row.message_count) ?? 0,
|
|
372
|
+
endedAt: unixSecondsToDate(row.ended_at),
|
|
373
|
+
usage,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function mapHermesUsage(row) {
|
|
377
|
+
const input = numberValue(row.input_tokens) ?? 0;
|
|
378
|
+
const output = numberValue(row.output_tokens) ?? 0;
|
|
379
|
+
const cacheRead = numberValue(row.cache_read_tokens) ?? 0;
|
|
380
|
+
const cacheWrite = numberValue(row.cache_write_tokens) ?? 0;
|
|
381
|
+
const reasoning = numberValue(row.reasoning_tokens) ?? 0;
|
|
382
|
+
const cost = numberValue(row.actual_cost_usd) ?? numberValue(row.estimated_cost_usd) ?? undefined;
|
|
383
|
+
if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0 && reasoning === 0 && cost === undefined) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
input,
|
|
388
|
+
output,
|
|
389
|
+
cacheRead,
|
|
390
|
+
cacheWrite,
|
|
391
|
+
total: input + output + cacheRead + cacheWrite + reasoning,
|
|
392
|
+
cost,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function parseReasoningFromModelConfig(value) {
|
|
396
|
+
const raw = stringValue(value);
|
|
397
|
+
if (!raw) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const parsed = JSON.parse(raw);
|
|
402
|
+
const agent = objectValue(parsed.agent);
|
|
403
|
+
return stringValue(parsed.reasoning_effort) ?? stringValue(agent?.reasoning_effort);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function extractHermesWorkspace(row, fallback) {
|
|
410
|
+
const parsed = parseJsonValue(stringValue(row.model_config));
|
|
411
|
+
const config = objectValue(parsed);
|
|
412
|
+
const agent = objectValue(config?.agent);
|
|
413
|
+
const candidates = [
|
|
414
|
+
stringValue(config?.cwd),
|
|
415
|
+
stringValue(config?.workspace),
|
|
416
|
+
stringValue(config?.working_directory),
|
|
417
|
+
stringValue(config?.workingDirectory),
|
|
418
|
+
stringValue(config?.project_dir),
|
|
419
|
+
stringValue(config?.projectDir),
|
|
420
|
+
stringValue(config?.repo_path),
|
|
421
|
+
stringValue(config?.repository),
|
|
422
|
+
stringValue(agent?.cwd),
|
|
423
|
+
stringValue(agent?.workspace),
|
|
424
|
+
workspaceFromSource(row.source),
|
|
425
|
+
fallback,
|
|
426
|
+
];
|
|
427
|
+
for (const candidate of candidates) {
|
|
428
|
+
if (candidate?.trim() && path.isAbsolute(candidate.trim())) {
|
|
429
|
+
return path.normalize(candidate.trim());
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return fallback?.trim() ? fallback : null;
|
|
433
|
+
}
|
|
434
|
+
function workspaceFromSource(value) {
|
|
435
|
+
const raw = stringValue(value);
|
|
436
|
+
if (!raw) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
if (path.isAbsolute(raw)) {
|
|
440
|
+
return raw;
|
|
441
|
+
}
|
|
442
|
+
if (raw.startsWith("file://")) {
|
|
443
|
+
const pathname = raw.slice("file://".length);
|
|
444
|
+
try {
|
|
445
|
+
return decodeURIComponent(pathname);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return pathname;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const keyValueMatch = raw.match(/(?:cwd|workspace|workdir|path|dir)=([^;,]+)/i);
|
|
452
|
+
if (keyValueMatch?.[1] && path.isAbsolute(keyValueMatch[1].trim())) {
|
|
453
|
+
return keyValueMatch[1].trim();
|
|
454
|
+
}
|
|
455
|
+
const prefixedPathMatch = raw.match(/^[a-z0-9_-]+:(\/.+)$/i);
|
|
456
|
+
if (prefixedPathMatch?.[1] && path.isAbsolute(prefixedPathMatch[1].trim())) {
|
|
457
|
+
return prefixedPathMatch[1].trim();
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
function withHermesDatabase(options, fn) {
|
|
462
|
+
if (!BetterSqlite3) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const stateDbPath = resolveHermesStateDbPath(options);
|
|
466
|
+
if (!existsSync(stateDbPath)) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
let db = null;
|
|
470
|
+
try {
|
|
471
|
+
db = new BetterSqlite3(stateDbPath, { readonly: true, fileMustExist: true });
|
|
472
|
+
return fn(db, stateDbPath);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
finally {
|
|
478
|
+
try {
|
|
479
|
+
db?.close();
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// Ignore close failures.
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function pushEvent(events, afterLine, event) {
|
|
487
|
+
if (event.lineNumber > afterLine) {
|
|
488
|
+
events.push(event);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function unixSecondsToDate(value) {
|
|
492
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
493
|
+
return new Date(value * 1000);
|
|
494
|
+
}
|
|
495
|
+
if (typeof value === "string" && value.trim()) {
|
|
496
|
+
const numeric = Number(value);
|
|
497
|
+
if (Number.isFinite(numeric)) {
|
|
498
|
+
return new Date(numeric * 1000);
|
|
499
|
+
}
|
|
500
|
+
const parsed = Date.parse(value);
|
|
501
|
+
return Number.isNaN(parsed) ? null : new Date(parsed);
|
|
502
|
+
}
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
function numberValue(value) {
|
|
506
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
507
|
+
}
|
|
508
|
+
function stringValue(value) {
|
|
509
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
510
|
+
}
|
|
511
|
+
function objectValue(value) {
|
|
512
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
513
|
+
? value
|
|
514
|
+
: null;
|
|
515
|
+
}
|
|
516
|
+
function extractHermesMessageText(message) {
|
|
517
|
+
const raw = stringValue(message.content);
|
|
518
|
+
if (!raw) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
const parsed = parseJsonValue(raw);
|
|
522
|
+
return parsed ? extractTextFromValue(parsed) ?? raw : raw;
|
|
523
|
+
}
|
|
524
|
+
function extractHermesReasoningText(message) {
|
|
525
|
+
const direct = stringValue(message.reasoning_content) ?? stringValue(message.reasoning);
|
|
526
|
+
if (!direct) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
const parsed = parseJsonValue(direct);
|
|
530
|
+
return parsed ? extractTextFromValue(parsed) ?? direct : direct;
|
|
531
|
+
}
|
|
532
|
+
function extractHermesToolName(message) {
|
|
533
|
+
const direct = stringValue(message.tool_name);
|
|
534
|
+
if (direct) {
|
|
535
|
+
return direct;
|
|
536
|
+
}
|
|
537
|
+
const parsed = parseJsonValue(stringValue(message.content));
|
|
538
|
+
return parsed ? extractToolNameFromValue(parsed) : null;
|
|
539
|
+
}
|
|
540
|
+
function parseJsonValue(raw) {
|
|
541
|
+
if (!raw) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
const trimmed = raw.trim();
|
|
545
|
+
if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
return JSON.parse(trimmed);
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function extractTextFromValue(value) {
|
|
556
|
+
if (typeof value === "string") {
|
|
557
|
+
return value.trim() || null;
|
|
558
|
+
}
|
|
559
|
+
if (Array.isArray(value)) {
|
|
560
|
+
const text = value.map(extractTextFromValue).filter(Boolean).join("\n").trim();
|
|
561
|
+
return text || null;
|
|
562
|
+
}
|
|
563
|
+
const object = objectValue(value);
|
|
564
|
+
if (!object) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
const direct = stringValue(object.text) ??
|
|
568
|
+
stringValue(object.content) ??
|
|
569
|
+
stringValue(object.message) ??
|
|
570
|
+
stringValue(object.output) ??
|
|
571
|
+
stringValue(object.result);
|
|
572
|
+
if (direct) {
|
|
573
|
+
return direct;
|
|
574
|
+
}
|
|
575
|
+
if (Array.isArray(object.content)) {
|
|
576
|
+
return extractTextFromValue(object.content);
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
function extractToolNameFromValue(value) {
|
|
581
|
+
const object = objectValue(value);
|
|
582
|
+
if (!object) {
|
|
583
|
+
if (Array.isArray(value)) {
|
|
584
|
+
for (const entry of value) {
|
|
585
|
+
const name = extractToolNameFromValue(entry);
|
|
586
|
+
if (name)
|
|
587
|
+
return name;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const functionObject = objectValue(object.function);
|
|
593
|
+
const toolCall = objectValue(object.tool_call) ?? objectValue(object.toolCall);
|
|
594
|
+
return (stringValue(object.tool_name) ??
|
|
595
|
+
stringValue(object.toolName) ??
|
|
596
|
+
stringValue(object.name) ??
|
|
597
|
+
stringValue(functionObject?.name) ??
|
|
598
|
+
extractToolNameFromValue(toolCall));
|
|
599
|
+
}
|
|
600
|
+
function escapeLikePrefix(value) {
|
|
601
|
+
return value.replace(/[%_\\]/g, (char) => `\\${char}`);
|
|
602
|
+
}
|
|
603
|
+
function summarizeTitle(text) {
|
|
604
|
+
if (!text) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
608
|
+
return normalized.length <= 60 ? normalized : `${normalized.slice(0, 57)}...`;
|
|
609
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,13 +2,21 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { webhookCallback } from "grammy";
|
|
5
|
+
import { agentLabel } from "./agent.js";
|
|
5
6
|
import { createBot, registerCommands } from "./bot.js";
|
|
6
7
|
import { checkAuthStatus } from "./codex-auth.js";
|
|
7
8
|
import { describeCodexCli, resolveCodexCli } from "./codex-cli.js";
|
|
9
|
+
import { checkClaudeCodeAuthStatus } from "./claude-code-auth.js";
|
|
10
|
+
import { describeClaudeCodeCli, resolveClaudeCodeCli } from "./claude-code-cli.js";
|
|
8
11
|
import { findLaunchProfile, formatLaunchProfileBehavior } from "./codex-launch.js";
|
|
9
12
|
import { enabledAgents } from "./agent-factory.js";
|
|
10
13
|
import { loadConfig } from "./config.js";
|
|
14
|
+
import { checkHermesAuthStatus } from "./hermes-auth.js";
|
|
15
|
+
import { describeHermesCli, resolveHermesCli } from "./hermes-cli.js";
|
|
16
|
+
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
17
|
+
import { describeOpenClawCli, resolveOpenClawCli } from "./openclaw-cli.js";
|
|
11
18
|
import { installConsoleLogger } from "./logger.js";
|
|
19
|
+
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
12
20
|
import { describePiCli, resolvePiCli } from "./pi-cli.js";
|
|
13
21
|
import { configureRedaction } from "./redaction.js";
|
|
14
22
|
import { SessionRegistry } from "./session-registry.js";
|
|
@@ -25,14 +33,10 @@ try {
|
|
|
25
33
|
bot = createBot(config, registry);
|
|
26
34
|
await registerCommands(bot);
|
|
27
35
|
console.log("NordRelay running");
|
|
28
|
-
const authStatus = config
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.log(`Auth: ${authStatus.authenticated ? "authenticated" : "not authenticated"} (${authStatus.method})`);
|
|
33
|
-
if (!authStatus.authenticated) {
|
|
34
|
-
console.warn("Warning: Codex is not authenticated. Use /login or set CODEX_API_KEY.");
|
|
35
|
-
}
|
|
36
|
+
const authStatus = await checkDefaultAgentAuth(config);
|
|
37
|
+
console.log(`Auth (${agentLabel(config.defaultAgent)}): ${authStatus.authenticated ? "authenticated" : "not authenticated"} (${authStatus.method})`);
|
|
38
|
+
if (!authStatus.authenticated) {
|
|
39
|
+
console.warn(`Warning: ${agentLabel(config.defaultAgent)} is not authenticated. ${authStatus.detail}`);
|
|
36
40
|
}
|
|
37
41
|
console.log(`Workspace: ${config.workspace}`);
|
|
38
42
|
console.log(`Enabled agents: ${enabledAgents(config).join(", ")} (default: ${config.defaultAgent})`);
|
|
@@ -41,8 +45,20 @@ try {
|
|
|
41
45
|
}
|
|
42
46
|
const codexCli = resolveCodexCli();
|
|
43
47
|
const piCli = resolvePiCli(process.env, config.piCliPath);
|
|
48
|
+
const hermesCli = resolveHermesCli(process.env, config.hermesCliPath);
|
|
49
|
+
const openClawCli = resolveOpenClawCli(process.env, config.openClawCliPath);
|
|
50
|
+
const claudeCodeCli = resolveClaudeCodeCli(process.env, config.claudeCodeCliPath);
|
|
44
51
|
console.log(`Codex CLI: ${describeCodexCli(codexCli)}`);
|
|
45
52
|
console.log(`Pi CLI: ${describePiCli(piCli)}`);
|
|
53
|
+
console.log(`Hermes CLI: ${describeHermesCli(hermesCli)}`);
|
|
54
|
+
console.log(`OpenClaw CLI: ${describeOpenClawCli(openClawCli)}`);
|
|
55
|
+
console.log(`Claude Code CLI: ${describeClaudeCodeCli(claudeCodeCli)}`);
|
|
56
|
+
if (config.hermesEnabled) {
|
|
57
|
+
console.log(`Hermes API: ${config.hermesApiBaseUrl}`);
|
|
58
|
+
}
|
|
59
|
+
if (config.openClawEnabled) {
|
|
60
|
+
console.log(`OpenClaw Gateway: ${config.openClawGatewayUrl}`);
|
|
61
|
+
}
|
|
46
62
|
const defaultLaunchProfile = findLaunchProfile(config.launchProfiles, config.defaultLaunchProfileId);
|
|
47
63
|
if (defaultLaunchProfile) {
|
|
48
64
|
console.log(`Default launch profile: ${defaultLaunchProfile.label} (${formatLaunchProfileBehavior(defaultLaunchProfile)})`);
|
|
@@ -62,6 +78,10 @@ try {
|
|
|
62
78
|
authMethod: authStatus.method,
|
|
63
79
|
codexCli: describeCodexCli(codexCli),
|
|
64
80
|
piCli: describePiCli(piCli),
|
|
81
|
+
hermesCli: describeHermesCli(hermesCli),
|
|
82
|
+
openClawCli: describeOpenClawCli(openClawCli),
|
|
83
|
+
claudeCodeCli: describeClaudeCodeCli(claudeCodeCli),
|
|
84
|
+
openClawGateway: config.openClawGatewayUrl,
|
|
65
85
|
telegramTransport: config.telegramTransport,
|
|
66
86
|
});
|
|
67
87
|
}
|
|
@@ -77,6 +97,29 @@ catch (error) {
|
|
|
77
97
|
registry?.disposeAll();
|
|
78
98
|
process.exit(1);
|
|
79
99
|
}
|
|
100
|
+
async function checkDefaultAgentAuth(config) {
|
|
101
|
+
const agentId = config.defaultAgent;
|
|
102
|
+
if (agentId === "pi") {
|
|
103
|
+
return checkPiAuthStatus(config.piDefaultModel);
|
|
104
|
+
}
|
|
105
|
+
if (agentId === "hermes") {
|
|
106
|
+
return checkHermesAuthStatus({
|
|
107
|
+
baseUrl: config.hermesApiBaseUrl,
|
|
108
|
+
apiKey: config.hermesApiKey,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (agentId === "openclaw") {
|
|
112
|
+
return checkOpenClawAuthStatus({
|
|
113
|
+
gatewayUrl: config.openClawGatewayUrl,
|
|
114
|
+
token: config.openClawGatewayToken,
|
|
115
|
+
password: config.openClawGatewayPassword,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (agentId === "claude-code") {
|
|
119
|
+
return checkClaudeCodeAuthStatus(config.claudeCodeCliPath);
|
|
120
|
+
}
|
|
121
|
+
return checkAuthStatus(config.codexApiKey);
|
|
122
|
+
}
|
|
80
123
|
let shuttingDown = false;
|
|
81
124
|
const shutdown = (signal) => {
|
|
82
125
|
if (shuttingDown) {
|