@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.
Files changed (48) hide show
  1. package/.env.example +45 -2
  2. package/README.md +204 -30
  3. package/dist/agent-activity.js +300 -0
  4. package/dist/agent-adapter.js +17 -30
  5. package/dist/agent-factory.js +27 -0
  6. package/dist/agent.js +123 -9
  7. package/dist/artifacts.js +1 -1
  8. package/dist/audit-log.js +1 -1
  9. package/dist/bot-ui.js +1 -1
  10. package/dist/bot.js +328 -159
  11. package/dist/claude-code-auth.js +121 -0
  12. package/dist/claude-code-cli.js +19 -0
  13. package/dist/claude-code-launch.js +73 -0
  14. package/dist/claude-code-session.js +660 -0
  15. package/dist/claude-code-state.js +590 -0
  16. package/dist/codex-session.js +12 -1
  17. package/dist/config.js +113 -9
  18. package/dist/hermes-api.js +150 -0
  19. package/dist/hermes-auth.js +96 -0
  20. package/dist/hermes-cli.js +19 -0
  21. package/dist/hermes-launch.js +57 -0
  22. package/dist/hermes-session.js +477 -0
  23. package/dist/hermes-state.js +609 -0
  24. package/dist/index.js +51 -8
  25. package/dist/openclaw-auth.js +27 -0
  26. package/dist/openclaw-cli.js +19 -0
  27. package/dist/openclaw-gateway.js +285 -0
  28. package/dist/openclaw-launch.js +65 -0
  29. package/dist/openclaw-session.js +549 -0
  30. package/dist/openclaw-state.js +409 -0
  31. package/dist/operations.js +83 -2
  32. package/dist/pi-auth.js +59 -0
  33. package/dist/pi-launch.js +61 -0
  34. package/dist/pi-rpc.js +18 -0
  35. package/dist/pi-session.js +103 -15
  36. package/dist/pi-state.js +253 -0
  37. package/dist/relay-runtime.js +673 -51
  38. package/dist/session-format.js +28 -18
  39. package/dist/session-registry.js +40 -15
  40. package/dist/settings-service.js +35 -4
  41. package/dist/web-dashboard-ui.js +18 -0
  42. package/dist/web-dashboard.js +329 -47
  43. package/package.json +8 -3
  44. package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
  45. package/plugins/nordrelay/commands/remote.md +2 -2
  46. package/plugins/nordrelay/scripts/nordrelay.mjs +131 -3
  47. package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
  48. 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.codexEnabled
29
- ? await checkAuthStatus(config.codexApiKey)
30
- : { authenticated: true, method: "codex-disabled" };
31
- if (config.codexEnabled) {
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) {