@nordbyte/nordrelay 0.2.1 → 0.3.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 +22 -0
- package/CHANGELOG.md +17 -0
- package/README.md +130 -8
- package/dist/access-control.js +6 -0
- package/dist/agent-adapter.js +60 -0
- package/dist/audit-log.js +54 -0
- package/dist/bot-preferences.js +13 -9
- package/dist/bot-ui.js +6 -0
- package/dist/bot.js +521 -24
- package/dist/channel-adapter.js +58 -0
- package/dist/config.js +47 -0
- package/dist/index.js +47 -2
- package/dist/logger.js +24 -1
- package/dist/operations.js +339 -14
- package/dist/prompt-store.js +33 -11
- package/dist/relay-runtime.js +479 -0
- package/dist/session-locks.js +81 -0
- package/dist/session-registry.js +10 -6
- package/dist/settings-service.js +230 -0
- package/dist/state-backend.js +74 -0
- package/dist/web-dashboard.js +764 -0
- package/package.json +4 -1
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +199 -2
package/dist/bot.js
CHANGED
|
@@ -7,8 +7,11 @@ import { Bot, InlineKeyboard, InputFile } from "grammy";
|
|
|
7
7
|
import { hasTelegramPermission, permissionForCallbackData, permissionForCommand, } from "./access-control.js";
|
|
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
|
+
import { listAgentAdapterDescriptors } from "./agent-adapter.js";
|
|
11
|
+
import { AuditLogStore } from "./audit-log.js";
|
|
10
12
|
import { formatSessionLabel, renderHelpMessage, renderWelcomeFirstTime, renderWelcomeReturning, } from "./bot-ui.js";
|
|
11
13
|
import { BotPreferencesStore, formatQuietHours, isQuietNow, parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
|
|
14
|
+
import { listChannelDescriptors } from "./channel-adapter.js";
|
|
12
15
|
import { CODEX_REASONING_EFFORTS, CODEX_AGENT_CAPABILITIES, PI_THINKING_LEVELS, agentLabel, agentReasoningLabel, } from "./agent.js";
|
|
13
16
|
import { enabledAgents } from "./agent-factory.js";
|
|
14
17
|
import { checkAuthStatus, clearAuthCache, startLogin, startLogout } from "./codex-auth.js";
|
|
@@ -17,9 +20,10 @@ import { getThreadActivity, getThreadActivityLog, getThreadRolloutSnapshot, } fr
|
|
|
17
20
|
import { contextKeyFromCtx, isTopicContextKey, parseContextKey } from "./context-key.js";
|
|
18
21
|
import { friendlyErrorText } from "./error-messages.js";
|
|
19
22
|
import { escapeHTML, formatTelegramHTML } from "./format.js";
|
|
20
|
-
import { getConnectorHealth, getUpdateLogPath, readConnectorState,
|
|
23
|
+
import { getConnectorHealth, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate, } from "./operations.js";
|
|
21
24
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
22
25
|
import { configureRedaction, redactText } from "./redaction.js";
|
|
26
|
+
import { canWriteWithLock, SessionLockStore } from "./session-locks.js";
|
|
23
27
|
import { formatFileSize, renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
|
|
24
28
|
import { SessionRegistry } from "./session-registry.js";
|
|
25
29
|
import { getAvailableBackends, transcribeAudio } from "./voice.js";
|
|
@@ -82,8 +86,10 @@ export function createBot(config, registry) {
|
|
|
82
86
|
const pendingAgentPicks = new Map();
|
|
83
87
|
const pendingMediaGroups = new Map();
|
|
84
88
|
const turnProgress = new Map();
|
|
85
|
-
const promptStore = new PromptStore(config.workspace);
|
|
86
|
-
const preferencesStore = new BotPreferencesStore(config.workspace);
|
|
89
|
+
const promptStore = new PromptStore(config.workspace, config.stateBackend);
|
|
90
|
+
const preferencesStore = new BotPreferencesStore(config.workspace, config.stateBackend);
|
|
91
|
+
const auditLog = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
92
|
+
const lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
87
93
|
const drainingQueues = new Set();
|
|
88
94
|
const externalQueueTimers = new Map();
|
|
89
95
|
const externalMirrors = new Map();
|
|
@@ -259,7 +265,10 @@ export function createBot(config, registry) {
|
|
|
259
265
|
const age = formatRelativeTime(new Date(item.createdAt));
|
|
260
266
|
const attempts = item.attempts && item.attempts > 0 ? ` · attempts ${item.attempts}` : "";
|
|
261
267
|
const error = item.lastError ? ` · last error: ${trimLine(item.lastError, 80)}` : "";
|
|
262
|
-
const
|
|
268
|
+
const scheduled = item.notBefore && item.notBefore > Date.now()
|
|
269
|
+
? `scheduled ${formatLocalDateTime(new Date(item.notBefore))}`
|
|
270
|
+
: index === 0 ? "next" : `after ${index} queued item${index === 1 ? "" : "s"}`;
|
|
271
|
+
const eta = scheduled;
|
|
263
272
|
return `${index + 1}. ${item.id} · ${age} · ${eta}${attempts}${error} · ${item.description}`;
|
|
264
273
|
});
|
|
265
274
|
const keyboard = new InlineKeyboard();
|
|
@@ -579,6 +588,42 @@ export function createBot(config, registry) {
|
|
|
579
588
|
}
|
|
580
589
|
return permissionForCommand(command);
|
|
581
590
|
};
|
|
591
|
+
const audit = (event) => {
|
|
592
|
+
try {
|
|
593
|
+
auditLog.append(event);
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
console.warn("Failed to write audit event:", error instanceof Error ? error.message : String(error));
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
const auditContext = (ctx, contextKey, session, patch) => {
|
|
600
|
+
const info = session.getInfo();
|
|
601
|
+
audit({
|
|
602
|
+
contextKey,
|
|
603
|
+
actorId: ctx.from?.id,
|
|
604
|
+
actorRole: getUserRole(ctx),
|
|
605
|
+
agentId: idOf(info),
|
|
606
|
+
threadId: info.threadId,
|
|
607
|
+
workspace: info.workspace,
|
|
608
|
+
...patch,
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
const denyIfLocked = async (ctx, contextKey, session) => {
|
|
612
|
+
const lock = lockStore.get(contextKey);
|
|
613
|
+
const isAdmin = getUserRole(ctx) === "admin";
|
|
614
|
+
if (canWriteWithLock(lock, ctx.from?.id, isAdmin)) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
const owner = formatLockOwner(lock);
|
|
618
|
+
const text = `Session is locked by ${owner}. Use /locks to inspect or ask an admin to /unlock.`;
|
|
619
|
+
auditContext(ctx, contextKey, session, {
|
|
620
|
+
action: "prompt_started",
|
|
621
|
+
status: "denied",
|
|
622
|
+
detail: text,
|
|
623
|
+
});
|
|
624
|
+
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
625
|
+
return true;
|
|
626
|
+
};
|
|
582
627
|
const setReaction = async (ctx, emoji) => {
|
|
583
628
|
if (!config.enableTelegramReactions) {
|
|
584
629
|
return;
|
|
@@ -669,6 +714,9 @@ export function createBot(config, registry) {
|
|
|
669
714
|
const parsed = parseContextKey(contextKey);
|
|
670
715
|
const messageThreadId = parsed.messageThreadId;
|
|
671
716
|
const envelope = isPromptEnvelopeLike(prompt) ? prompt : toPromptEnvelope(prompt);
|
|
717
|
+
if (!options.fromQueue && await denyIfLocked(ctx, contextKey, session)) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
672
720
|
const busy = getBusyReason(contextKey);
|
|
673
721
|
if (busy.busy) {
|
|
674
722
|
if (options.fromQueue) {
|
|
@@ -689,6 +737,13 @@ export function createBot(config, registry) {
|
|
|
689
737
|
fallbackText: queuedMessage,
|
|
690
738
|
replyMarkup: createQueuedPromptCancelKeyboard(contextKey, item.id),
|
|
691
739
|
});
|
|
740
|
+
auditContext(ctx, contextKey, session, {
|
|
741
|
+
action: "prompt_queued",
|
|
742
|
+
status: "ok",
|
|
743
|
+
promptId: item.id,
|
|
744
|
+
description: item.description,
|
|
745
|
+
detail: busy.kind,
|
|
746
|
+
});
|
|
692
747
|
if (busy.kind === "external") {
|
|
693
748
|
scheduleExternalQueueDrain(ctx, contextKey, chatId, session);
|
|
694
749
|
}
|
|
@@ -1101,6 +1156,11 @@ export function createBot(config, registry) {
|
|
|
1101
1156
|
return;
|
|
1102
1157
|
}
|
|
1103
1158
|
promptStore.setLastPrompt(contextKey, envelope);
|
|
1159
|
+
auditContext(ctx, contextKey, session, {
|
|
1160
|
+
action: "prompt_started",
|
|
1161
|
+
status: "ok",
|
|
1162
|
+
description: envelope.description,
|
|
1163
|
+
});
|
|
1104
1164
|
await session.prompt(envelope.input, callbacks);
|
|
1105
1165
|
updateSessionMetadata(contextKey, session);
|
|
1106
1166
|
await finalizeResponse();
|
|
@@ -1115,10 +1175,21 @@ export function createBot(config, registry) {
|
|
|
1115
1175
|
progress.status = "completed";
|
|
1116
1176
|
progress.completedAt = Date.now();
|
|
1117
1177
|
progress.updatedAt = progress.completedAt;
|
|
1178
|
+
auditContext(ctx, contextKey, session, {
|
|
1179
|
+
action: "prompt_completed",
|
|
1180
|
+
status: "ok",
|
|
1181
|
+
description: envelope.description,
|
|
1182
|
+
});
|
|
1118
1183
|
}
|
|
1119
1184
|
catch (error) {
|
|
1120
1185
|
progress.status = "failed";
|
|
1121
1186
|
progress.error = friendlyErrorText(error);
|
|
1187
|
+
auditContext(ctx, contextKey, session, {
|
|
1188
|
+
action: "prompt_failed",
|
|
1189
|
+
status: "failed",
|
|
1190
|
+
description: envelope.description,
|
|
1191
|
+
detail: progress.error,
|
|
1192
|
+
});
|
|
1122
1193
|
progress.completedAt = Date.now();
|
|
1123
1194
|
progress.updatedAt = progress.completedAt;
|
|
1124
1195
|
stopTyping();
|
|
@@ -1175,6 +1246,11 @@ export function createBot(config, registry) {
|
|
|
1175
1246
|
}
|
|
1176
1247
|
const next = promptStore.dequeue(contextKey);
|
|
1177
1248
|
if (!next) {
|
|
1249
|
+
const nextRunnableAt = promptStore.nextRunnableAt(contextKey);
|
|
1250
|
+
const queued = promptStore.list(contextKey).length;
|
|
1251
|
+
if (nextRunnableAt && queued > 0) {
|
|
1252
|
+
await updateQueueStatusMessage(contextKey, `Next queued prompt is scheduled for ${formatLocalDateTime(new Date(nextRunnableAt))}. ${queued} queued.`);
|
|
1253
|
+
}
|
|
1178
1254
|
return;
|
|
1179
1255
|
}
|
|
1180
1256
|
const remainingBeforeRun = promptStore.list(contextKey).length + 1;
|
|
@@ -1457,6 +1533,50 @@ export function createBot(config, registry) {
|
|
|
1457
1533
|
const help = renderHelpMessage();
|
|
1458
1534
|
await safeReply(ctx, help.html, { fallbackText: help.plain });
|
|
1459
1535
|
});
|
|
1536
|
+
bot.command("channels", async (ctx) => {
|
|
1537
|
+
const descriptors = listChannelDescriptors();
|
|
1538
|
+
const lines = descriptors.map((descriptor) => {
|
|
1539
|
+
const status = descriptor.status === "available" ? "available" : "planned";
|
|
1540
|
+
return `${descriptor.label}: ${status} · ${descriptor.capabilities.join(", ")}`;
|
|
1541
|
+
});
|
|
1542
|
+
const html = [
|
|
1543
|
+
"<b>Channel adapters:</b>",
|
|
1544
|
+
...descriptors.map((descriptor) => {
|
|
1545
|
+
const statusIcon = descriptor.status === "available" ? "✅" : "🟡";
|
|
1546
|
+
const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
|
|
1547
|
+
return `${statusIcon} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(descriptor.status)}</code>\n <code>${escapeHTML(descriptor.capabilities.join(", "))}</code>${notes}`;
|
|
1548
|
+
}),
|
|
1549
|
+
].join("\n");
|
|
1550
|
+
await safeReply(ctx, html, { fallbackText: ["Channel adapters:", ...lines].join("\n") });
|
|
1551
|
+
});
|
|
1552
|
+
bot.command("agents", async (ctx) => {
|
|
1553
|
+
const descriptors = listAgentAdapterDescriptors();
|
|
1554
|
+
const plain = [
|
|
1555
|
+
"Agent adapters:",
|
|
1556
|
+
...descriptors.map((descriptor) => {
|
|
1557
|
+
const enabled = descriptor.id === "codex"
|
|
1558
|
+
? config.codexEnabled
|
|
1559
|
+
: descriptor.id === "pi"
|
|
1560
|
+
? config.piEnabled
|
|
1561
|
+
: false;
|
|
1562
|
+
return `${descriptor.label}: ${descriptor.status}${descriptor.status === "available" ? ` · ${enabled ? "enabled" : "disabled"}` : ""}`;
|
|
1563
|
+
}),
|
|
1564
|
+
].join("\n");
|
|
1565
|
+
const html = [
|
|
1566
|
+
"<b>Agent adapters:</b>",
|
|
1567
|
+
...descriptors.map((descriptor) => {
|
|
1568
|
+
const enabled = descriptor.id === "codex"
|
|
1569
|
+
? config.codexEnabled
|
|
1570
|
+
: descriptor.id === "pi"
|
|
1571
|
+
? config.piEnabled
|
|
1572
|
+
: false;
|
|
1573
|
+
const status = descriptor.status === "available" ? `${enabled ? "enabled" : "disabled"}` : "planned";
|
|
1574
|
+
const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
|
|
1575
|
+
return `${descriptor.status === "available" ? "✅" : "🟡"} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(status)}</code>${notes}`;
|
|
1576
|
+
}),
|
|
1577
|
+
].join("\n");
|
|
1578
|
+
await safeReply(ctx, html, { fallbackText: plain });
|
|
1579
|
+
});
|
|
1460
1580
|
bot.command("agent", async (ctx) => {
|
|
1461
1581
|
const contextSession = await getContextSession(ctx, { deferThreadStart: true });
|
|
1462
1582
|
if (!contextSession) {
|
|
@@ -1817,17 +1937,22 @@ export function createBot(config, registry) {
|
|
|
1817
1937
|
bot.command("version", async (ctx) => {
|
|
1818
1938
|
const health = await getConnectorHealth();
|
|
1819
1939
|
const state = await readConnectorState();
|
|
1940
|
+
const versions = await getVersionChecks({ piCliPath: config.piCliPath });
|
|
1820
1941
|
const plain = [
|
|
1821
|
-
|
|
1942
|
+
renderVersionCheckPlain(versions.nordrelay),
|
|
1822
1943
|
`Runtime status: ${state.status ?? "unknown"}`,
|
|
1823
|
-
|
|
1824
|
-
|
|
1944
|
+
formatCliPathPlain("Codex CLI", health.codexCliPath, health.codexCli),
|
|
1945
|
+
renderVersionCheckPlain(versions.codex),
|
|
1946
|
+
formatCliPathPlain("Pi CLI", health.piCliPath, health.piCli),
|
|
1947
|
+
renderVersionCheckPlain(versions.pi),
|
|
1825
1948
|
].join("\n");
|
|
1826
1949
|
const html = [
|
|
1827
|
-
|
|
1950
|
+
renderVersionCheckHTML(versions.nordrelay),
|
|
1828
1951
|
`<b>Runtime status:</b> <code>${escapeHTML(state.status ?? "unknown")}</code>`,
|
|
1829
|
-
|
|
1830
|
-
|
|
1952
|
+
formatCliPathHTML("Codex CLI", health.codexCliPath, health.codexCli),
|
|
1953
|
+
renderVersionCheckHTML(versions.codex),
|
|
1954
|
+
formatCliPathHTML("Pi CLI", health.piCliPath, health.piCli),
|
|
1955
|
+
renderVersionCheckHTML(versions.pi),
|
|
1831
1956
|
].join("\n");
|
|
1832
1957
|
await safeReply(ctx, html, { fallbackText: plain });
|
|
1833
1958
|
});
|
|
@@ -1882,6 +2007,61 @@ export function createBot(config, registry) {
|
|
|
1882
2007
|
}
|
|
1883
2008
|
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
1884
2009
|
});
|
|
2010
|
+
bot.command("audit", async (ctx) => {
|
|
2011
|
+
const rawText = ctx.message?.text ?? "";
|
|
2012
|
+
const limitArg = rawText.replace(/^\/audit(?:@\w+)?\s*/i, "").trim();
|
|
2013
|
+
const limit = /^\d+$/.test(limitArg) ? Number(limitArg) : 20;
|
|
2014
|
+
const events = auditLog.list(limit);
|
|
2015
|
+
const rendered = renderAuditEvents(events);
|
|
2016
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2017
|
+
});
|
|
2018
|
+
bot.command("lock", async (ctx) => {
|
|
2019
|
+
const contextSession = await getContextSession(ctx, { deferThreadStart: true });
|
|
2020
|
+
if (!contextSession || !ctx.from) {
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
const { contextKey, session } = contextSession;
|
|
2024
|
+
const existing = lockStore.get(contextKey);
|
|
2025
|
+
if (existing && existing.ownerId !== ctx.from.id && getUserRole(ctx) !== "admin") {
|
|
2026
|
+
const text = `Session is already locked by ${formatLockOwner(existing)}.`;
|
|
2027
|
+
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
const lock = lockStore.set(contextKey, ctx.from.id, formatTelegramName(ctx), config.sessionLockTtlMs);
|
|
2031
|
+
auditContext(ctx, contextKey, session, {
|
|
2032
|
+
action: "lock_updated",
|
|
2033
|
+
status: "ok",
|
|
2034
|
+
detail: `locked by ${lock.ownerId}`,
|
|
2035
|
+
});
|
|
2036
|
+
const text = `Session locked by ${formatLockOwner(lock)}${lock.expiresAt ? ` until ${formatLocalDateTime(new Date(lock.expiresAt))}` : ""}.`;
|
|
2037
|
+
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
2038
|
+
});
|
|
2039
|
+
bot.command("unlock", async (ctx) => {
|
|
2040
|
+
const contextSession = await getContextSession(ctx, { deferThreadStart: true });
|
|
2041
|
+
if (!contextSession) {
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
const { contextKey, session } = contextSession;
|
|
2045
|
+
const lock = lockStore.get(contextKey);
|
|
2046
|
+
if (lock && lock.ownerId !== ctx.from?.id && getUserRole(ctx) !== "admin") {
|
|
2047
|
+
const text = `Only ${formatLockOwner(lock)} or an admin can unlock this session.`;
|
|
2048
|
+
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const removed = lockStore.clear(contextKey);
|
|
2052
|
+
auditContext(ctx, contextKey, session, {
|
|
2053
|
+
action: "lock_updated",
|
|
2054
|
+
status: "ok",
|
|
2055
|
+
detail: removed ? "unlocked" : "no lock",
|
|
2056
|
+
});
|
|
2057
|
+
const text = removed ? "Session lock released." : "No active lock for this session.";
|
|
2058
|
+
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
2059
|
+
});
|
|
2060
|
+
bot.command("locks", async (ctx) => {
|
|
2061
|
+
const locks = lockStore.list();
|
|
2062
|
+
const rendered = renderSessionLocks(locks);
|
|
2063
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2064
|
+
});
|
|
1885
2065
|
bot.command("diagnostics", async (ctx) => {
|
|
1886
2066
|
const health = await getConnectorHealth();
|
|
1887
2067
|
const authStatus = await checkAuthStatus(config.codexApiKey);
|
|
@@ -1944,10 +2124,20 @@ export function createBot(config, registry) {
|
|
|
1944
2124
|
bot.command("logs", async (ctx) => {
|
|
1945
2125
|
const rawText = ctx.message?.text ?? "";
|
|
1946
2126
|
const argument = rawText.replace(/^\/logs(?:@\w+)?\s*/i, "").trim();
|
|
1947
|
-
const
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
2127
|
+
const logRequest = parseLogsCommand(argument);
|
|
2128
|
+
const logs = logRequest.target === "all"
|
|
2129
|
+
? [
|
|
2130
|
+
{ title: "Connector", tail: await readFormattedLogTail(logRequest.lines) },
|
|
2131
|
+
{ title: "Update", tail: await readFormattedLogTail(logRequest.lines, getUpdateLogPath()) },
|
|
2132
|
+
]
|
|
2133
|
+
: [
|
|
2134
|
+
{
|
|
2135
|
+
title: logRequest.target === "update" ? "Update" : "Connector",
|
|
2136
|
+
tail: await readFormattedLogTail(logRequest.lines, logRequest.target === "update" ? getUpdateLogPath() : undefined),
|
|
2137
|
+
},
|
|
2138
|
+
];
|
|
2139
|
+
const plain = logs.map(({ title, tail }) => renderLogTailPlain(title, tail)).join("\n\n");
|
|
2140
|
+
const html = logs.map(({ title, tail }) => renderLogTailHTML(title, tail)).join("\n\n");
|
|
1951
2141
|
await safeReply(ctx, html, { fallbackText: plain });
|
|
1952
2142
|
});
|
|
1953
2143
|
bot.command("restart", async (ctx) => {
|
|
@@ -1959,18 +2149,22 @@ export function createBot(config, registry) {
|
|
|
1959
2149
|
}, 300);
|
|
1960
2150
|
});
|
|
1961
2151
|
bot.command("update", async (ctx) => {
|
|
1962
|
-
const
|
|
2152
|
+
const update = spawnSelfUpdate();
|
|
1963
2153
|
const plain = [
|
|
1964
2154
|
"Update started.",
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2155
|
+
`Method: ${update.method}`,
|
|
2156
|
+
update.summary,
|
|
2157
|
+
`Source: ${update.sourceRoot}`,
|
|
2158
|
+
`Log: ${update.logPath}`,
|
|
2159
|
+
"Use /logs update after the restart or inspect update.log on the host.",
|
|
1968
2160
|
].join("\n");
|
|
1969
2161
|
const html = [
|
|
1970
2162
|
"<b>Update started.</b>",
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2163
|
+
`<b>Method:</b> <code>${escapeHTML(update.method)}</code>`,
|
|
2164
|
+
escapeHTML(update.summary),
|
|
2165
|
+
`<b>Source:</b> <code>${escapeHTML(update.sourceRoot)}</code>`,
|
|
2166
|
+
`<b>Log:</b> <code>${escapeHTML(update.logPath)}</code>`,
|
|
2167
|
+
`Use <code>/logs update</code> after the restart or inspect <code>${escapeHTML(getUpdateLogPath())}</code> on the host.`,
|
|
1974
2168
|
].join("\n");
|
|
1975
2169
|
await safeReply(ctx, html, { fallbackText: plain });
|
|
1976
2170
|
});
|
|
@@ -2085,6 +2279,39 @@ export function createBot(config, registry) {
|
|
|
2085
2279
|
const { contextKey, session } = contextSession;
|
|
2086
2280
|
const rawText = ctx.message?.text ?? "";
|
|
2087
2281
|
const argument = rawText.replace(/^\/queue(?:@\w+)?\s*/i, "").trim();
|
|
2282
|
+
const laterMatch = argument.match(/^later\s+(\d+)(?:m|min|minutes?)?\s+([\s\S]+)$/i);
|
|
2283
|
+
if (laterMatch) {
|
|
2284
|
+
const minutes = Math.min(7 * 24 * 60, Math.max(1, Number(laterMatch[1])));
|
|
2285
|
+
const text = laterMatch[2].trim();
|
|
2286
|
+
const notBefore = Date.now() + minutes * 60 * 1000;
|
|
2287
|
+
const item = promptStore.enqueue(contextKey, toPromptEnvelope(text), { notBefore });
|
|
2288
|
+
const message = `Queued prompt ${item.id} for ${formatLocalDateTime(new Date(notBefore))}.`;
|
|
2289
|
+
await safeReply(ctx, escapeHTML(message), {
|
|
2290
|
+
fallbackText: message,
|
|
2291
|
+
replyMarkup: createQueuedPromptCancelKeyboard(contextKey, item.id),
|
|
2292
|
+
});
|
|
2293
|
+
auditContext(ctx, contextKey, session, {
|
|
2294
|
+
action: "prompt_queued",
|
|
2295
|
+
status: "ok",
|
|
2296
|
+
promptId: item.id,
|
|
2297
|
+
description: item.description,
|
|
2298
|
+
detail: "scheduled",
|
|
2299
|
+
});
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const inspectMatch = argument.match(/^inspect\s+([a-z0-9]+)$/i);
|
|
2303
|
+
if (inspectMatch) {
|
|
2304
|
+
const item = promptStore.get(contextKey, inspectMatch[1]);
|
|
2305
|
+
if (!item) {
|
|
2306
|
+
await safeReply(ctx, escapeHTML(`No queued prompt found with id ${inspectMatch[1]}.`), {
|
|
2307
|
+
fallbackText: `No queued prompt found with id ${inspectMatch[1]}.`,
|
|
2308
|
+
});
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
const rendered = renderQueuedPromptDetail(item);
|
|
2312
|
+
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2088
2315
|
if (/^pause$/i.test(argument)) {
|
|
2089
2316
|
promptStore.pause(contextKey);
|
|
2090
2317
|
const message = `Queue paused. ${promptStore.list(contextKey).length} queued.`;
|
|
@@ -2151,8 +2378,8 @@ export function createBot(config, registry) {
|
|
|
2151
2378
|
return;
|
|
2152
2379
|
}
|
|
2153
2380
|
if (argument) {
|
|
2154
|
-
await safeReply(ctx, escapeHTML("Usage: /queue, /queue pause, /queue resume, /queue move <id> top|up|down, /queue run <id>"), {
|
|
2155
|
-
fallbackText: "Usage: /queue, /queue pause, /queue resume, /queue move <id> top|up|down, /queue run <id>",
|
|
2381
|
+
await safeReply(ctx, escapeHTML("Usage: /queue, /queue pause, /queue resume, /queue later <minutes> <prompt>, /queue inspect <id>, /queue move <id> top|up|down, /queue run <id>"), {
|
|
2382
|
+
fallbackText: "Usage: /queue, /queue pause, /queue resume, /queue later <minutes> <prompt>, /queue inspect <id>, /queue move <id> top|up|down, /queue run <id>",
|
|
2156
2383
|
});
|
|
2157
2384
|
return;
|
|
2158
2385
|
}
|
|
@@ -2218,6 +2445,34 @@ export function createBot(config, registry) {
|
|
|
2218
2445
|
}
|
|
2219
2446
|
if (argument) {
|
|
2220
2447
|
const parts = argument.split(/\s+/).filter(Boolean);
|
|
2448
|
+
if (parts[0]?.toLowerCase() === "delete" && parts[1]) {
|
|
2449
|
+
const selected = reports.find((report) => report.turnId === parts[1] || report.turnId.startsWith(parts[1]));
|
|
2450
|
+
if (!selected) {
|
|
2451
|
+
await safeReply(ctx, escapeHTML(`No artifact turn found for "${parts[1]}".`), {
|
|
2452
|
+
fallbackText: `No artifact turn found for "${parts[1]}".`,
|
|
2453
|
+
});
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
const removed = await removeArtifactTurn(workspace, selected.turnId);
|
|
2457
|
+
const text = removed ? `Deleted artifact turn: ${selected.turnId}` : `Artifact turn not found: ${selected.turnId}`;
|
|
2458
|
+
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
const filtered = filterArtifactReports(reports, argument);
|
|
2462
|
+
if (filtered) {
|
|
2463
|
+
if (filtered.length === 0) {
|
|
2464
|
+
await safeReply(ctx, escapeHTML(`No artifacts matched "${argument}".`), {
|
|
2465
|
+
fallbackText: `No artifacts matched "${argument}".`,
|
|
2466
|
+
});
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
const rendered = renderArtifactReports(filtered);
|
|
2470
|
+
await safeReply(ctx, rendered.html, {
|
|
2471
|
+
fallbackText: rendered.plain,
|
|
2472
|
+
replyMarkup: buildArtifactActionsKeyboard(filtered),
|
|
2473
|
+
});
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2221
2476
|
const shouldZip = parts[0]?.toLowerCase() === "zip";
|
|
2222
2477
|
const requestedTurn = shouldZip ? parts[1] : parts[0];
|
|
2223
2478
|
const selected = !requestedTurn || requestedTurn.toLowerCase() === "latest"
|
|
@@ -3641,6 +3896,8 @@ export async function registerCommands(bot) {
|
|
|
3641
3896
|
await bot.api.setMyCommands([
|
|
3642
3897
|
{ command: "start", description: "Welcome & status" },
|
|
3643
3898
|
{ command: "help", description: "Command reference" },
|
|
3899
|
+
{ command: "channels", description: "Messaging adapter status" },
|
|
3900
|
+
{ command: "agents", description: "Agent adapter status" },
|
|
3644
3901
|
{ command: "agent", description: "Select Codex or Pi" },
|
|
3645
3902
|
{ command: "new", description: "Start a new thread" },
|
|
3646
3903
|
{ command: "session", description: "Current thread details" },
|
|
@@ -3670,11 +3927,15 @@ export async function registerCommands(bot) {
|
|
|
3670
3927
|
{ command: "tasks", description: "Current turn progress" },
|
|
3671
3928
|
{ command: "progress", description: "Current turn progress" },
|
|
3672
3929
|
{ command: "activity", description: "Thread activity timeline" },
|
|
3930
|
+
{ command: "audit", description: "Admin: recent audit events" },
|
|
3673
3931
|
{ command: "status", description: "Connector runtime status" },
|
|
3674
3932
|
{ command: "health", description: "Connector health report" },
|
|
3675
3933
|
{ command: "version", description: "Connector version" },
|
|
3676
3934
|
{ command: "logs", description: "Admin: show connector logs" },
|
|
3677
3935
|
{ command: "diagnostics", description: "Admin: connector diagnostics" },
|
|
3936
|
+
{ command: "lock", description: "Lock session writes to you" },
|
|
3937
|
+
{ command: "unlock", description: "Release session write lock" },
|
|
3938
|
+
{ command: "locks", description: "List session write locks" },
|
|
3678
3939
|
{ command: "restart", description: "Admin: restart connector" },
|
|
3679
3940
|
{ command: "update", description: "Admin: update connector" },
|
|
3680
3941
|
{ command: "handback", description: "Hand session back to CLI" },
|
|
@@ -3688,11 +3949,210 @@ function renderArtifactReports(reports) {
|
|
|
3688
3949
|
const skipped = report.skippedCount > 0 ? `, ${report.skippedCount} skipped` : "";
|
|
3689
3950
|
return `${index + 1}. ${report.turnId} · ${formatRelativeTime(report.updatedAt)} · ${report.artifacts.length} file${report.artifacts.length === 1 ? "" : "s"} · ${size}${skipped}`;
|
|
3690
3951
|
});
|
|
3691
|
-
const usage = "Tap an action below, or use /artifacts latest, /artifacts zip latest, or /artifacts <turn-id>.";
|
|
3952
|
+
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>.";
|
|
3692
3953
|
const plain = ["Recent artifacts:", ...lines, "", usage].join("\n");
|
|
3693
3954
|
const html = ["<b>Recent artifacts:</b>", ...lines.map(escapeHTML), "", escapeHTML(usage)].join("\n");
|
|
3694
3955
|
return { html, plain };
|
|
3695
3956
|
}
|
|
3957
|
+
function renderVersionCheckPlain(check) {
|
|
3958
|
+
const icon = versionStatusIcon(check);
|
|
3959
|
+
const label = check.label === "NordRelay" ? "NordRelay" : `${check.label} version`;
|
|
3960
|
+
return `${label}: ${icon} ${formatVersionCheckDetailPlain(check)}`;
|
|
3961
|
+
}
|
|
3962
|
+
function renderVersionCheckHTML(check) {
|
|
3963
|
+
const icon = versionStatusIcon(check);
|
|
3964
|
+
const label = check.label === "NordRelay" ? "NordRelay" : `${check.label} version`;
|
|
3965
|
+
return `<b>${escapeHTML(label)}:</b> ${icon} ${formatVersionCheckDetailHTML(check)}`;
|
|
3966
|
+
}
|
|
3967
|
+
function formatCliPathPlain(label, cliPath, fallback) {
|
|
3968
|
+
return cliPath ? `${label} path: ${cliPath}` : `${label}: ${fallback}`;
|
|
3969
|
+
}
|
|
3970
|
+
function formatCliPathHTML(label, cliPath, fallback) {
|
|
3971
|
+
return cliPath
|
|
3972
|
+
? `<b>${escapeHTML(label)} path:</b> <code>${escapeHTML(cliPath)}</code>`
|
|
3973
|
+
: `<b>${escapeHTML(label)}:</b> <code>${escapeHTML(fallback)}</code>`;
|
|
3974
|
+
}
|
|
3975
|
+
function formatVersionCheckDetailPlain(check) {
|
|
3976
|
+
if (check.status === "not-installed") {
|
|
3977
|
+
return "not installed";
|
|
3978
|
+
}
|
|
3979
|
+
if (check.status === "outdated") {
|
|
3980
|
+
return `${check.installedLabel} (latest ${check.latestVersion ?? "unknown"})`;
|
|
3981
|
+
}
|
|
3982
|
+
if (check.status === "current") {
|
|
3983
|
+
return `${check.installedLabel} (latest)`;
|
|
3984
|
+
}
|
|
3985
|
+
return `${check.installedLabel} (latest unknown${check.detail ? `: ${check.detail}` : ""})`;
|
|
3986
|
+
}
|
|
3987
|
+
function formatVersionCheckDetailHTML(check) {
|
|
3988
|
+
if (check.status === "not-installed") {
|
|
3989
|
+
return "<code>not installed</code>";
|
|
3990
|
+
}
|
|
3991
|
+
if (check.status === "outdated") {
|
|
3992
|
+
return `<code>${escapeHTML(check.installedLabel)}</code> <i>(latest ${escapeHTML(check.latestVersion ?? "unknown")})</i>`;
|
|
3993
|
+
}
|
|
3994
|
+
if (check.status === "current") {
|
|
3995
|
+
return `<code>${escapeHTML(check.installedLabel)}</code> <i>(latest)</i>`;
|
|
3996
|
+
}
|
|
3997
|
+
return `<code>${escapeHTML(check.installedLabel)}</code> <i>(latest unknown${check.detail ? `: ${escapeHTML(check.detail)}` : ""})</i>`;
|
|
3998
|
+
}
|
|
3999
|
+
function versionStatusIcon(check) {
|
|
4000
|
+
return check.status === "current" ? "✅" : "⚠️";
|
|
4001
|
+
}
|
|
4002
|
+
function parseLogsCommand(argument) {
|
|
4003
|
+
const tokens = argument.split(/\s+/).filter(Boolean);
|
|
4004
|
+
let target = "connector";
|
|
4005
|
+
let lines = 80;
|
|
4006
|
+
for (const token of tokens) {
|
|
4007
|
+
const normalized = token.toLowerCase();
|
|
4008
|
+
if (normalized === "connector" || normalized === "main") {
|
|
4009
|
+
target = "connector";
|
|
4010
|
+
continue;
|
|
4011
|
+
}
|
|
4012
|
+
if (normalized === "update" || normalized === "updates") {
|
|
4013
|
+
target = "update";
|
|
4014
|
+
continue;
|
|
4015
|
+
}
|
|
4016
|
+
if (normalized === "all") {
|
|
4017
|
+
target = "all";
|
|
4018
|
+
continue;
|
|
4019
|
+
}
|
|
4020
|
+
const parsedLines = Number.parseInt(token, 10);
|
|
4021
|
+
if (!Number.isNaN(parsedLines)) {
|
|
4022
|
+
lines = parsedLines;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
return { target, lines };
|
|
4026
|
+
}
|
|
4027
|
+
function renderLogTailPlain(title, tail) {
|
|
4028
|
+
return [
|
|
4029
|
+
`${title} log tail`,
|
|
4030
|
+
`File: ${tail.filePath}`,
|
|
4031
|
+
`Updated: ${tail.updatedAt ? formatLogDate(tail.updatedAt) : "-"}`,
|
|
4032
|
+
`Lines: ${tail.lineCount}/${tail.requestedLines}`,
|
|
4033
|
+
"",
|
|
4034
|
+
tail.plain || "(empty)",
|
|
4035
|
+
].join("\n");
|
|
4036
|
+
}
|
|
4037
|
+
function renderLogTailHTML(title, tail) {
|
|
4038
|
+
const body = tail.plain
|
|
4039
|
+
? tail.plain.split("\n").map(renderLogLineHTML).join("\n")
|
|
4040
|
+
: "<code>(empty)</code>";
|
|
4041
|
+
return [
|
|
4042
|
+
`<b>${escapeHTML(title)} log tail</b>`,
|
|
4043
|
+
`<b>File:</b> <code>${escapeHTML(tail.filePath)}</code>`,
|
|
4044
|
+
`<b>Updated:</b> <code>${escapeHTML(tail.updatedAt ? formatLogDate(tail.updatedAt) : "-")}</code>`,
|
|
4045
|
+
`<b>Lines:</b> <code>${tail.lineCount}/${tail.requestedLines}</code>`,
|
|
4046
|
+
"",
|
|
4047
|
+
body,
|
|
4048
|
+
].join("\n");
|
|
4049
|
+
}
|
|
4050
|
+
function formatLogDate(date) {
|
|
4051
|
+
return [
|
|
4052
|
+
`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`,
|
|
4053
|
+
`${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`,
|
|
4054
|
+
].join(" ");
|
|
4055
|
+
}
|
|
4056
|
+
function renderLogLineHTML(line) {
|
|
4057
|
+
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>.*)$/);
|
|
4058
|
+
if (structured?.groups) {
|
|
4059
|
+
const level = structured.groups.level;
|
|
4060
|
+
const levelHtml = level === "INFO" ? escapeHTML(level) : `<b>${escapeHTML(level)}</b>`;
|
|
4061
|
+
return [
|
|
4062
|
+
`<code>${escapeHTML(structured.groups.timestamp.trim())}</code>`,
|
|
4063
|
+
levelHtml,
|
|
4064
|
+
escapeHTML(structured.groups.message),
|
|
4065
|
+
].join(" ");
|
|
4066
|
+
}
|
|
4067
|
+
return escapeHTML(line);
|
|
4068
|
+
}
|
|
4069
|
+
function renderAuditEvents(events) {
|
|
4070
|
+
if (events.length === 0) {
|
|
4071
|
+
return {
|
|
4072
|
+
plain: "Audit log is empty.",
|
|
4073
|
+
html: escapeHTML("Audit log is empty."),
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
const lines = events.map((event) => {
|
|
4077
|
+
const time = formatLocalDateTime(new Date(event.timestamp));
|
|
4078
|
+
const actor = event.actorId ? `user ${event.actorId}` : "system";
|
|
4079
|
+
const prompt = event.promptId ? ` · ${event.promptId}` : "";
|
|
4080
|
+
const detail = event.detail ? ` · ${trimLine(event.detail, 90)}` : "";
|
|
4081
|
+
const description = event.description ? ` · ${trimLine(event.description, 90)}` : "";
|
|
4082
|
+
return `${time} · ${event.status.toUpperCase()} · ${event.action} · ${actor}${prompt}${description}${detail}`;
|
|
4083
|
+
});
|
|
4084
|
+
return {
|
|
4085
|
+
plain: ["Audit:", ...lines].join("\n"),
|
|
4086
|
+
html: [
|
|
4087
|
+
"<b>Audit:</b>",
|
|
4088
|
+
...lines.map((line) => escapeHTML(line)),
|
|
4089
|
+
].join("\n"),
|
|
4090
|
+
};
|
|
4091
|
+
}
|
|
4092
|
+
function renderSessionLocks(locks) {
|
|
4093
|
+
if (locks.length === 0) {
|
|
4094
|
+
return {
|
|
4095
|
+
plain: "No active session locks.",
|
|
4096
|
+
html: escapeHTML("No active session locks."),
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
4099
|
+
const lines = locks.map((lock) => {
|
|
4100
|
+
const expires = lock.expiresAt ? ` · expires ${formatLocalDateTime(new Date(lock.expiresAt))}` : "";
|
|
4101
|
+
return `${lock.contextKey} · ${formatLockOwner(lock)}${expires}`;
|
|
4102
|
+
});
|
|
4103
|
+
return {
|
|
4104
|
+
plain: ["Session locks:", ...lines].join("\n"),
|
|
4105
|
+
html: ["<b>Session locks:</b>", ...lines.map((line) => escapeHTML(line))].join("\n"),
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
function renderQueuedPromptDetail(item) {
|
|
4109
|
+
const lines = [
|
|
4110
|
+
"Queued prompt:",
|
|
4111
|
+
`ID: ${item.id}`,
|
|
4112
|
+
`Created: ${formatLocalDateTime(new Date(item.createdAt))}`,
|
|
4113
|
+
item.notBefore ? `Scheduled: ${formatLocalDateTime(new Date(item.notBefore))}` : undefined,
|
|
4114
|
+
`Attempts: ${item.attempts ?? 0}`,
|
|
4115
|
+
item.lastError ? `Last error: ${item.lastError}` : undefined,
|
|
4116
|
+
`Description: ${item.description}`,
|
|
4117
|
+
].filter((line) => Boolean(line));
|
|
4118
|
+
return {
|
|
4119
|
+
plain: lines.join("\n"),
|
|
4120
|
+
html: [
|
|
4121
|
+
"<b>Queued prompt:</b>",
|
|
4122
|
+
`<b>ID:</b> <code>${escapeHTML(item.id)}</code>`,
|
|
4123
|
+
`<b>Created:</b> <code>${escapeHTML(formatLocalDateTime(new Date(item.createdAt)))}</code>`,
|
|
4124
|
+
item.notBefore ? `<b>Scheduled:</b> <code>${escapeHTML(formatLocalDateTime(new Date(item.notBefore)))}</code>` : undefined,
|
|
4125
|
+
`<b>Attempts:</b> <code>${item.attempts ?? 0}</code>`,
|
|
4126
|
+
item.lastError ? `<b>Last error:</b> ${escapeHTML(item.lastError)}` : undefined,
|
|
4127
|
+
`<b>Description:</b> ${escapeHTML(item.description)}`,
|
|
4128
|
+
].filter((line) => Boolean(line)).join("\n"),
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4131
|
+
function formatLockOwner(lock) {
|
|
4132
|
+
if (!lock) {
|
|
4133
|
+
return "nobody";
|
|
4134
|
+
}
|
|
4135
|
+
return lock.ownerName ? `${lock.ownerName} (${lock.ownerId})` : `user ${lock.ownerId}`;
|
|
4136
|
+
}
|
|
4137
|
+
function formatTelegramName(ctx) {
|
|
4138
|
+
const firstName = ctx.from?.first_name?.trim();
|
|
4139
|
+
const lastName = ctx.from?.last_name?.trim();
|
|
4140
|
+
const username = ctx.from?.username?.trim();
|
|
4141
|
+
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim();
|
|
4142
|
+
return fullName || (username ? `@${username}` : undefined);
|
|
4143
|
+
}
|
|
4144
|
+
function formatLocalDateTime(date) {
|
|
4145
|
+
if (Number.isNaN(date.getTime())) {
|
|
4146
|
+
return "-";
|
|
4147
|
+
}
|
|
4148
|
+
return [
|
|
4149
|
+
`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`,
|
|
4150
|
+
`${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`,
|
|
4151
|
+
].join(" ");
|
|
4152
|
+
}
|
|
4153
|
+
function pad2(value) {
|
|
4154
|
+
return String(value).padStart(2, "0");
|
|
4155
|
+
}
|
|
3696
4156
|
function buildArtifactActionsKeyboard(reports) {
|
|
3697
4157
|
const keyboard = new InlineKeyboard();
|
|
3698
4158
|
for (const [index, report] of reports.slice(0, 5).entries()) {
|
|
@@ -3705,6 +4165,35 @@ function buildArtifactActionsKeyboard(reports) {
|
|
|
3705
4165
|
}
|
|
3706
4166
|
return keyboard;
|
|
3707
4167
|
}
|
|
4168
|
+
function filterArtifactReports(reports, argument) {
|
|
4169
|
+
const normalized = argument.trim().toLowerCase();
|
|
4170
|
+
if (!normalized) {
|
|
4171
|
+
return null;
|
|
4172
|
+
}
|
|
4173
|
+
let predicate = null;
|
|
4174
|
+
if (normalized === "images" || normalized === "image" || normalized === "photos") {
|
|
4175
|
+
predicate = (artifact) => isTelegramImagePreview(artifact);
|
|
4176
|
+
}
|
|
4177
|
+
else if (normalized === "docs" || normalized === "documents" || normalized === "files") {
|
|
4178
|
+
predicate = (artifact) => !isTelegramImagePreview(artifact);
|
|
4179
|
+
}
|
|
4180
|
+
else if (normalized.startsWith("search ")) {
|
|
4181
|
+
const query = normalized.slice("search ".length).trim();
|
|
4182
|
+
if (!query) {
|
|
4183
|
+
return [];
|
|
4184
|
+
}
|
|
4185
|
+
predicate = (artifact) => artifact.name.toLowerCase().includes(query);
|
|
4186
|
+
}
|
|
4187
|
+
if (!predicate) {
|
|
4188
|
+
return null;
|
|
4189
|
+
}
|
|
4190
|
+
return reports
|
|
4191
|
+
.map((report) => ({
|
|
4192
|
+
...report,
|
|
4193
|
+
artifacts: report.artifacts.filter(predicate),
|
|
4194
|
+
}))
|
|
4195
|
+
.filter((report) => report.artifacts.length > 0);
|
|
4196
|
+
}
|
|
3708
4197
|
function renderProgressPlain(progress, queueLength, busyState, info) {
|
|
3709
4198
|
const busyFlags = formatBusyFlags(busyState);
|
|
3710
4199
|
if (!progress) {
|
|
@@ -3966,6 +4455,8 @@ function renderDiagnosticsPlain(config, registry, health, authenticated, role, q
|
|
|
3966
4455
|
`PID: ${health.state.pid ?? "-"} (${health.pidRunning ? "running" : "not running"})`,
|
|
3967
4456
|
`App PID: ${health.state.appPid ?? "-"} (${health.appPidRunning ? "running" : "not running"})`,
|
|
3968
4457
|
`Workspace: ${config.workspace}`,
|
|
4458
|
+
`State backend: ${config.stateBackend}`,
|
|
4459
|
+
`Telegram transport: ${config.telegramTransport}`,
|
|
3969
4460
|
`Codex CLI: ${health.codexCli}`,
|
|
3970
4461
|
`Pi CLI: ${health.piCli}`,
|
|
3971
4462
|
`Enabled agents/default: ${enabledAgents(config).join(", ")} / ${config.defaultAgent}`,
|
|
@@ -3986,6 +4477,8 @@ function renderDiagnosticsPlain(config, registry, health, authenticated, role, q
|
|
|
3986
4477
|
`Artifact retention: ${config.artifactRetentionDays}d / ${config.artifactMaxTurnDirs} turns / ${config.artifactMaxInboxDirs} inbox dirs`,
|
|
3987
4478
|
`Workspace allowed/warn roots: ${config.workspaceAllowedRoots.length}/${config.workspaceWarnRoots.length}`,
|
|
3988
4479
|
`Allowed users/chats/admins/readonly: ${config.telegramAllowedUserIds.length}/${config.telegramAllowedChatIds.length}/${config.telegramAdminUserIds.length}/${config.telegramReadOnlyUserIds.length}`,
|
|
4480
|
+
`Session lock TTL: ${config.sessionLockTtlMs} ms`,
|
|
4481
|
+
`Audit max events: ${config.auditMaxEvents}`,
|
|
3989
4482
|
`Loaded sessions: ${contexts.length}`,
|
|
3990
4483
|
`Current queue: ${queueLength}`,
|
|
3991
4484
|
`Current progress: ${progress?.status ?? "idle"}`,
|
|
@@ -4002,6 +4495,8 @@ function renderDiagnosticsHTML(config, registry, health, authenticated, role, qu
|
|
|
4002
4495
|
`<b>PID:</b> <code>${escapeHTML(String(health.state.pid ?? "-"))} (${health.pidRunning ? "running" : "not running"})</code>`,
|
|
4003
4496
|
`<b>App PID:</b> <code>${escapeHTML(String(health.state.appPid ?? "-"))} (${health.appPidRunning ? "running" : "not running"})</code>`,
|
|
4004
4497
|
`<b>Workspace:</b> <code>${escapeHTML(config.workspace)}</code>`,
|
|
4498
|
+
`<b>State backend:</b> <code>${escapeHTML(config.stateBackend)}</code>`,
|
|
4499
|
+
`<b>Telegram transport:</b> <code>${escapeHTML(config.telegramTransport)}</code>`,
|
|
4005
4500
|
`<b>Codex CLI:</b> <code>${escapeHTML(health.codexCli)}</code>`,
|
|
4006
4501
|
`<b>Pi CLI:</b> <code>${escapeHTML(health.piCli)}</code>`,
|
|
4007
4502
|
`<b>Enabled agents/default:</b> <code>${escapeHTML(`${enabledAgents(config).join(", ")} / ${config.defaultAgent}`)}</code>`,
|
|
@@ -4022,6 +4517,8 @@ function renderDiagnosticsHTML(config, registry, health, authenticated, role, qu
|
|
|
4022
4517
|
`<b>Artifact retention:</b> <code>${config.artifactRetentionDays}d / ${config.artifactMaxTurnDirs} turns / ${config.artifactMaxInboxDirs} inbox dirs</code>`,
|
|
4023
4518
|
`<b>Workspace allowed/warn roots:</b> <code>${config.workspaceAllowedRoots.length}/${config.workspaceWarnRoots.length}</code>`,
|
|
4024
4519
|
`<b>Allowed users/chats/admins/readonly:</b> <code>${config.telegramAllowedUserIds.length}/${config.telegramAllowedChatIds.length}/${config.telegramAdminUserIds.length}/${config.telegramReadOnlyUserIds.length}</code>`,
|
|
4520
|
+
`<b>Session lock TTL:</b> <code>${config.sessionLockTtlMs} ms</code>`,
|
|
4521
|
+
`<b>Audit max events:</b> <code>${config.auditMaxEvents}</code>`,
|
|
4025
4522
|
`<b>Loaded sessions:</b> <code>${contexts.length}</code>`,
|
|
4026
4523
|
`<b>Current queue:</b> <code>${queueLength}</code>`,
|
|
4027
4524
|
`<b>Current progress:</b> <code>${escapeHTML(progress?.status ?? "idle")}</code>`,
|