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