@nordbyte/nordrelay 0.4.0 → 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/README.md +17 -5
- package/dist/access-control.js +3 -0
- package/dist/agent-feature-matrix.js +42 -0
- package/dist/agent-updates.js +294 -0
- package/dist/bot.js +169 -209
- package/dist/channel-actions.js +372 -0
- package/dist/operations.js +33 -8
- package/dist/relay-runtime.js +128 -24
- package/dist/session-format.js +72 -3
- 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.js +62 -244
- package/package.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +56 -9
package/dist/bot.js
CHANGED
|
@@ -8,9 +8,11 @@ import { hasTelegramPermission, permissionForCallbackData, permissionForCommand,
|
|
|
8
8
|
import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
|
|
9
9
|
import { collectArtifactReport, collectRecentWorkspaceArtifacts, createArtifactZipBundle, ensureOutDir, formatArtifactSummary, getArtifactTurnReport, isTelegramImagePreview, listRecentArtifactReports, persistWorkspaceArtifactReport, pruneConnectorTurnDirs, removeArtifactTurn, telegramArtifactFilename, totalArtifactSize, } from "./artifacts.js";
|
|
10
10
|
import { listAgentAdapterDescriptors } from "./agent-adapter.js";
|
|
11
|
+
import { AgentUpdateManager } from "./agent-updates.js";
|
|
11
12
|
import { AuditLogStore } from "./audit-log.js";
|
|
12
13
|
import { formatSessionLabel, renderHelpMessage, renderWelcomeFirstTime, renderWelcomeReturning, } from "./bot-ui.js";
|
|
13
14
|
import { BotPreferencesStore, formatQuietHours, isQuietNow, parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
|
|
15
|
+
import { logTailRequests, parseAgentUpdateId, parseLogsCommand, renderAgentUpdateJobAction, renderAgentUpdateJobsAction, renderAgentUpdateLogAction, renderAgentUpdatePickerAction, renderAgentsAction, renderArtifactReportsAction, renderChannelsAction, renderLogTailsAction, renderQueueListAction, renderQueuedPromptDetailAction, renderSelfUpdateStartedAction, } from "./channel-actions.js";
|
|
14
16
|
import { listChannelDescriptors } from "./channel-adapter.js";
|
|
15
17
|
import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
|
|
16
18
|
import { getAgentActivityLog, getAgentDiagnostics, getExternalActivityForSession, getExternalSnapshotForSession, } from "./agent-activity.js";
|
|
@@ -21,14 +23,14 @@ import { formatLaunchProfileBehavior } from "./codex-launch.js";
|
|
|
21
23
|
import { contextKeyFromCtx, isTelegramContextKey, isTopicContextKey, parseContextKey } from "./context-key.js";
|
|
22
24
|
import { friendlyErrorText } from "./error-messages.js";
|
|
23
25
|
import { escapeHTML, formatTelegramHTML } from "./format.js";
|
|
24
|
-
import { getConnectorHealth,
|
|
26
|
+
import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate, } from "./operations.js";
|
|
25
27
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
26
28
|
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
|
|
27
29
|
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
28
30
|
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
29
31
|
import { configureRedaction, redactText } from "./redaction.js";
|
|
30
32
|
import { canWriteWithLock, SessionLockStore } from "./session-locks.js";
|
|
31
|
-
import {
|
|
33
|
+
import { renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
|
|
32
34
|
import { SessionRegistry } from "./session-registry.js";
|
|
33
35
|
import { getAvailableBackends, transcribeAudio } from "./voice.js";
|
|
34
36
|
import { getTelegramRateLimitMetrics, telegramRateLimiter } from "./telegram-rate-limit.js";
|
|
@@ -67,6 +69,37 @@ function paginateKeyboard(items, page, prefix) {
|
|
|
67
69
|
}
|
|
68
70
|
return keyboard;
|
|
69
71
|
}
|
|
72
|
+
function actionKeyboard(rows) {
|
|
73
|
+
if (!rows || rows.length === 0) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const keyboard = new InlineKeyboard();
|
|
77
|
+
for (const row of rows) {
|
|
78
|
+
for (const button of row) {
|
|
79
|
+
keyboard.text(button.label, telegramActionData(button.action));
|
|
80
|
+
}
|
|
81
|
+
keyboard.row();
|
|
82
|
+
}
|
|
83
|
+
return keyboard;
|
|
84
|
+
}
|
|
85
|
+
function telegramActionData(action) {
|
|
86
|
+
if (action === "agent-update:jobs") {
|
|
87
|
+
return "upd_jobs";
|
|
88
|
+
}
|
|
89
|
+
const agentUpdateStart = action.match(/^agent-update:start:(.+)$/);
|
|
90
|
+
if (agentUpdateStart?.[1]) {
|
|
91
|
+
return `upd_agent:${agentUpdateStart[1]}`;
|
|
92
|
+
}
|
|
93
|
+
const agentUpdateLog = action.match(/^agent-update:log:(.+)$/);
|
|
94
|
+
if (agentUpdateLog?.[1]) {
|
|
95
|
+
return `upd_log:${agentUpdateLog[1]}`;
|
|
96
|
+
}
|
|
97
|
+
const agentUpdateCancel = action.match(/^agent-update:cancel:(.+)$/);
|
|
98
|
+
if (agentUpdateCancel?.[1]) {
|
|
99
|
+
return `upd_cancel:${agentUpdateCancel[1]}`;
|
|
100
|
+
}
|
|
101
|
+
return action;
|
|
102
|
+
}
|
|
70
103
|
export function createBot(config, registry) {
|
|
71
104
|
configureRedaction(config.telegramRedactPatterns);
|
|
72
105
|
telegramRateLimiter.configure({
|
|
@@ -94,6 +127,7 @@ export function createBot(config, registry) {
|
|
|
94
127
|
const preferencesStore = new BotPreferencesStore(config.workspace, config.stateBackend);
|
|
95
128
|
const auditLog = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
96
129
|
const lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
130
|
+
const agentUpdates = new AgentUpdateManager();
|
|
97
131
|
const drainingQueues = new Set();
|
|
98
132
|
const externalQueueTimers = new Map();
|
|
99
133
|
const externalMirrors = new Map();
|
|
@@ -220,6 +254,37 @@ export function createBot(config, registry) {
|
|
|
220
254
|
}
|
|
221
255
|
return checkAuthStatus(config.codexApiKey);
|
|
222
256
|
};
|
|
257
|
+
const agentUpdateContext = () => ({
|
|
258
|
+
piCliPath: config.piCliPath,
|
|
259
|
+
hermesCliPath: config.hermesCliPath,
|
|
260
|
+
openClawCliPath: config.openClawCliPath,
|
|
261
|
+
claudeCodeCliPath: config.claudeCodeCliPath,
|
|
262
|
+
});
|
|
263
|
+
const startTelegramAgentUpdate = async (ctx, agentId) => {
|
|
264
|
+
try {
|
|
265
|
+
const job = agentUpdates.start(agentId, agentUpdateContext());
|
|
266
|
+
const contextKey = contextKeyFromCtx(ctx);
|
|
267
|
+
if (contextKey) {
|
|
268
|
+
audit({
|
|
269
|
+
action: "command",
|
|
270
|
+
status: "ok",
|
|
271
|
+
contextKey,
|
|
272
|
+
agentId,
|
|
273
|
+
description: `update ${agentId}`,
|
|
274
|
+
detail: job.summary,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const rendered = renderAgentUpdateJobAction(job);
|
|
278
|
+
await safeReply(ctx, rendered.html, {
|
|
279
|
+
fallbackText: rendered.plain,
|
|
280
|
+
replyMarkup: actionKeyboard(rendered.buttons),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
const message = `Failed to start ${agentLabel(agentId)} update: ${friendlyErrorText(error)}`;
|
|
285
|
+
await safeReply(ctx, `<b>Update failed:</b> ${escapeHTML(message)}`, { fallbackText: message });
|
|
286
|
+
}
|
|
287
|
+
};
|
|
223
288
|
const startAgentLogin = (info) => {
|
|
224
289
|
const agentId = agentIdForAuth(info);
|
|
225
290
|
if (agentId === "hermes") {
|
|
@@ -324,22 +389,10 @@ export function createBot(config, registry) {
|
|
|
324
389
|
const createQueuedPromptCancelKeyboard = (contextKey, queueId, label = "Cancel queued message") => new InlineKeyboard().text(label, queueCancelCallbackData("cancel", contextKey, queueId));
|
|
325
390
|
const renderQueueList = (contextKey, queue) => {
|
|
326
391
|
const paused = promptStore.isPaused(contextKey);
|
|
392
|
+
const rendered = renderQueueListAction(queue, paused);
|
|
327
393
|
if (queue.length === 0) {
|
|
328
|
-
return
|
|
329
|
-
plain: paused ? "Queue is empty and paused." : "Queue is empty.",
|
|
330
|
-
html: escapeHTML(paused ? "Queue is empty and paused." : "Queue is empty."),
|
|
331
|
-
};
|
|
394
|
+
return rendered;
|
|
332
395
|
}
|
|
333
|
-
const lines = queue.map((item, index) => {
|
|
334
|
-
const age = formatRelativeTime(new Date(item.createdAt));
|
|
335
|
-
const attempts = item.attempts && item.attempts > 0 ? ` · attempts ${item.attempts}` : "";
|
|
336
|
-
const error = item.lastError ? ` · last error: ${trimLine(item.lastError, 80)}` : "";
|
|
337
|
-
const scheduled = item.notBefore && item.notBefore > Date.now()
|
|
338
|
-
? `scheduled ${formatLocalDateTime(new Date(item.notBefore))}`
|
|
339
|
-
: index === 0 ? "next" : `after ${index} queued item${index === 1 ? "" : "s"}`;
|
|
340
|
-
const eta = scheduled;
|
|
341
|
-
return `${index + 1}. ${item.id} · ${age} · ${eta}${attempts}${error} · ${item.description}`;
|
|
342
|
-
});
|
|
343
396
|
const keyboard = new InlineKeyboard();
|
|
344
397
|
queue.forEach((item, index) => {
|
|
345
398
|
keyboard
|
|
@@ -352,11 +405,7 @@ export function createBot(config, registry) {
|
|
|
352
405
|
.text("Down", queueCancelCallbackData("down", contextKey, item.id))
|
|
353
406
|
.row();
|
|
354
407
|
});
|
|
355
|
-
return {
|
|
356
|
-
plain: [paused ? "Queued prompts (paused):" : "Queued prompts:", ...lines].join("\n"),
|
|
357
|
-
html: [paused ? "<b>Queued prompts:</b> <code>paused</code>" : "<b>Queued prompts:</b>", ...lines.map(escapeHTML)].join("\n"),
|
|
358
|
-
keyboard,
|
|
359
|
-
};
|
|
408
|
+
return { ...rendered, keyboard };
|
|
360
409
|
};
|
|
361
410
|
const createSystemContext = (contextKey) => {
|
|
362
411
|
const parsed = parseContextKey(contextKey);
|
|
@@ -1629,60 +1678,12 @@ export function createBot(config, registry) {
|
|
|
1629
1678
|
await safeReply(ctx, help.html, { fallbackText: help.plain });
|
|
1630
1679
|
});
|
|
1631
1680
|
bot.command("channels", async (ctx) => {
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
const status = descriptor.status === "available" ? "available" : "planned";
|
|
1635
|
-
return `${descriptor.label}: ${status} · ${descriptor.capabilities.join(", ")}`;
|
|
1636
|
-
});
|
|
1637
|
-
const html = [
|
|
1638
|
-
"<b>Channel adapters:</b>",
|
|
1639
|
-
...descriptors.map((descriptor) => {
|
|
1640
|
-
const statusIcon = descriptor.status === "available" ? "✅" : "🟡";
|
|
1641
|
-
const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
|
|
1642
|
-
return `${statusIcon} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(descriptor.status)}</code>\n <code>${escapeHTML(descriptor.capabilities.join(", "))}</code>${notes}`;
|
|
1643
|
-
}),
|
|
1644
|
-
].join("\n");
|
|
1645
|
-
await safeReply(ctx, html, { fallbackText: ["Channel adapters:", ...lines].join("\n") });
|
|
1681
|
+
const rendered = renderChannelsAction(listChannelDescriptors());
|
|
1682
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
1646
1683
|
});
|
|
1647
1684
|
bot.command("agents", async (ctx) => {
|
|
1648
|
-
const
|
|
1649
|
-
|
|
1650
|
-
"Agent adapters:",
|
|
1651
|
-
...descriptors.map((descriptor) => {
|
|
1652
|
-
const enabled = descriptor.id === "codex"
|
|
1653
|
-
? config.codexEnabled
|
|
1654
|
-
: descriptor.id === "pi"
|
|
1655
|
-
? config.piEnabled
|
|
1656
|
-
: descriptor.id === "hermes"
|
|
1657
|
-
? config.hermesEnabled
|
|
1658
|
-
: descriptor.id === "openclaw"
|
|
1659
|
-
? config.openClawEnabled
|
|
1660
|
-
: descriptor.id === "claude-code"
|
|
1661
|
-
? config.claudeCodeEnabled
|
|
1662
|
-
: false;
|
|
1663
|
-
return `${descriptor.label}: ${descriptor.status}${descriptor.status === "available" ? ` · ${enabled ? "enabled" : "disabled"}` : ""}`;
|
|
1664
|
-
}),
|
|
1665
|
-
].join("\n");
|
|
1666
|
-
const html = [
|
|
1667
|
-
"<b>Agent adapters:</b>",
|
|
1668
|
-
...descriptors.map((descriptor) => {
|
|
1669
|
-
const enabled = descriptor.id === "codex"
|
|
1670
|
-
? config.codexEnabled
|
|
1671
|
-
: descriptor.id === "pi"
|
|
1672
|
-
? config.piEnabled
|
|
1673
|
-
: descriptor.id === "hermes"
|
|
1674
|
-
? config.hermesEnabled
|
|
1675
|
-
: descriptor.id === "openclaw"
|
|
1676
|
-
? config.openClawEnabled
|
|
1677
|
-
: descriptor.id === "claude-code"
|
|
1678
|
-
? config.claudeCodeEnabled
|
|
1679
|
-
: false;
|
|
1680
|
-
const status = descriptor.status === "available" ? `${enabled ? "enabled" : "disabled"}` : "planned";
|
|
1681
|
-
const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
|
|
1682
|
-
return `${descriptor.status === "available" ? "✅" : "🟡"} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(status)}</code>${notes}`;
|
|
1683
|
-
}),
|
|
1684
|
-
].join("\n");
|
|
1685
|
-
await safeReply(ctx, html, { fallbackText: plain });
|
|
1685
|
+
const rendered = renderAgentsAction(listAgentAdapterDescriptors(), enabledAgents(config));
|
|
1686
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
1686
1687
|
});
|
|
1687
1688
|
bot.command("agent", async (ctx) => {
|
|
1688
1689
|
const contextSession = await getContextSession(ctx, { deferThreadStart: true });
|
|
@@ -2244,20 +2245,12 @@ export function createBot(config, registry) {
|
|
|
2244
2245
|
const rawText = ctx.message?.text ?? "";
|
|
2245
2246
|
const argument = rawText.replace(/^\/logs(?:@\w+)?\s*/i, "").trim();
|
|
2246
2247
|
const logRequest = parseLogsCommand(argument);
|
|
2247
|
-
const logs = logRequest.target
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
{
|
|
2254
|
-
title: logRequest.target === "update" ? "Update" : "Connector",
|
|
2255
|
-
tail: await readFormattedLogTail(logRequest.lines, logRequest.target === "update" ? getUpdateLogPath() : undefined),
|
|
2256
|
-
},
|
|
2257
|
-
];
|
|
2258
|
-
const plain = logs.map(({ title, tail }) => renderLogTailPlain(title, tail)).join("\n\n");
|
|
2259
|
-
const html = logs.map(({ title, tail }) => renderLogTailHTML(title, tail)).join("\n\n");
|
|
2260
|
-
await safeReply(ctx, html, { fallbackText: plain });
|
|
2248
|
+
const logs = await Promise.all(logTailRequests(logRequest.target).map(async (request) => ({
|
|
2249
|
+
title: request.title,
|
|
2250
|
+
tail: await readFormattedLogTail(logRequest.lines, request.path),
|
|
2251
|
+
})));
|
|
2252
|
+
const rendered = renderLogTailsAction(logs);
|
|
2253
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2261
2254
|
});
|
|
2262
2255
|
bot.command("restart", async (ctx) => {
|
|
2263
2256
|
await safeReply(ctx, escapeHTML("Restarting connector..."), {
|
|
@@ -2268,24 +2261,92 @@ export function createBot(config, registry) {
|
|
|
2268
2261
|
}, 300);
|
|
2269
2262
|
});
|
|
2270
2263
|
bot.command("update", async (ctx) => {
|
|
2264
|
+
const rawText = ctx.message?.text ?? "";
|
|
2265
|
+
const argument = rawText.replace(/^\/update(?:@\w+)?\s*/i, "").trim();
|
|
2266
|
+
const tokens = argument.split(/\s+/).filter(Boolean);
|
|
2267
|
+
const subcommand = tokens[0]?.toLowerCase();
|
|
2268
|
+
if (subcommand === "agents" || subcommand === "agent") {
|
|
2269
|
+
const rendered = renderAgentUpdatePickerAction(listAgentAdapterDescriptors());
|
|
2270
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain, replyMarkup: actionKeyboard(rendered.buttons) });
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
if (subcommand === "jobs" || subcommand === "status") {
|
|
2274
|
+
const rendered = renderAgentUpdateJobsAction(agentUpdates.list());
|
|
2275
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
if (subcommand === "log" && tokens[1]) {
|
|
2279
|
+
const rendered = renderAgentUpdateLogAction(agentUpdates.readLog(tokens[1]));
|
|
2280
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if (subcommand === "cancel" && tokens[1]) {
|
|
2284
|
+
const job = agentUpdates.cancel(tokens[1]);
|
|
2285
|
+
const rendered = renderAgentUpdateJobAction(job);
|
|
2286
|
+
await safeReply(ctx, rendered.html, {
|
|
2287
|
+
fallbackText: rendered.plain,
|
|
2288
|
+
replyMarkup: actionKeyboard(rendered.buttons),
|
|
2289
|
+
});
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
if ((subcommand === "input" || subcommand === "send") && tokens[1] && tokens.slice(2).join(" ").trim()) {
|
|
2293
|
+
const job = agentUpdates.sendInput(tokens[1], tokens.slice(2).join(" "));
|
|
2294
|
+
const rendered = renderAgentUpdateJobAction(job);
|
|
2295
|
+
await safeReply(ctx, rendered.html, {
|
|
2296
|
+
fallbackText: rendered.plain,
|
|
2297
|
+
replyMarkup: actionKeyboard(rendered.buttons),
|
|
2298
|
+
});
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
const requestedAgent = parseAgentUpdateId(subcommand);
|
|
2302
|
+
if (requestedAgent) {
|
|
2303
|
+
await startTelegramAgentUpdate(ctx, requestedAgent);
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
if (subcommand) {
|
|
2307
|
+
const usage = "Unknown update target. Use /update, /update agents, /update jobs, /update <agent>, /update log <id>, /update cancel <id>, or /update input <id> <text>.";
|
|
2308
|
+
await safeReply(ctx, escapeHTML(usage), { fallbackText: usage });
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2271
2311
|
const update = spawnSelfUpdate();
|
|
2272
|
-
const
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2312
|
+
const rendered = renderSelfUpdateStartedAction(update);
|
|
2313
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2314
|
+
});
|
|
2315
|
+
bot.callbackQuery("upd_jobs", async (ctx) => {
|
|
2316
|
+
await ctx.answerCallbackQuery();
|
|
2317
|
+
const rendered = renderAgentUpdateJobsAction(agentUpdates.list());
|
|
2318
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2319
|
+
});
|
|
2320
|
+
bot.callbackQuery(/^upd_agent:(codex|pi|hermes|openclaw|claude-code)$/, async (ctx) => {
|
|
2321
|
+
const agentId = ctx.match?.[1];
|
|
2322
|
+
if (!agentId) {
|
|
2323
|
+
await ctx.answerCallbackQuery();
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
await ctx.answerCallbackQuery({ text: `Starting ${agentLabel(agentId)} update...` });
|
|
2327
|
+
await startTelegramAgentUpdate(ctx, agentId);
|
|
2328
|
+
});
|
|
2329
|
+
bot.callbackQuery(/^upd_log:(.+)$/, async (ctx) => {
|
|
2330
|
+
const id = ctx.match?.[1];
|
|
2331
|
+
await ctx.answerCallbackQuery();
|
|
2332
|
+
if (!id) {
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
const rendered = renderAgentUpdateLogAction(agentUpdates.readLog(id));
|
|
2336
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2337
|
+
});
|
|
2338
|
+
bot.callbackQuery(/^upd_cancel:(.+)$/, async (ctx) => {
|
|
2339
|
+
const id = ctx.match?.[1];
|
|
2340
|
+
await ctx.answerCallbackQuery({ text: "Cancelling update..." });
|
|
2341
|
+
if (!id) {
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
const job = agentUpdates.cancel(id);
|
|
2345
|
+
const rendered = renderAgentUpdateJobAction(job);
|
|
2346
|
+
await safeReply(ctx, rendered.html, {
|
|
2347
|
+
fallbackText: rendered.plain,
|
|
2348
|
+
replyMarkup: actionKeyboard(rendered.buttons),
|
|
2349
|
+
});
|
|
2289
2350
|
});
|
|
2290
2351
|
bot.command("new", async (ctx) => {
|
|
2291
2352
|
const chatId = ctx.chat?.id;
|
|
@@ -2433,7 +2494,7 @@ export function createBot(config, registry) {
|
|
|
2433
2494
|
});
|
|
2434
2495
|
return;
|
|
2435
2496
|
}
|
|
2436
|
-
const rendered =
|
|
2497
|
+
const rendered = renderQueuedPromptDetailAction(item);
|
|
2437
2498
|
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2438
2499
|
return;
|
|
2439
2500
|
}
|
|
@@ -2591,7 +2652,7 @@ export function createBot(config, registry) {
|
|
|
2591
2652
|
});
|
|
2592
2653
|
return;
|
|
2593
2654
|
}
|
|
2594
|
-
const rendered =
|
|
2655
|
+
const rendered = renderArtifactReportsAction(filtered);
|
|
2595
2656
|
await safeReply(ctx, rendered.html, {
|
|
2596
2657
|
fallbackText: rendered.plain,
|
|
2597
2658
|
replyMarkup: buildArtifactActionsKeyboard(filtered),
|
|
@@ -2617,7 +2678,7 @@ export function createBot(config, registry) {
|
|
|
2617
2678
|
}
|
|
2618
2679
|
return;
|
|
2619
2680
|
}
|
|
2620
|
-
const { html, plain } =
|
|
2681
|
+
const { html, plain } = renderArtifactReportsAction(reports);
|
|
2621
2682
|
await safeReply(ctx, html, {
|
|
2622
2683
|
fallbackText: plain,
|
|
2623
2684
|
replyMarkup: buildArtifactActionsKeyboard(reports),
|
|
@@ -4065,23 +4126,12 @@ export async function registerCommands(bot) {
|
|
|
4065
4126
|
{ command: "unlock", description: "Release session write lock" },
|
|
4066
4127
|
{ command: "locks", description: "List session write locks" },
|
|
4067
4128
|
{ command: "restart", description: "Admin: restart connector" },
|
|
4068
|
-
{ command: "update", description: "Admin: update connector" },
|
|
4129
|
+
{ command: "update", description: "Admin: update connector or agents" },
|
|
4069
4130
|
{ command: "handback", description: "Hand session back to CLI" },
|
|
4070
4131
|
{ command: "attach", description: "Bind a session to this topic" },
|
|
4071
4132
|
{ command: "switch", description: "Switch to a thread by ID" },
|
|
4072
4133
|
]);
|
|
4073
4134
|
}
|
|
4074
|
-
function renderArtifactReports(reports) {
|
|
4075
|
-
const lines = reports.slice(0, 5).map((report, index) => {
|
|
4076
|
-
const size = formatFileSize(totalArtifactSize(report.artifacts));
|
|
4077
|
-
const skipped = report.skippedCount > 0 ? `, ${report.skippedCount} skipped` : "";
|
|
4078
|
-
return `${index + 1}. ${report.turnId} · ${formatRelativeTime(report.updatedAt)} · ${report.artifacts.length} file${report.artifacts.length === 1 ? "" : "s"} · ${size}${skipped}`;
|
|
4079
|
-
});
|
|
4080
|
-
const usage = "Tap an action below, or use /artifacts latest, /artifacts zip latest, /artifacts images, /artifacts docs, /artifacts search <text>, or /artifacts delete <turn-id>.";
|
|
4081
|
-
const plain = ["Recent artifacts:", ...lines, "", usage].join("\n");
|
|
4082
|
-
const html = ["<b>Recent artifacts:</b>", ...lines.map(escapeHTML), "", escapeHTML(usage)].join("\n");
|
|
4083
|
-
return { html, plain };
|
|
4084
|
-
}
|
|
4085
4135
|
function renderVersionCheckPlain(check) {
|
|
4086
4136
|
const icon = versionStatusIcon(check);
|
|
4087
4137
|
const label = check.label === "NordRelay" ? "NordRelay" : `${check.label} version`;
|
|
@@ -4127,73 +4177,6 @@ function formatVersionCheckDetailHTML(check) {
|
|
|
4127
4177
|
function versionStatusIcon(check) {
|
|
4128
4178
|
return check.status === "current" ? "✅" : "⚠️";
|
|
4129
4179
|
}
|
|
4130
|
-
function parseLogsCommand(argument) {
|
|
4131
|
-
const tokens = argument.split(/\s+/).filter(Boolean);
|
|
4132
|
-
let target = "connector";
|
|
4133
|
-
let lines = 80;
|
|
4134
|
-
for (const token of tokens) {
|
|
4135
|
-
const normalized = token.toLowerCase();
|
|
4136
|
-
if (normalized === "connector" || normalized === "main") {
|
|
4137
|
-
target = "connector";
|
|
4138
|
-
continue;
|
|
4139
|
-
}
|
|
4140
|
-
if (normalized === "update" || normalized === "updates") {
|
|
4141
|
-
target = "update";
|
|
4142
|
-
continue;
|
|
4143
|
-
}
|
|
4144
|
-
if (normalized === "all") {
|
|
4145
|
-
target = "all";
|
|
4146
|
-
continue;
|
|
4147
|
-
}
|
|
4148
|
-
const parsedLines = Number.parseInt(token, 10);
|
|
4149
|
-
if (!Number.isNaN(parsedLines)) {
|
|
4150
|
-
lines = parsedLines;
|
|
4151
|
-
}
|
|
4152
|
-
}
|
|
4153
|
-
return { target, lines };
|
|
4154
|
-
}
|
|
4155
|
-
function renderLogTailPlain(title, tail) {
|
|
4156
|
-
return [
|
|
4157
|
-
`${title} log tail`,
|
|
4158
|
-
`File: ${tail.filePath}`,
|
|
4159
|
-
`Updated: ${tail.updatedAt ? formatLogDate(tail.updatedAt) : "-"}`,
|
|
4160
|
-
`Lines: ${tail.lineCount}/${tail.requestedLines}`,
|
|
4161
|
-
"",
|
|
4162
|
-
tail.plain || "(empty)",
|
|
4163
|
-
].join("\n");
|
|
4164
|
-
}
|
|
4165
|
-
function renderLogTailHTML(title, tail) {
|
|
4166
|
-
const body = tail.plain
|
|
4167
|
-
? tail.plain.split("\n").map(renderLogLineHTML).join("\n")
|
|
4168
|
-
: "<code>(empty)</code>";
|
|
4169
|
-
return [
|
|
4170
|
-
`<b>${escapeHTML(title)} log tail</b>`,
|
|
4171
|
-
`<b>File:</b> <code>${escapeHTML(tail.filePath)}</code>`,
|
|
4172
|
-
`<b>Updated:</b> <code>${escapeHTML(tail.updatedAt ? formatLogDate(tail.updatedAt) : "-")}</code>`,
|
|
4173
|
-
`<b>Lines:</b> <code>${tail.lineCount}/${tail.requestedLines}</code>`,
|
|
4174
|
-
"",
|
|
4175
|
-
body,
|
|
4176
|
-
].join("\n");
|
|
4177
|
-
}
|
|
4178
|
-
function formatLogDate(date) {
|
|
4179
|
-
return [
|
|
4180
|
-
`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`,
|
|
4181
|
-
`${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`,
|
|
4182
|
-
].join(" ");
|
|
4183
|
-
}
|
|
4184
|
-
function renderLogLineHTML(line) {
|
|
4185
|
-
const structured = line.match(/^(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}|unknown time\s*)\s+(?<level>INFO|WARN|ERROR)\s+(?<message>.*)$/);
|
|
4186
|
-
if (structured?.groups) {
|
|
4187
|
-
const level = structured.groups.level;
|
|
4188
|
-
const levelHtml = level === "INFO" ? escapeHTML(level) : `<b>${escapeHTML(level)}</b>`;
|
|
4189
|
-
return [
|
|
4190
|
-
`<code>${escapeHTML(structured.groups.timestamp.trim())}</code>`,
|
|
4191
|
-
levelHtml,
|
|
4192
|
-
escapeHTML(structured.groups.message),
|
|
4193
|
-
].join(" ");
|
|
4194
|
-
}
|
|
4195
|
-
return escapeHTML(line);
|
|
4196
|
-
}
|
|
4197
4180
|
function renderAuditEvents(events) {
|
|
4198
4181
|
if (events.length === 0) {
|
|
4199
4182
|
return {
|
|
@@ -4233,29 +4216,6 @@ function renderSessionLocks(locks) {
|
|
|
4233
4216
|
html: ["<b>Session locks:</b>", ...lines.map((line) => escapeHTML(line))].join("\n"),
|
|
4234
4217
|
};
|
|
4235
4218
|
}
|
|
4236
|
-
function renderQueuedPromptDetail(item) {
|
|
4237
|
-
const lines = [
|
|
4238
|
-
"Queued prompt:",
|
|
4239
|
-
`ID: ${item.id}`,
|
|
4240
|
-
`Created: ${formatLocalDateTime(new Date(item.createdAt))}`,
|
|
4241
|
-
item.notBefore ? `Scheduled: ${formatLocalDateTime(new Date(item.notBefore))}` : undefined,
|
|
4242
|
-
`Attempts: ${item.attempts ?? 0}`,
|
|
4243
|
-
item.lastError ? `Last error: ${item.lastError}` : undefined,
|
|
4244
|
-
`Description: ${item.description}`,
|
|
4245
|
-
].filter((line) => Boolean(line));
|
|
4246
|
-
return {
|
|
4247
|
-
plain: lines.join("\n"),
|
|
4248
|
-
html: [
|
|
4249
|
-
"<b>Queued prompt:</b>",
|
|
4250
|
-
`<b>ID:</b> <code>${escapeHTML(item.id)}</code>`,
|
|
4251
|
-
`<b>Created:</b> <code>${escapeHTML(formatLocalDateTime(new Date(item.createdAt)))}</code>`,
|
|
4252
|
-
item.notBefore ? `<b>Scheduled:</b> <code>${escapeHTML(formatLocalDateTime(new Date(item.notBefore)))}</code>` : undefined,
|
|
4253
|
-
`<b>Attempts:</b> <code>${item.attempts ?? 0}</code>`,
|
|
4254
|
-
item.lastError ? `<b>Last error:</b> ${escapeHTML(item.lastError)}` : undefined,
|
|
4255
|
-
`<b>Description:</b> ${escapeHTML(item.description)}`,
|
|
4256
|
-
].filter((line) => Boolean(line)).join("\n"),
|
|
4257
|
-
};
|
|
4258
|
-
}
|
|
4259
4219
|
function formatLockOwner(lock) {
|
|
4260
4220
|
if (!lock) {
|
|
4261
4221
|
return "nobody";
|