@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/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, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate, } from "./operations.js";
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 { formatFileSize, renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
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 descriptors = listChannelDescriptors();
1633
- const lines = descriptors.map((descriptor) => {
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 descriptors = listAgentAdapterDescriptors();
1649
- const plain = [
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 === "all"
2248
- ? [
2249
- { title: "Connector", tail: await readFormattedLogTail(logRequest.lines) },
2250
- { title: "Update", tail: await readFormattedLogTail(logRequest.lines, getUpdateLogPath()) },
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 plain = [
2273
- "Update started.",
2274
- `Method: ${update.method}`,
2275
- update.summary,
2276
- `Source: ${update.sourceRoot}`,
2277
- `Log: ${update.logPath}`,
2278
- "Use /logs update after the restart or inspect update.log on the host.",
2279
- ].join("\n");
2280
- const html = [
2281
- "<b>Update started.</b>",
2282
- `<b>Method:</b> <code>${escapeHTML(update.method)}</code>`,
2283
- escapeHTML(update.summary),
2284
- `<b>Source:</b> <code>${escapeHTML(update.sourceRoot)}</code>`,
2285
- `<b>Log:</b> <code>${escapeHTML(update.logPath)}</code>`,
2286
- `Use <code>/logs update</code> after the restart or inspect <code>${escapeHTML(getUpdateLogPath())}</code> on the host.`,
2287
- ].join("\n");
2288
- await safeReply(ctx, html, { fallbackText: plain });
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 = renderQueuedPromptDetail(item);
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 = renderArtifactReports(filtered);
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 } = renderArtifactReports(reports);
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";