@nordbyte/nordrelay 0.4.0 → 0.5.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 +155 -64
- package/README.md +80 -58
- package/dist/access-control.js +126 -114
- package/dist/agent-feature-matrix.js +42 -0
- package/dist/agent-updates.js +312 -0
- package/dist/bot-rendering.js +838 -0
- package/dist/bot.js +130 -1371
- package/dist/channel-actions.js +372 -0
- package/dist/channel-runtime.js +89 -0
- package/dist/config-metadata.js +238 -0
- package/dist/config.js +0 -58
- package/dist/index.js +8 -0
- package/dist/operations.js +33 -8
- package/dist/relay-runtime.js +159 -31
- package/dist/session-format.js +72 -3
- package/dist/settings-service.js +2 -117
- package/dist/telegram-access-commands.js +123 -0
- package/dist/telegram-access-middleware.js +129 -0
- package/dist/telegram-channel-runtime.js +132 -0
- package/dist/telegram-command-menu.js +54 -0
- package/dist/telegram-output.js +216 -0
- package/dist/telegram-update-commands.js +88 -0
- package/dist/user-management.js +708 -0
- package/dist/web-api-contract.js +56 -0
- package/dist/web-dashboard-assets.js +33 -0
- package/dist/web-dashboard-ui.js +14 -14
- package/dist/web-dashboard.js +649 -369
- package/dist/webui-assets/dashboard.css +919 -0
- package/dist/webui-assets/dashboard.js +1611 -0
- package/package.json +6 -3
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/commands/remote.md +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +283 -87
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +1 -1
package/dist/bot.js
CHANGED
|
@@ -4,15 +4,18 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { autoRetry } from "@grammyjs/auto-retry";
|
|
6
6
|
import { Bot, InlineKeyboard, InputFile } from "grammy";
|
|
7
|
-
import {
|
|
7
|
+
import { ADMIN_GROUP_ID } 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
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, parseLogsCommand, renderAgentUpdateJobAction, renderAgentsAction, renderArtifactReportsAction, renderChannelsAction, renderLogTailsAction, renderQueueListAction, renderQueuedPromptDetailAction, } from "./channel-actions.js";
|
|
14
16
|
import { listChannelDescriptors } from "./channel-adapter.js";
|
|
15
|
-
import {
|
|
17
|
+
import { deliverChannelAction } from "./channel-runtime.js";
|
|
18
|
+
import { agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
|
|
16
19
|
import { getAgentActivityLog, getAgentDiagnostics, getExternalActivityForSession, getExternalSnapshotForSession, } from "./agent-activity.js";
|
|
17
20
|
import { enabledAgents } from "./agent-factory.js";
|
|
18
21
|
import { checkAuthStatus, clearAuthCache, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
|
|
@@ -20,53 +23,34 @@ import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout
|
|
|
20
23
|
import { formatLaunchProfileBehavior } from "./codex-launch.js";
|
|
21
24
|
import { contextKeyFromCtx, isTelegramContextKey, isTopicContextKey, parseContextKey } from "./context-key.js";
|
|
22
25
|
import { friendlyErrorText } from "./error-messages.js";
|
|
23
|
-
import { escapeHTML
|
|
24
|
-
import { getConnectorHealth,
|
|
26
|
+
import { escapeHTML } from "./format.js";
|
|
27
|
+
import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, } from "./operations.js";
|
|
25
28
|
import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
|
|
26
29
|
import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
|
|
27
30
|
import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
|
|
28
31
|
import { checkPiAuthStatus } from "./pi-auth.js";
|
|
29
32
|
import { configureRedaction, redactText } from "./redaction.js";
|
|
30
33
|
import { canWriteWithLock, SessionLockStore } from "./session-locks.js";
|
|
31
|
-
import {
|
|
34
|
+
import { renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
|
|
32
35
|
import { SessionRegistry } from "./session-registry.js";
|
|
33
36
|
import { getAvailableBackends, transcribeAudio } from "./voice.js";
|
|
34
37
|
import { getTelegramRateLimitMetrics, telegramRateLimiter } from "./telegram-rate-limit.js";
|
|
38
|
+
import { chatBucket, downloadTelegramFile, isMessageNotModifiedError, renderMarkdownChunkWithinLimit, safeEditMessage, safeEditReplyMarkup, safeReply, sendChatActionSafe, sendTextMessage, splitMarkdownForTelegram, } from "./telegram-output.js";
|
|
39
|
+
import { NOOP_PAGE_CALLBACK_DATA, TelegramBotChannelRuntime, paginateKeyboard, telegramChannelContextFromCtx, } from "./telegram-channel-runtime.js";
|
|
40
|
+
import { createTelegramAccessMiddleware } from "./telegram-access-middleware.js";
|
|
41
|
+
import { registerTelegramAccessCommands } from "./telegram-access-commands.js";
|
|
42
|
+
import { registerTelegramUpdateCommands } from "./telegram-update-commands.js";
|
|
43
|
+
import { appendWithCap, authHelpText, buildArtifactActionsKeyboard, buildStreamingPreview, capabilitiesOf, filterActivityEvents, filterArtifactReports, filterSessions, formatAgentLaunchProfileLabel, formatAgentSettingScope, formatCliPathHTML, formatCliPathPlain, formatDurationSeconds, formatError, formatLocalDateTime, formatLockOwner, formatModelButtonLabel, formatRelativeTime, formatTelegramName, formatToolSummaryLine, formatTurnUsageLine, getWorkspaceShortName, idOf, isEmptyArtifactReport, isPromptEnvelopeLike, isQueuedPromptLike, labelOf, orderPinnedSessions, parseActivityOptions, parseFastModeArgument, parseToggle, renderActivityTimeline, renderAgentDiagnostics, renderAuditEvents, renderDiagnosticsHTML, renderDiagnosticsPlain, renderExternalMirrorEvent, renderExternalMirrorStatus, renderHealthHTML, renderHealthPlain, renderPromptFailure, renderProgressHTML, renderProgressPlain, renderSessionLocks, renderTodoList, renderToolEndMessage, renderToolStartMessage, renderVersionCheckHTML, renderVersionCheckPlain, requiresTurnApproval, trimLine, } from "./bot-rendering.js";
|
|
44
|
+
import { UserStore } from "./user-management.js";
|
|
35
45
|
import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "./workspace-policy.js";
|
|
36
|
-
|
|
46
|
+
export { formatToolSummaryLine, formatTurnUsageLine, summarizeToolName } from "./bot-rendering.js";
|
|
47
|
+
export { registerCommands } from "./telegram-command-menu.js";
|
|
37
48
|
const EDIT_DEBOUNCE_MS = 1500;
|
|
38
49
|
const TYPING_INTERVAL_MS = 4500;
|
|
39
50
|
const TOOL_OUTPUT_PREVIEW_LIMIT = 500;
|
|
40
|
-
const STREAMING_PREVIEW_LIMIT = 3800;
|
|
41
|
-
const FORMATTED_CHUNK_TARGET = 3000;
|
|
42
51
|
const MAX_AUDIO_FILE_SIZE = 25 * 1024 * 1024;
|
|
43
52
|
const MEDIA_GROUP_FLUSH_MS = 1200;
|
|
44
|
-
const KEYBOARD_PAGE_SIZE = 6;
|
|
45
|
-
const NOOP_PAGE_CALLBACK_DATA = "noop_page";
|
|
46
53
|
const LAUNCH_PROFILES_COMMAND = "/launch_profiles";
|
|
47
|
-
function paginateKeyboard(items, page, prefix) {
|
|
48
|
-
const totalPages = Math.max(1, Math.ceil(items.length / KEYBOARD_PAGE_SIZE));
|
|
49
|
-
const currentPage = Math.min(Math.max(page, 0), totalPages - 1);
|
|
50
|
-
const start = currentPage * KEYBOARD_PAGE_SIZE;
|
|
51
|
-
const pageItems = items.slice(start, start + KEYBOARD_PAGE_SIZE);
|
|
52
|
-
const keyboard = new InlineKeyboard();
|
|
53
|
-
pageItems.forEach((item, index) => {
|
|
54
|
-
keyboard.text(item.label, item.callbackData);
|
|
55
|
-
if (index < pageItems.length - 1 || totalPages > 1) {
|
|
56
|
-
keyboard.row();
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
if (totalPages > 1) {
|
|
60
|
-
if (currentPage > 0) {
|
|
61
|
-
keyboard.text("◀️ Prev", `${prefix}_page_${currentPage - 1}`);
|
|
62
|
-
}
|
|
63
|
-
keyboard.text(`${currentPage + 1}/${totalPages}`, NOOP_PAGE_CALLBACK_DATA);
|
|
64
|
-
if (currentPage < totalPages - 1) {
|
|
65
|
-
keyboard.text("Next ▶️", `${prefix}_page_${currentPage + 1}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return keyboard;
|
|
69
|
-
}
|
|
70
54
|
export function createBot(config, registry) {
|
|
71
55
|
configureRedaction(config.telegramRedactPatterns);
|
|
72
56
|
telegramRateLimiter.configure({
|
|
@@ -76,6 +60,7 @@ export function createBot(config, registry) {
|
|
|
76
60
|
});
|
|
77
61
|
const bot = new Bot(config.telegramBotToken);
|
|
78
62
|
bot.api.config.use(autoRetry({ maxRetryAttempts: 3, maxDelaySeconds: 10 }));
|
|
63
|
+
const telegramChannelRuntime = new TelegramBotChannelRuntime(bot);
|
|
79
64
|
const contextBusy = new Map();
|
|
80
65
|
const pendingApprovals = new Map();
|
|
81
66
|
const pendingSessionPicks = new Map();
|
|
@@ -94,6 +79,10 @@ export function createBot(config, registry) {
|
|
|
94
79
|
const preferencesStore = new BotPreferencesStore(config.workspace, config.stateBackend);
|
|
95
80
|
const auditLog = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
|
|
96
81
|
const lockStore = new SessionLockStore(config.workspace, config.stateBackend);
|
|
82
|
+
const userStore = new UserStore();
|
|
83
|
+
const contextUsers = new WeakMap();
|
|
84
|
+
const agentUpdates = new AgentUpdateManager();
|
|
85
|
+
const linkAttempts = new Map();
|
|
97
86
|
const drainingQueues = new Set();
|
|
98
87
|
const externalQueueTimers = new Map();
|
|
99
88
|
const externalMirrors = new Map();
|
|
@@ -220,6 +209,41 @@ export function createBot(config, registry) {
|
|
|
220
209
|
}
|
|
221
210
|
return checkAuthStatus(config.codexApiKey);
|
|
222
211
|
};
|
|
212
|
+
const replyChannelAction = async (ctx, rendered) => {
|
|
213
|
+
const channelContext = telegramChannelContextFromCtx(ctx);
|
|
214
|
+
if (!channelContext) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await deliverChannelAction(telegramChannelRuntime, channelContext, rendered);
|
|
218
|
+
};
|
|
219
|
+
const agentUpdateContext = () => ({
|
|
220
|
+
piCliPath: config.piCliPath,
|
|
221
|
+
hermesCliPath: config.hermesCliPath,
|
|
222
|
+
openClawCliPath: config.openClawCliPath,
|
|
223
|
+
claudeCodeCliPath: config.claudeCodeCliPath,
|
|
224
|
+
});
|
|
225
|
+
const startTelegramAgentUpdate = async (ctx, agentId) => {
|
|
226
|
+
try {
|
|
227
|
+
const job = agentUpdates.start(agentId, agentUpdateContext());
|
|
228
|
+
const contextKey = contextKeyFromCtx(ctx);
|
|
229
|
+
if (contextKey) {
|
|
230
|
+
audit({
|
|
231
|
+
action: "command",
|
|
232
|
+
status: "ok",
|
|
233
|
+
contextKey,
|
|
234
|
+
agentId,
|
|
235
|
+
description: `update ${agentId}`,
|
|
236
|
+
detail: job.summary,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const rendered = renderAgentUpdateJobAction(job);
|
|
240
|
+
await replyChannelAction(ctx, rendered);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const message = `Failed to start ${agentLabel(agentId)} update: ${friendlyErrorText(error)}`;
|
|
244
|
+
await safeReply(ctx, `<b>Update failed:</b> ${escapeHTML(message)}`, { fallbackText: message });
|
|
245
|
+
}
|
|
246
|
+
};
|
|
223
247
|
const startAgentLogin = (info) => {
|
|
224
248
|
const agentId = agentIdForAuth(info);
|
|
225
249
|
if (agentId === "hermes") {
|
|
@@ -324,22 +348,10 @@ export function createBot(config, registry) {
|
|
|
324
348
|
const createQueuedPromptCancelKeyboard = (contextKey, queueId, label = "Cancel queued message") => new InlineKeyboard().text(label, queueCancelCallbackData("cancel", contextKey, queueId));
|
|
325
349
|
const renderQueueList = (contextKey, queue) => {
|
|
326
350
|
const paused = promptStore.isPaused(contextKey);
|
|
351
|
+
const rendered = renderQueueListAction(queue, paused);
|
|
327
352
|
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
|
-
};
|
|
353
|
+
return rendered;
|
|
332
354
|
}
|
|
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
355
|
const keyboard = new InlineKeyboard();
|
|
344
356
|
queue.forEach((item, index) => {
|
|
345
357
|
keyboard
|
|
@@ -352,11 +364,7 @@ export function createBot(config, registry) {
|
|
|
352
364
|
.text("Down", queueCancelCallbackData("down", contextKey, item.id))
|
|
353
365
|
.row();
|
|
354
366
|
});
|
|
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
|
-
};
|
|
367
|
+
return { ...rendered, keyboard };
|
|
360
368
|
};
|
|
361
369
|
const createSystemContext = (contextKey) => {
|
|
362
370
|
const parsed = parseContextKey(contextKey);
|
|
@@ -367,6 +375,9 @@ export function createBot(config, registry) {
|
|
|
367
375
|
};
|
|
368
376
|
};
|
|
369
377
|
const updateQueueStatusMessage = async (contextKey, text) => {
|
|
378
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
370
381
|
const parsed = parseContextKey(contextKey);
|
|
371
382
|
const html = escapeHTML(text);
|
|
372
383
|
const state = queueStatusMessages.get(contextKey) ?? {};
|
|
@@ -409,6 +420,9 @@ export function createBot(config, registry) {
|
|
|
409
420
|
if (!isTelegramContextKey(contextKey)) {
|
|
410
421
|
return;
|
|
411
422
|
}
|
|
423
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
412
426
|
const session = await registry.getOrCreate(contextKey, { deferThreadStart: true }).catch(() => null);
|
|
413
427
|
if (!session) {
|
|
414
428
|
return;
|
|
@@ -557,7 +571,20 @@ export function createBot(config, registry) {
|
|
|
557
571
|
}
|
|
558
572
|
state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
|
|
559
573
|
};
|
|
574
|
+
const canSendSystemMessagesToContext = (contextKey) => {
|
|
575
|
+
if (!userStore.hasAdminUser()) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
const parsed = parseContextKey(contextKey);
|
|
579
|
+
if (parsed.chatId > 0) {
|
|
580
|
+
return Boolean(userStore.resolveTelegramUser(parsed.chatId));
|
|
581
|
+
}
|
|
582
|
+
return userStore.snapshot().telegramChats.some((chat) => chat.chatId === parsed.chatId && chat.enabled);
|
|
583
|
+
};
|
|
560
584
|
const deliverCliGeneratedArtifacts = async (contextKey, chatId, session, startedAt, turnId, messageThreadId) => {
|
|
585
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
561
588
|
if (!startedAt || !turnId) {
|
|
562
589
|
return;
|
|
563
590
|
}
|
|
@@ -609,6 +636,9 @@ export function createBot(config, registry) {
|
|
|
609
636
|
if (promptStore.list(contextKey).length === 0) {
|
|
610
637
|
return;
|
|
611
638
|
}
|
|
639
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
612
642
|
const busy = getBusyReason(contextKey);
|
|
613
643
|
if (busy.kind === "external") {
|
|
614
644
|
const label = busy.activity.agentLabel;
|
|
@@ -628,37 +658,12 @@ export function createBot(config, registry) {
|
|
|
628
658
|
timer.unref?.();
|
|
629
659
|
externalQueueTimers.set(contextKey, timer);
|
|
630
660
|
};
|
|
661
|
+
const getAuthenticatedUser = (ctx) => contextUsers.get(ctx) ?? null;
|
|
631
662
|
const getUserRole = (ctx) => {
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
return "admin";
|
|
635
|
-
}
|
|
636
|
-
if (fromId !== undefined && config.telegramReadOnlyUserIdSet.has(fromId)) {
|
|
637
|
-
return "readonly";
|
|
638
|
-
}
|
|
639
|
-
return "operator";
|
|
640
|
-
};
|
|
641
|
-
const getRequiredPermission = (ctx) => {
|
|
642
|
-
if (ctx.callbackQuery?.data) {
|
|
643
|
-
return permissionForCallbackData(ctx.callbackQuery.data);
|
|
644
|
-
}
|
|
645
|
-
if (ctx.message?.voice || ctx.message?.audio || ctx.message?.photo || ctx.message?.document) {
|
|
646
|
-
return "files";
|
|
647
|
-
}
|
|
648
|
-
const text = ctx.message?.text?.trim();
|
|
649
|
-
if (!text) {
|
|
650
|
-
return "inspect";
|
|
651
|
-
}
|
|
652
|
-
if (!text.startsWith("/")) {
|
|
653
|
-
return "prompt";
|
|
654
|
-
}
|
|
655
|
-
const command = extractCommandName(text);
|
|
656
|
-
if (command === "queue") {
|
|
657
|
-
const argument = text.replace(/^\/queue(?:@\w+)?\s*/i, "").trim();
|
|
658
|
-
return argument ? "prompt" : "inspect";
|
|
659
|
-
}
|
|
660
|
-
return permissionForCommand(command);
|
|
663
|
+
const authUser = getAuthenticatedUser(ctx);
|
|
664
|
+
return authUser?.groups.map((group) => group.name).join(", ") || "unauthenticated";
|
|
661
665
|
};
|
|
666
|
+
const isAdminUser = (ctx) => Boolean(getAuthenticatedUser(ctx)?.groups.some((group) => group.id === ADMIN_GROUP_ID));
|
|
662
667
|
const audit = (event) => {
|
|
663
668
|
try {
|
|
664
669
|
auditLog.append(event);
|
|
@@ -681,7 +686,7 @@ export function createBot(config, registry) {
|
|
|
681
686
|
};
|
|
682
687
|
const denyIfLocked = async (ctx, contextKey, session) => {
|
|
683
688
|
const lock = lockStore.get(contextKey);
|
|
684
|
-
const isAdmin =
|
|
689
|
+
const isAdmin = isAdminUser(ctx);
|
|
685
690
|
if (canWriteWithLock(lock, ctx.from?.id, isAdmin)) {
|
|
686
691
|
return false;
|
|
687
692
|
}
|
|
@@ -752,6 +757,9 @@ export function createBot(config, registry) {
|
|
|
752
757
|
}
|
|
753
758
|
pendingApprovals.delete(approvalId);
|
|
754
759
|
getBusyState(contextKey).approving = false;
|
|
760
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
755
763
|
const parsed = parseContextKey(contextKey);
|
|
756
764
|
void sendTextMessage(bot.api, parsed.chatId, `Approval timed out for prompt ${approvalId}.`, {
|
|
757
765
|
messageThreadId: parsed.messageThreadId,
|
|
@@ -782,6 +790,9 @@ export function createBot(config, registry) {
|
|
|
782
790
|
await safeReply(ctx, html, { fallbackText: plain, replyMarkup: keyboard });
|
|
783
791
|
};
|
|
784
792
|
const handleUserPrompt = async (ctx, contextKey, chatId, session, prompt, options = {}) => {
|
|
793
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
785
796
|
const parsed = parseContextKey(contextKey);
|
|
786
797
|
const messageThreadId = parsed.messageThreadId;
|
|
787
798
|
const envelope = isPromptEnvelopeLike(prompt) ? prompt : toPromptEnvelope(prompt);
|
|
@@ -1325,6 +1336,9 @@ export function createBot(config, registry) {
|
|
|
1325
1336
|
if (drainingQueues.has(contextKey)) {
|
|
1326
1337
|
return;
|
|
1327
1338
|
}
|
|
1339
|
+
if (!canSendSystemMessagesToContext(contextKey)) {
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1328
1342
|
drainingQueues.add(contextKey);
|
|
1329
1343
|
try {
|
|
1330
1344
|
while (true) {
|
|
@@ -1487,16 +1501,25 @@ export function createBot(config, registry) {
|
|
|
1487
1501
|
clearTimeout(pending.timer);
|
|
1488
1502
|
pendingMediaGroups.delete(key);
|
|
1489
1503
|
try {
|
|
1504
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1490
1507
|
await processMediaGroup(pending);
|
|
1491
1508
|
}
|
|
1492
1509
|
catch (error) {
|
|
1493
1510
|
console.error("Failed to process media group:", error);
|
|
1511
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1494
1514
|
await safeReply(pending.ctx, `<b>Failed to process media group:</b> ${escapeHTML(friendlyErrorText(error))}`, {
|
|
1495
1515
|
fallbackText: `Failed to process media group: ${friendlyErrorText(error)}`,
|
|
1496
1516
|
});
|
|
1497
1517
|
}
|
|
1498
1518
|
};
|
|
1499
1519
|
const processMediaGroup = async (pending) => {
|
|
1520
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1500
1523
|
const busyState = getBusyState(pending.contextKey);
|
|
1501
1524
|
busyState.transcribing = true;
|
|
1502
1525
|
const turnId = randomUUID().slice(0, 12);
|
|
@@ -1547,10 +1570,16 @@ export function createBot(config, registry) {
|
|
|
1547
1570
|
busyState.transcribing = false;
|
|
1548
1571
|
}
|
|
1549
1572
|
if (stagedFiles.length === 0) {
|
|
1573
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1550
1576
|
const text = skippedCount > 0 ? "No media group files could be staged." : "Media group was empty.";
|
|
1551
1577
|
await safeReply(pending.ctx, escapeHTML(text), { fallbackText: text });
|
|
1552
1578
|
return;
|
|
1553
1579
|
}
|
|
1580
|
+
if (!canSendSystemMessagesToContext(pending.contextKey)) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1554
1583
|
const receivedText = `Received ${stagedFiles.length} media group file${stagedFiles.length === 1 ? "" : "s"}${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}.`;
|
|
1555
1584
|
await safeReply(pending.ctx, escapeHTML(receivedText), { fallbackText: receivedText });
|
|
1556
1585
|
await sendChatActionSafe(pending.ctx.api, pending.chatId, "typing", pending.messageThreadId).catch(() => { });
|
|
@@ -1572,35 +1601,8 @@ export function createBot(config, registry) {
|
|
|
1572
1601
|
await clearReaction(pending.ctx);
|
|
1573
1602
|
}
|
|
1574
1603
|
};
|
|
1575
|
-
bot.use(
|
|
1576
|
-
|
|
1577
|
-
const chatId = ctx.chat?.id;
|
|
1578
|
-
const authorized = config.telegramAllowAnyChat ||
|
|
1579
|
-
(fromId !== undefined && config.telegramAllowedUserIdSet.has(fromId)) ||
|
|
1580
|
-
(chatId !== undefined && config.telegramAllowedChatIdSet.has(chatId));
|
|
1581
|
-
if (!authorized) {
|
|
1582
|
-
if (ctx.callbackQuery) {
|
|
1583
|
-
await ctx.answerCallbackQuery({ text: "Unauthorized" }).catch(() => { });
|
|
1584
|
-
}
|
|
1585
|
-
else if (ctx.chat) {
|
|
1586
|
-
await safeReply(ctx, escapeHTML("Unauthorized"), { fallbackText: "Unauthorized" });
|
|
1587
|
-
}
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
const role = getUserRole(ctx);
|
|
1591
|
-
const permission = getRequiredPermission(ctx);
|
|
1592
|
-
if (!hasTelegramPermission(config.telegramRolePolicies, role, permission)) {
|
|
1593
|
-
const message = `Access denied: ${permission} permission required.`;
|
|
1594
|
-
if (ctx.callbackQuery) {
|
|
1595
|
-
await ctx.answerCallbackQuery({ text: message }).catch(() => { });
|
|
1596
|
-
}
|
|
1597
|
-
else {
|
|
1598
|
-
await safeReply(ctx, escapeHTML(message), { fallbackText: message });
|
|
1599
|
-
}
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
await next();
|
|
1603
|
-
});
|
|
1604
|
+
bot.use(createTelegramAccessMiddleware({ userStore, contextUsers, audit }));
|
|
1605
|
+
registerTelegramAccessCommands({ bot, userStore, contextUsers, linkAttempts, audit, getUserRole });
|
|
1604
1606
|
bot.command("start", async (ctx) => {
|
|
1605
1607
|
const contextSession = await getContextSession(ctx, { deferThreadStart: true });
|
|
1606
1608
|
if (!contextSession) {
|
|
@@ -1629,60 +1631,12 @@ export function createBot(config, registry) {
|
|
|
1629
1631
|
await safeReply(ctx, help.html, { fallbackText: help.plain });
|
|
1630
1632
|
});
|
|
1631
1633
|
bot.command("channels", async (ctx) => {
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
const status = descriptor.status === "available" ? "available" : "planned";
|
|
1635
|
-
return `${descriptor.label}: ${status} · ${descriptor.capabilities.join(", ")}`;
|
|
1636
|
-
});
|
|
1637
|
-
const html = [
|
|
1638
|
-
"<b>Channel adapters:</b>",
|
|
1639
|
-
...descriptors.map((descriptor) => {
|
|
1640
|
-
const statusIcon = descriptor.status === "available" ? "✅" : "🟡";
|
|
1641
|
-
const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
|
|
1642
|
-
return `${statusIcon} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(descriptor.status)}</code>\n <code>${escapeHTML(descriptor.capabilities.join(", "))}</code>${notes}`;
|
|
1643
|
-
}),
|
|
1644
|
-
].join("\n");
|
|
1645
|
-
await safeReply(ctx, html, { fallbackText: ["Channel adapters:", ...lines].join("\n") });
|
|
1634
|
+
const rendered = renderChannelsAction(listChannelDescriptors());
|
|
1635
|
+
await replyChannelAction(ctx, rendered);
|
|
1646
1636
|
});
|
|
1647
1637
|
bot.command("agents", async (ctx) => {
|
|
1648
|
-
const
|
|
1649
|
-
|
|
1650
|
-
"Agent adapters:",
|
|
1651
|
-
...descriptors.map((descriptor) => {
|
|
1652
|
-
const enabled = descriptor.id === "codex"
|
|
1653
|
-
? config.codexEnabled
|
|
1654
|
-
: descriptor.id === "pi"
|
|
1655
|
-
? config.piEnabled
|
|
1656
|
-
: descriptor.id === "hermes"
|
|
1657
|
-
? config.hermesEnabled
|
|
1658
|
-
: descriptor.id === "openclaw"
|
|
1659
|
-
? config.openClawEnabled
|
|
1660
|
-
: descriptor.id === "claude-code"
|
|
1661
|
-
? config.claudeCodeEnabled
|
|
1662
|
-
: false;
|
|
1663
|
-
return `${descriptor.label}: ${descriptor.status}${descriptor.status === "available" ? ` · ${enabled ? "enabled" : "disabled"}` : ""}`;
|
|
1664
|
-
}),
|
|
1665
|
-
].join("\n");
|
|
1666
|
-
const html = [
|
|
1667
|
-
"<b>Agent adapters:</b>",
|
|
1668
|
-
...descriptors.map((descriptor) => {
|
|
1669
|
-
const enabled = descriptor.id === "codex"
|
|
1670
|
-
? config.codexEnabled
|
|
1671
|
-
: descriptor.id === "pi"
|
|
1672
|
-
? config.piEnabled
|
|
1673
|
-
: descriptor.id === "hermes"
|
|
1674
|
-
? config.hermesEnabled
|
|
1675
|
-
: descriptor.id === "openclaw"
|
|
1676
|
-
? config.openClawEnabled
|
|
1677
|
-
: descriptor.id === "claude-code"
|
|
1678
|
-
? config.claudeCodeEnabled
|
|
1679
|
-
: false;
|
|
1680
|
-
const status = descriptor.status === "available" ? `${enabled ? "enabled" : "disabled"}` : "planned";
|
|
1681
|
-
const notes = descriptor.notes ? `\n ${escapeHTML(descriptor.notes)}` : "";
|
|
1682
|
-
return `${descriptor.status === "available" ? "✅" : "🟡"} <b>${escapeHTML(descriptor.label)}</b> <code>${escapeHTML(status)}</code>${notes}`;
|
|
1683
|
-
}),
|
|
1684
|
-
].join("\n");
|
|
1685
|
-
await safeReply(ctx, html, { fallbackText: plain });
|
|
1638
|
+
const rendered = renderAgentsAction(listAgentAdapterDescriptors(), enabledAgents(config));
|
|
1639
|
+
await replyChannelAction(ctx, rendered);
|
|
1686
1640
|
});
|
|
1687
1641
|
bot.command("agent", async (ctx) => {
|
|
1688
1642
|
const contextSession = await getContextSession(ctx, { deferThreadStart: true });
|
|
@@ -2139,7 +2093,7 @@ export function createBot(config, registry) {
|
|
|
2139
2093
|
}
|
|
2140
2094
|
const { contextKey, session } = contextSession;
|
|
2141
2095
|
const existing = lockStore.get(contextKey);
|
|
2142
|
-
if (existing && existing.ownerId !== ctx.from.id &&
|
|
2096
|
+
if (existing && existing.ownerId !== ctx.from.id && !isAdminUser(ctx)) {
|
|
2143
2097
|
const text = `Session is already locked by ${formatLockOwner(existing)}.`;
|
|
2144
2098
|
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
2145
2099
|
return;
|
|
@@ -2160,7 +2114,7 @@ export function createBot(config, registry) {
|
|
|
2160
2114
|
}
|
|
2161
2115
|
const { contextKey, session } = contextSession;
|
|
2162
2116
|
const lock = lockStore.get(contextKey);
|
|
2163
|
-
if (lock && lock.ownerId !== ctx.from?.id &&
|
|
2117
|
+
if (lock && lock.ownerId !== ctx.from?.id && !isAdminUser(ctx)) {
|
|
2164
2118
|
const text = `Only ${formatLockOwner(lock)} or an admin can unlock this session.`;
|
|
2165
2119
|
await safeReply(ctx, escapeHTML(text), { fallbackText: text });
|
|
2166
2120
|
return;
|
|
@@ -2244,20 +2198,12 @@ export function createBot(config, registry) {
|
|
|
2244
2198
|
const rawText = ctx.message?.text ?? "";
|
|
2245
2199
|
const argument = rawText.replace(/^\/logs(?:@\w+)?\s*/i, "").trim();
|
|
2246
2200
|
const logRequest = parseLogsCommand(argument);
|
|
2247
|
-
const logs = logRequest.target
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
{
|
|
2254
|
-
title: logRequest.target === "update" ? "Update" : "Connector",
|
|
2255
|
-
tail: await readFormattedLogTail(logRequest.lines, logRequest.target === "update" ? getUpdateLogPath() : undefined),
|
|
2256
|
-
},
|
|
2257
|
-
];
|
|
2258
|
-
const plain = logs.map(({ title, tail }) => renderLogTailPlain(title, tail)).join("\n\n");
|
|
2259
|
-
const html = logs.map(({ title, tail }) => renderLogTailHTML(title, tail)).join("\n\n");
|
|
2260
|
-
await safeReply(ctx, html, { fallbackText: plain });
|
|
2201
|
+
const logs = await Promise.all(logTailRequests(logRequest.target).map(async (request) => ({
|
|
2202
|
+
title: request.title,
|
|
2203
|
+
tail: await readFormattedLogTail(logRequest.lines, request.path),
|
|
2204
|
+
})));
|
|
2205
|
+
const rendered = renderLogTailsAction(logs);
|
|
2206
|
+
await replyChannelAction(ctx, rendered);
|
|
2261
2207
|
});
|
|
2262
2208
|
bot.command("restart", async (ctx) => {
|
|
2263
2209
|
await safeReply(ctx, escapeHTML("Restarting connector..."), {
|
|
@@ -2267,26 +2213,7 @@ export function createBot(config, registry) {
|
|
|
2267
2213
|
spawnConnectorRestart();
|
|
2268
2214
|
}, 300);
|
|
2269
2215
|
});
|
|
2270
|
-
bot
|
|
2271
|
-
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 });
|
|
2289
|
-
});
|
|
2216
|
+
registerTelegramUpdateCommands({ bot, agentUpdates, replyChannelAction, startTelegramAgentUpdate });
|
|
2290
2217
|
bot.command("new", async (ctx) => {
|
|
2291
2218
|
const chatId = ctx.chat?.id;
|
|
2292
2219
|
if (!chatId) {
|
|
@@ -2433,7 +2360,7 @@ export function createBot(config, registry) {
|
|
|
2433
2360
|
});
|
|
2434
2361
|
return;
|
|
2435
2362
|
}
|
|
2436
|
-
const rendered =
|
|
2363
|
+
const rendered = renderQueuedPromptDetailAction(item);
|
|
2437
2364
|
await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
|
|
2438
2365
|
return;
|
|
2439
2366
|
}
|
|
@@ -2591,7 +2518,7 @@ export function createBot(config, registry) {
|
|
|
2591
2518
|
});
|
|
2592
2519
|
return;
|
|
2593
2520
|
}
|
|
2594
|
-
const rendered =
|
|
2521
|
+
const rendered = renderArtifactReportsAction(filtered);
|
|
2595
2522
|
await safeReply(ctx, rendered.html, {
|
|
2596
2523
|
fallbackText: rendered.plain,
|
|
2597
2524
|
replyMarkup: buildArtifactActionsKeyboard(filtered),
|
|
@@ -2617,7 +2544,7 @@ export function createBot(config, registry) {
|
|
|
2617
2544
|
}
|
|
2618
2545
|
return;
|
|
2619
2546
|
}
|
|
2620
|
-
const { html, plain } =
|
|
2547
|
+
const { html, plain } = renderArtifactReportsAction(reports);
|
|
2621
2548
|
await safeReply(ctx, html, {
|
|
2622
2549
|
fallbackText: plain,
|
|
2623
2550
|
replyMarkup: buildArtifactActionsKeyboard(reports),
|
|
@@ -3303,8 +3230,7 @@ export function createBot(config, registry) {
|
|
|
3303
3230
|
await ctx.answerCallbackQuery({ text: "Approval expired" });
|
|
3304
3231
|
return;
|
|
3305
3232
|
}
|
|
3306
|
-
|
|
3307
|
-
if (pending.requestedBy !== undefined && ctx.from?.id !== pending.requestedBy && role !== "admin") {
|
|
3233
|
+
if (pending.requestedBy !== undefined && ctx.from?.id !== pending.requestedBy && !isAdminUser(ctx)) {
|
|
3308
3234
|
await ctx.answerCallbackQuery({ text: "Only the requester or an admin can approve" });
|
|
3309
3235
|
return;
|
|
3310
3236
|
}
|
|
@@ -4020,1170 +3946,3 @@ export function createBot(config, registry) {
|
|
|
4020
3946
|
});
|
|
4021
3947
|
return bot;
|
|
4022
3948
|
}
|
|
4023
|
-
export async function registerCommands(bot) {
|
|
4024
|
-
await bot.api.setMyCommands([
|
|
4025
|
-
{ command: "start", description: "Welcome & status" },
|
|
4026
|
-
{ command: "help", description: "Command reference" },
|
|
4027
|
-
{ command: "channels", description: "Messaging adapter status" },
|
|
4028
|
-
{ command: "agents", description: "Agent adapter status" },
|
|
4029
|
-
{ command: "agent", description: "Select agent" },
|
|
4030
|
-
{ command: "new", description: "Start a new thread" },
|
|
4031
|
-
{ command: "session", description: "Current thread details" },
|
|
4032
|
-
{ command: "sessions", description: "Browse & switch threads" },
|
|
4033
|
-
{ command: "sync", description: "Sync active session from CLI state" },
|
|
4034
|
-
{ command: "pinned", description: "Show pinned threads" },
|
|
4035
|
-
{ command: "pin", description: "Pin current or given thread" },
|
|
4036
|
-
{ command: "unpin", description: "Unpin current or given thread" },
|
|
4037
|
-
{ command: "retry", description: "Resend the last prompt" },
|
|
4038
|
-
{ command: "queue", description: "Show queued prompts" },
|
|
4039
|
-
{ command: "cancel", description: "Cancel a queued prompt" },
|
|
4040
|
-
{ command: "clearqueue", description: "Clear queued prompts" },
|
|
4041
|
-
{ command: "artifacts", description: "List or resend generated files" },
|
|
4042
|
-
{ command: "workspaces", description: "List allowed workspaces" },
|
|
4043
|
-
{ command: "abort", description: "Cancel current operation" },
|
|
4044
|
-
{ command: "stop", description: "Cancel current operation" },
|
|
4045
|
-
{ command: "launch_profiles", description: "Select launch profile" },
|
|
4046
|
-
{ command: "fast", description: "Toggle fast mode" },
|
|
4047
|
-
{ command: "model", description: "View & change model" },
|
|
4048
|
-
{ command: "reasoning", description: "Set reasoning effort" },
|
|
4049
|
-
{ command: "mirror", description: "Control CLI mirroring" },
|
|
4050
|
-
{ command: "notify", description: "Control notifications" },
|
|
4051
|
-
{ command: "auth", description: "Check auth status" },
|
|
4052
|
-
{ command: "login", description: "Start authentication" },
|
|
4053
|
-
{ command: "logout", description: "Sign out" },
|
|
4054
|
-
{ command: "voice", description: "Voice transcription status" },
|
|
4055
|
-
{ command: "tasks", description: "Current turn progress" },
|
|
4056
|
-
{ command: "progress", description: "Current turn progress" },
|
|
4057
|
-
{ command: "activity", description: "Thread activity timeline" },
|
|
4058
|
-
{ command: "audit", description: "Admin: recent audit events" },
|
|
4059
|
-
{ command: "status", description: "Connector runtime status" },
|
|
4060
|
-
{ command: "health", description: "Connector health report" },
|
|
4061
|
-
{ command: "version", description: "Connector version" },
|
|
4062
|
-
{ command: "logs", description: "Admin: show connector logs" },
|
|
4063
|
-
{ command: "diagnostics", description: "Admin: connector diagnostics" },
|
|
4064
|
-
{ command: "lock", description: "Lock session writes to you" },
|
|
4065
|
-
{ command: "unlock", description: "Release session write lock" },
|
|
4066
|
-
{ command: "locks", description: "List session write locks" },
|
|
4067
|
-
{ command: "restart", description: "Admin: restart connector" },
|
|
4068
|
-
{ command: "update", description: "Admin: update connector" },
|
|
4069
|
-
{ command: "handback", description: "Hand session back to CLI" },
|
|
4070
|
-
{ command: "attach", description: "Bind a session to this topic" },
|
|
4071
|
-
{ command: "switch", description: "Switch to a thread by ID" },
|
|
4072
|
-
]);
|
|
4073
|
-
}
|
|
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
|
-
function renderVersionCheckPlain(check) {
|
|
4086
|
-
const icon = versionStatusIcon(check);
|
|
4087
|
-
const label = check.label === "NordRelay" ? "NordRelay" : `${check.label} version`;
|
|
4088
|
-
return `${label}: ${icon} ${formatVersionCheckDetailPlain(check)}`;
|
|
4089
|
-
}
|
|
4090
|
-
function renderVersionCheckHTML(check) {
|
|
4091
|
-
const icon = versionStatusIcon(check);
|
|
4092
|
-
const label = check.label === "NordRelay" ? "NordRelay" : `${check.label} version`;
|
|
4093
|
-
return `<b>${escapeHTML(label)}:</b> ${icon} ${formatVersionCheckDetailHTML(check)}`;
|
|
4094
|
-
}
|
|
4095
|
-
function formatCliPathPlain(label, cliPath, fallback) {
|
|
4096
|
-
return cliPath ? `${label} path: ${cliPath}` : `${label}: ${fallback}`;
|
|
4097
|
-
}
|
|
4098
|
-
function formatCliPathHTML(label, cliPath, fallback) {
|
|
4099
|
-
return cliPath
|
|
4100
|
-
? `<b>${escapeHTML(label)} path:</b> <code>${escapeHTML(cliPath)}</code>`
|
|
4101
|
-
: `<b>${escapeHTML(label)}:</b> <code>${escapeHTML(fallback)}</code>`;
|
|
4102
|
-
}
|
|
4103
|
-
function formatVersionCheckDetailPlain(check) {
|
|
4104
|
-
if (check.status === "not-installed") {
|
|
4105
|
-
return "not installed";
|
|
4106
|
-
}
|
|
4107
|
-
if (check.status === "outdated") {
|
|
4108
|
-
return `${check.installedLabel} (latest ${check.latestVersion ?? "unknown"})`;
|
|
4109
|
-
}
|
|
4110
|
-
if (check.status === "current") {
|
|
4111
|
-
return `${check.installedLabel} (latest)`;
|
|
4112
|
-
}
|
|
4113
|
-
return `${check.installedLabel} (latest unknown${check.detail ? `: ${check.detail}` : ""})`;
|
|
4114
|
-
}
|
|
4115
|
-
function formatVersionCheckDetailHTML(check) {
|
|
4116
|
-
if (check.status === "not-installed") {
|
|
4117
|
-
return "<code>not installed</code>";
|
|
4118
|
-
}
|
|
4119
|
-
if (check.status === "outdated") {
|
|
4120
|
-
return `<code>${escapeHTML(check.installedLabel)}</code> <i>(latest ${escapeHTML(check.latestVersion ?? "unknown")})</i>`;
|
|
4121
|
-
}
|
|
4122
|
-
if (check.status === "current") {
|
|
4123
|
-
return `<code>${escapeHTML(check.installedLabel)}</code> <i>(latest)</i>`;
|
|
4124
|
-
}
|
|
4125
|
-
return `<code>${escapeHTML(check.installedLabel)}</code> <i>(latest unknown${check.detail ? `: ${escapeHTML(check.detail)}` : ""})</i>`;
|
|
4126
|
-
}
|
|
4127
|
-
function versionStatusIcon(check) {
|
|
4128
|
-
return check.status === "current" ? "✅" : "⚠️";
|
|
4129
|
-
}
|
|
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
|
-
function renderAuditEvents(events) {
|
|
4198
|
-
if (events.length === 0) {
|
|
4199
|
-
return {
|
|
4200
|
-
plain: "Audit log is empty.",
|
|
4201
|
-
html: escapeHTML("Audit log is empty."),
|
|
4202
|
-
};
|
|
4203
|
-
}
|
|
4204
|
-
const lines = events.map((event) => {
|
|
4205
|
-
const time = formatLocalDateTime(new Date(event.timestamp));
|
|
4206
|
-
const actor = event.actorId ? `user ${event.actorId}` : "system";
|
|
4207
|
-
const prompt = event.promptId ? ` · ${event.promptId}` : "";
|
|
4208
|
-
const detail = event.detail ? ` · ${trimLine(event.detail, 90)}` : "";
|
|
4209
|
-
const description = event.description ? ` · ${trimLine(event.description, 90)}` : "";
|
|
4210
|
-
return `${time} · ${event.status.toUpperCase()} · ${event.action} · ${actor}${prompt}${description}${detail}`;
|
|
4211
|
-
});
|
|
4212
|
-
return {
|
|
4213
|
-
plain: ["Audit:", ...lines].join("\n"),
|
|
4214
|
-
html: [
|
|
4215
|
-
"<b>Audit:</b>",
|
|
4216
|
-
...lines.map((line) => escapeHTML(line)),
|
|
4217
|
-
].join("\n"),
|
|
4218
|
-
};
|
|
4219
|
-
}
|
|
4220
|
-
function renderSessionLocks(locks) {
|
|
4221
|
-
if (locks.length === 0) {
|
|
4222
|
-
return {
|
|
4223
|
-
plain: "No active session locks.",
|
|
4224
|
-
html: escapeHTML("No active session locks."),
|
|
4225
|
-
};
|
|
4226
|
-
}
|
|
4227
|
-
const lines = locks.map((lock) => {
|
|
4228
|
-
const expires = lock.expiresAt ? ` · expires ${formatLocalDateTime(new Date(lock.expiresAt))}` : "";
|
|
4229
|
-
return `${lock.contextKey} · ${formatLockOwner(lock)}${expires}`;
|
|
4230
|
-
});
|
|
4231
|
-
return {
|
|
4232
|
-
plain: ["Session locks:", ...lines].join("\n"),
|
|
4233
|
-
html: ["<b>Session locks:</b>", ...lines.map((line) => escapeHTML(line))].join("\n"),
|
|
4234
|
-
};
|
|
4235
|
-
}
|
|
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
|
-
function formatLockOwner(lock) {
|
|
4260
|
-
if (!lock) {
|
|
4261
|
-
return "nobody";
|
|
4262
|
-
}
|
|
4263
|
-
return lock.ownerName ? `${lock.ownerName} (${lock.ownerId})` : `user ${lock.ownerId}`;
|
|
4264
|
-
}
|
|
4265
|
-
function formatTelegramName(ctx) {
|
|
4266
|
-
const firstName = ctx.from?.first_name?.trim();
|
|
4267
|
-
const lastName = ctx.from?.last_name?.trim();
|
|
4268
|
-
const username = ctx.from?.username?.trim();
|
|
4269
|
-
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim();
|
|
4270
|
-
return fullName || (username ? `@${username}` : undefined);
|
|
4271
|
-
}
|
|
4272
|
-
function formatLocalDateTime(date) {
|
|
4273
|
-
if (Number.isNaN(date.getTime())) {
|
|
4274
|
-
return "-";
|
|
4275
|
-
}
|
|
4276
|
-
return [
|
|
4277
|
-
`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`,
|
|
4278
|
-
`${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`,
|
|
4279
|
-
].join(" ");
|
|
4280
|
-
}
|
|
4281
|
-
function pad2(value) {
|
|
4282
|
-
return String(value).padStart(2, "0");
|
|
4283
|
-
}
|
|
4284
|
-
function buildArtifactActionsKeyboard(reports) {
|
|
4285
|
-
const keyboard = new InlineKeyboard();
|
|
4286
|
-
for (const [index, report] of reports.slice(0, 5).entries()) {
|
|
4287
|
-
const label = `${index + 1}`;
|
|
4288
|
-
keyboard
|
|
4289
|
-
.text(`${label} Send`, `artifact_send:${report.turnId}`)
|
|
4290
|
-
.text(`${label} ZIP`, `artifact_zip:${report.turnId}`)
|
|
4291
|
-
.text(`${label} Delete`, `artifact_delete:${report.turnId}`)
|
|
4292
|
-
.row();
|
|
4293
|
-
}
|
|
4294
|
-
return keyboard;
|
|
4295
|
-
}
|
|
4296
|
-
function filterArtifactReports(reports, argument) {
|
|
4297
|
-
const normalized = argument.trim().toLowerCase();
|
|
4298
|
-
if (!normalized) {
|
|
4299
|
-
return null;
|
|
4300
|
-
}
|
|
4301
|
-
let predicate = null;
|
|
4302
|
-
if (normalized === "images" || normalized === "image" || normalized === "photos") {
|
|
4303
|
-
predicate = (artifact) => isTelegramImagePreview(artifact);
|
|
4304
|
-
}
|
|
4305
|
-
else if (normalized === "docs" || normalized === "documents" || normalized === "files") {
|
|
4306
|
-
predicate = (artifact) => !isTelegramImagePreview(artifact);
|
|
4307
|
-
}
|
|
4308
|
-
else if (normalized.startsWith("search ")) {
|
|
4309
|
-
const query = normalized.slice("search ".length).trim();
|
|
4310
|
-
if (!query) {
|
|
4311
|
-
return [];
|
|
4312
|
-
}
|
|
4313
|
-
predicate = (artifact) => artifact.name.toLowerCase().includes(query);
|
|
4314
|
-
}
|
|
4315
|
-
if (!predicate) {
|
|
4316
|
-
return null;
|
|
4317
|
-
}
|
|
4318
|
-
return reports
|
|
4319
|
-
.map((report) => ({
|
|
4320
|
-
...report,
|
|
4321
|
-
artifacts: report.artifacts.filter(predicate),
|
|
4322
|
-
}))
|
|
4323
|
-
.filter((report) => report.artifacts.length > 0);
|
|
4324
|
-
}
|
|
4325
|
-
function renderProgressPlain(progress, queueLength, busyState, info) {
|
|
4326
|
-
const busyFlags = formatBusyFlags(busyState);
|
|
4327
|
-
if (!progress) {
|
|
4328
|
-
return [
|
|
4329
|
-
"Progress:",
|
|
4330
|
-
"Status: idle",
|
|
4331
|
-
`Thread: ${info.threadId ?? "(not started yet)"}`,
|
|
4332
|
-
`Queue: ${queueLength}`,
|
|
4333
|
-
`Busy: ${busyFlags || "no"}`,
|
|
4334
|
-
].join("\n");
|
|
4335
|
-
}
|
|
4336
|
-
const lines = [
|
|
4337
|
-
"Progress:",
|
|
4338
|
-
`Status: ${progress.status}`,
|
|
4339
|
-
`Prompt: ${progress.promptDescription}`,
|
|
4340
|
-
`Elapsed: ${formatDurationSeconds(((progress.completedAt ?? Date.now()) - progress.startedAt) / 1000)}`,
|
|
4341
|
-
`Current tool: ${progress.currentTool ?? "-"}`,
|
|
4342
|
-
`Last tool: ${progress.lastTool ?? "-"}`,
|
|
4343
|
-
`Tools: ${formatToolSummaryLine(progress.toolCounts) || "-"}`,
|
|
4344
|
-
`Output chars: ${progress.textCharacters}`,
|
|
4345
|
-
`Queue: ${queueLength}`,
|
|
4346
|
-
`Busy: ${busyFlags || "no"}`,
|
|
4347
|
-
];
|
|
4348
|
-
if (progress.error) {
|
|
4349
|
-
lines.push(`Error: ${progress.error}`);
|
|
4350
|
-
}
|
|
4351
|
-
return lines.join("\n");
|
|
4352
|
-
}
|
|
4353
|
-
function renderProgressHTML(progress, queueLength, busyState, info) {
|
|
4354
|
-
const busyFlags = formatBusyFlags(busyState);
|
|
4355
|
-
if (!progress) {
|
|
4356
|
-
return [
|
|
4357
|
-
"<b>Progress:</b>",
|
|
4358
|
-
"<b>Status:</b> <code>idle</code>",
|
|
4359
|
-
`<b>Thread:</b> <code>${escapeHTML(info.threadId ?? "(not started yet)")}</code>`,
|
|
4360
|
-
`<b>Queue:</b> <code>${queueLength}</code>`,
|
|
4361
|
-
`<b>Busy:</b> <code>${escapeHTML(busyFlags || "no")}</code>`,
|
|
4362
|
-
].join("\n");
|
|
4363
|
-
}
|
|
4364
|
-
const lines = [
|
|
4365
|
-
"<b>Progress:</b>",
|
|
4366
|
-
`<b>Status:</b> <code>${escapeHTML(progress.status)}</code>`,
|
|
4367
|
-
`<b>Prompt:</b> <code>${escapeHTML(progress.promptDescription)}</code>`,
|
|
4368
|
-
`<b>Elapsed:</b> <code>${escapeHTML(formatDurationSeconds(((progress.completedAt ?? Date.now()) - progress.startedAt) / 1000))}</code>`,
|
|
4369
|
-
`<b>Current tool:</b> <code>${escapeHTML(progress.currentTool ?? "-")}</code>`,
|
|
4370
|
-
`<b>Last tool:</b> <code>${escapeHTML(progress.lastTool ?? "-")}</code>`,
|
|
4371
|
-
`<b>Tools:</b> <code>${escapeHTML(formatToolSummaryLine(progress.toolCounts) || "-")}</code>`,
|
|
4372
|
-
`<b>Output chars:</b> <code>${progress.textCharacters}</code>`,
|
|
4373
|
-
`<b>Queue:</b> <code>${queueLength}</code>`,
|
|
4374
|
-
`<b>Busy:</b> <code>${escapeHTML(busyFlags || "no")}</code>`,
|
|
4375
|
-
];
|
|
4376
|
-
if (progress.error) {
|
|
4377
|
-
lines.push(`<b>Error:</b> <code>${escapeHTML(progress.error)}</code>`);
|
|
4378
|
-
}
|
|
4379
|
-
return lines.join("\n");
|
|
4380
|
-
}
|
|
4381
|
-
function renderExternalMirrorStatus(snapshot, queueLength) {
|
|
4382
|
-
const prompt = trimLine(snapshot.latestUserMessage ?? "-", 180);
|
|
4383
|
-
const elapsed = snapshot.activity.startedAt
|
|
4384
|
-
? formatDurationSeconds((Date.now() - snapshot.activity.startedAt.getTime()) / 1000)
|
|
4385
|
-
: "-";
|
|
4386
|
-
const lines = [
|
|
4387
|
-
`${snapshot.agentLabel} CLI task running.`,
|
|
4388
|
-
`Thread: ${snapshot.threadId}`,
|
|
4389
|
-
`Elapsed: ${elapsed}`,
|
|
4390
|
-
`Prompt: ${prompt}`,
|
|
4391
|
-
`Last tool: ${snapshot.latestToolName ?? "-"}`,
|
|
4392
|
-
`Queue: ${queueLength}`,
|
|
4393
|
-
];
|
|
4394
|
-
return {
|
|
4395
|
-
plain: lines.join("\n"),
|
|
4396
|
-
html: [
|
|
4397
|
-
`<b>${escapeHTML(snapshot.agentLabel)} CLI task running.</b>`,
|
|
4398
|
-
`<b>Thread:</b> <code>${escapeHTML(snapshot.threadId)}</code>`,
|
|
4399
|
-
`<b>Elapsed:</b> <code>${escapeHTML(elapsed)}</code>`,
|
|
4400
|
-
`<b>Prompt:</b> <code>${escapeHTML(prompt)}</code>`,
|
|
4401
|
-
`<b>Last tool:</b> <code>${escapeHTML(snapshot.latestToolName ?? "-")}</code>`,
|
|
4402
|
-
`<b>Queue:</b> <code>${queueLength}</code>`,
|
|
4403
|
-
].join("\n"),
|
|
4404
|
-
};
|
|
4405
|
-
}
|
|
4406
|
-
function renderExternalMirrorEvent(event) {
|
|
4407
|
-
if (event.kind === "task") {
|
|
4408
|
-
const status = event.status ?? event.type;
|
|
4409
|
-
const plain = `CLI task: ${status}`;
|
|
4410
|
-
return {
|
|
4411
|
-
plain,
|
|
4412
|
-
html: `<b>CLI task:</b> <code>${escapeHTML(status)}</code>`,
|
|
4413
|
-
};
|
|
4414
|
-
}
|
|
4415
|
-
if (event.kind !== "tool") {
|
|
4416
|
-
return null;
|
|
4417
|
-
}
|
|
4418
|
-
const status = event.status ?? event.type;
|
|
4419
|
-
const tool = event.toolName ?? "tool";
|
|
4420
|
-
const detail = event.text ? `\n${trimLine(event.text.replace(/\s+/g, " "), 180)}` : "";
|
|
4421
|
-
const plain = `CLI tool ${status}: ${tool}${detail}`;
|
|
4422
|
-
return {
|
|
4423
|
-
plain,
|
|
4424
|
-
html: `<b>CLI tool ${escapeHTML(status)}:</b> <code>${escapeHTML(tool)}</code>${detail ? `\n<code>${escapeHTML(detail.trim())}</code>` : ""}`,
|
|
4425
|
-
};
|
|
4426
|
-
}
|
|
4427
|
-
function renderActivityTimeline(threadId, events, options = { limit: 16, filter: "all", exportFile: false }) {
|
|
4428
|
-
if (events.length === 0) {
|
|
4429
|
-
return {
|
|
4430
|
-
plain: `Activity:\nThread: ${threadId}\nFilter: ${options.filter}\nNo activity events found.`,
|
|
4431
|
-
html: `<b>Activity:</b>\n<b>Thread:</b> <code>${escapeHTML(threadId)}</code>\n<b>Filter:</b> <code>${escapeHTML(options.filter)}</code>\n<code>No activity events found.</code>`,
|
|
4432
|
-
};
|
|
4433
|
-
}
|
|
4434
|
-
const lines = events.map((event) => {
|
|
4435
|
-
const time = event.timestamp ? event.timestamp.toISOString().slice(11, 19) : "--:--:--";
|
|
4436
|
-
const label = activityEventLabel(event);
|
|
4437
|
-
const detail = event.text ? ` · ${trimLine(event.text.replace(/\s+/g, " ").trim(), 120)}` : "";
|
|
4438
|
-
const tool = event.toolName ? ` · ${event.toolName}` : "";
|
|
4439
|
-
return `${time} · ${label}${tool}${detail}`;
|
|
4440
|
-
});
|
|
4441
|
-
return {
|
|
4442
|
-
plain: ["Activity:", `Thread: ${threadId}`, `Filter: ${options.filter}`, `Events: ${events.length}`, ...lines].join("\n"),
|
|
4443
|
-
html: [
|
|
4444
|
-
"<b>Activity:</b>",
|
|
4445
|
-
`<b>Thread:</b> <code>${escapeHTML(threadId)}</code>`,
|
|
4446
|
-
`<b>Filter:</b> <code>${escapeHTML(options.filter)}</code>`,
|
|
4447
|
-
`<b>Events:</b> <code>${events.length}</code>`,
|
|
4448
|
-
...lines.map((line) => `<code>${escapeHTML(line)}</code>`),
|
|
4449
|
-
].join("\n"),
|
|
4450
|
-
};
|
|
4451
|
-
}
|
|
4452
|
-
function parseActivityOptions(argument) {
|
|
4453
|
-
const options = {
|
|
4454
|
-
limit: 16,
|
|
4455
|
-
filter: "all",
|
|
4456
|
-
exportFile: false,
|
|
4457
|
-
};
|
|
4458
|
-
const parts = argument.split(/\s+/).filter(Boolean);
|
|
4459
|
-
for (let index = 0; index < parts.length; index += 1) {
|
|
4460
|
-
const part = parts[index].toLowerCase();
|
|
4461
|
-
if (/^\d+$/.test(part)) {
|
|
4462
|
-
options.limit = Math.min(200, Math.max(1, Number(part)));
|
|
4463
|
-
continue;
|
|
4464
|
-
}
|
|
4465
|
-
if (part === "export") {
|
|
4466
|
-
options.exportFile = true;
|
|
4467
|
-
continue;
|
|
4468
|
-
}
|
|
4469
|
-
if (isActivityFilter(part)) {
|
|
4470
|
-
options.filter = part;
|
|
4471
|
-
continue;
|
|
4472
|
-
}
|
|
4473
|
-
if (part === "since" && parts[index + 1]) {
|
|
4474
|
-
options.sinceMs = parseDurationToMs(parts[index + 1]);
|
|
4475
|
-
index += 1;
|
|
4476
|
-
}
|
|
4477
|
-
}
|
|
4478
|
-
return options;
|
|
4479
|
-
}
|
|
4480
|
-
function filterActivityEvents(events, options) {
|
|
4481
|
-
const cutoff = options.sinceMs ? Date.now() - options.sinceMs : undefined;
|
|
4482
|
-
return events
|
|
4483
|
-
.filter((event) => {
|
|
4484
|
-
if (cutoff && event.timestamp && event.timestamp.getTime() < cutoff) {
|
|
4485
|
-
return false;
|
|
4486
|
-
}
|
|
4487
|
-
switch (options.filter) {
|
|
4488
|
-
case "tools":
|
|
4489
|
-
return event.kind === "tool";
|
|
4490
|
-
case "errors":
|
|
4491
|
-
return event.status === "failed" || event.status === "error" || /error|failed/i.test(event.text ?? "");
|
|
4492
|
-
case "user":
|
|
4493
|
-
return event.kind === "user";
|
|
4494
|
-
case "agent":
|
|
4495
|
-
return event.kind === "agent";
|
|
4496
|
-
case "tasks":
|
|
4497
|
-
return event.kind === "task";
|
|
4498
|
-
default:
|
|
4499
|
-
return true;
|
|
4500
|
-
}
|
|
4501
|
-
})
|
|
4502
|
-
.slice(-options.limit);
|
|
4503
|
-
}
|
|
4504
|
-
function isActivityFilter(value) {
|
|
4505
|
-
return value === "all" || value === "tools" || value === "errors" || value === "user" || value === "agent" || value === "tasks";
|
|
4506
|
-
}
|
|
4507
|
-
function formatAgentLaunchProfileLabel(profile, selected) {
|
|
4508
|
-
const prefix = selected ? "✅" : profile.unsafe ? "⚠️" : "🚀";
|
|
4509
|
-
return `${prefix} ${profile.label} · ${trimLine(profile.behavior, 24)}`;
|
|
4510
|
-
}
|
|
4511
|
-
function formatModelButtonLabel(model, selected) {
|
|
4512
|
-
const meta = [
|
|
4513
|
-
model.contextWindow ? formatCompactNumber(model.contextWindow) : undefined,
|
|
4514
|
-
model.supportsImages === true ? "img" : model.supportsImages === false ? "text" : undefined,
|
|
4515
|
-
model.supportsThinking === true ? "think" : undefined,
|
|
4516
|
-
].filter(Boolean).join(" ");
|
|
4517
|
-
return trimLine(`${selected ? "✅ " : ""}${model.displayName}${meta ? ` · ${meta}` : ""}`, 58);
|
|
4518
|
-
}
|
|
4519
|
-
function formatCompactNumber(value) {
|
|
4520
|
-
if (value >= 1_000_000_000)
|
|
4521
|
-
return `${Math.round(value / 100_000_000) / 10}B`;
|
|
4522
|
-
if (value >= 1_000_000)
|
|
4523
|
-
return `${Math.round(value / 100_000) / 10}M`;
|
|
4524
|
-
if (value >= 1_000)
|
|
4525
|
-
return `${Math.round(value / 100) / 10}K`;
|
|
4526
|
-
return String(value);
|
|
4527
|
-
}
|
|
4528
|
-
function renderAgentDiagnostics(diagnostics) {
|
|
4529
|
-
return {
|
|
4530
|
-
plain: [
|
|
4531
|
-
`${diagnostics.agentLabel} state:`,
|
|
4532
|
-
...diagnostics.lines.map((line) => `${line.label}: ${line.value}`),
|
|
4533
|
-
].join("\n"),
|
|
4534
|
-
html: [
|
|
4535
|
-
`<b>${escapeHTML(diagnostics.agentLabel)} state:</b>`,
|
|
4536
|
-
...diagnostics.lines.map((line) => `<b>${escapeHTML(line.label)}:</b> <code>${escapeHTML(line.value)}</code>`),
|
|
4537
|
-
].join("\n"),
|
|
4538
|
-
};
|
|
4539
|
-
}
|
|
4540
|
-
function activityEventLabel(event) {
|
|
4541
|
-
if (event.kind === "task") {
|
|
4542
|
-
return `task ${event.status ?? event.type}`;
|
|
4543
|
-
}
|
|
4544
|
-
if (event.kind === "user") {
|
|
4545
|
-
return "user";
|
|
4546
|
-
}
|
|
4547
|
-
if (event.kind === "agent") {
|
|
4548
|
-
return event.phase ? `agent ${event.phase}` : "agent";
|
|
4549
|
-
}
|
|
4550
|
-
return event.status ? `tool ${event.status}` : "tool";
|
|
4551
|
-
}
|
|
4552
|
-
function isEmptyArtifactReport(report) {
|
|
4553
|
-
return report.artifacts.length === 0 && report.skippedCount === 0 && !(report.omittedCount && report.omittedCount > 0);
|
|
4554
|
-
}
|
|
4555
|
-
function formatBusyFlags(state) {
|
|
4556
|
-
return Object.entries(state)
|
|
4557
|
-
.filter(([, enabled]) => enabled)
|
|
4558
|
-
.map(([name]) => name)
|
|
4559
|
-
.join(", ");
|
|
4560
|
-
}
|
|
4561
|
-
function renderDiagnosticsPlain(config, registry, health, authenticated, role, queueLength, progress, runtime) {
|
|
4562
|
-
const contexts = registry.listContexts();
|
|
4563
|
-
return [
|
|
4564
|
-
"Diagnostics:",
|
|
4565
|
-
`Status: ${health.state.status ?? "unknown"}`,
|
|
4566
|
-
`Version: ${health.version}`,
|
|
4567
|
-
`Role: ${role}`,
|
|
4568
|
-
`Auth: ${authenticated ? "yes" : "no"} (${health.state.authMethod ?? "-"})`,
|
|
4569
|
-
`PID: ${health.state.pid ?? "-"} (${health.pidRunning ? "running" : "not running"})`,
|
|
4570
|
-
`App PID: ${health.state.appPid ?? "-"} (${health.appPidRunning ? "running" : "not running"})`,
|
|
4571
|
-
`Workspace: ${config.workspace}`,
|
|
4572
|
-
`State backend: ${config.stateBackend}`,
|
|
4573
|
-
`Telegram transport: ${config.telegramTransport}`,
|
|
4574
|
-
`Codex CLI: ${health.codexCli}`,
|
|
4575
|
-
`Pi CLI: ${health.piCli}`,
|
|
4576
|
-
`Hermes CLI: ${health.hermesCli}`,
|
|
4577
|
-
`OpenClaw CLI: ${health.openClawCli}`,
|
|
4578
|
-
`Claude Code CLI: ${health.claudeCodeCli}`,
|
|
4579
|
-
`Hermes API: ${config.hermesApiBaseUrl}`,
|
|
4580
|
-
`OpenClaw Gateway: ${config.openClawGatewayUrl}`,
|
|
4581
|
-
`Enabled agents/default: ${enabledAgents(config).join(", ")} / ${config.defaultAgent}`,
|
|
4582
|
-
`State DB: ${health.databasePath ?? "-"}`,
|
|
4583
|
-
`Log file: ${health.logFile}`,
|
|
4584
|
-
`Log format: ${config.logFormat}`,
|
|
4585
|
-
`Tool verbosity: ${config.toolVerbosity}`,
|
|
4586
|
-
`Telegram rate limit queued/running/retries/429: ${runtime.rateLimit.queued}/${runtime.rateLimit.running}/${runtime.rateLimit.retries}/${runtime.rateLimit.rateLimitHits}`,
|
|
4587
|
-
`Telegram last retry_after: ${runtime.rateLimit.lastRetryAfterSeconds ?? "-"}s`,
|
|
4588
|
-
`CLI mirror mode/update: ${runtime.mirrorMode} / ${config.telegramMirrorMinUpdateMs} ms`,
|
|
4589
|
-
`Notify/quiet: ${runtime.notifyMode} / ${runtime.quietHours}`,
|
|
4590
|
-
`Voice: ${runtime.voiceBackend} / ${runtime.voiceLanguage} / transcribe-only ${runtime.voiceTranscribeOnly ? "on" : "off"}`,
|
|
4591
|
-
`Sync interval: ${config.codexSyncIntervalMs} ms`,
|
|
4592
|
-
`External busy check/stale: ${config.codexExternalBusyCheckMs} ms / ${config.codexExternalBusyStaleMs} ms`,
|
|
4593
|
-
`External mirrors/timers/status messages: ${runtime.externalMirrors}/${runtime.externalQueueTimers}/${runtime.queueStatusMessages}`,
|
|
4594
|
-
`Auto-send artifacts: ${config.telegramAutoSendArtifacts ? "yes" : "no"}`,
|
|
4595
|
-
`Artifact ignore dirs/globs: ${config.artifactIgnoreDirs.length}/${config.artifactIgnoreGlobs.length}`,
|
|
4596
|
-
`Artifact retention: ${config.artifactRetentionDays}d / ${config.artifactMaxTurnDirs} turns / ${config.artifactMaxInboxDirs} inbox dirs`,
|
|
4597
|
-
`Workspace allowed/warn roots: ${config.workspaceAllowedRoots.length}/${config.workspaceWarnRoots.length}`,
|
|
4598
|
-
`Allowed users/chats/admins/readonly: ${config.telegramAllowedUserIds.length}/${config.telegramAllowedChatIds.length}/${config.telegramAdminUserIds.length}/${config.telegramReadOnlyUserIds.length}`,
|
|
4599
|
-
`Session lock TTL: ${config.sessionLockTtlMs} ms`,
|
|
4600
|
-
`Audit max events: ${config.auditMaxEvents}`,
|
|
4601
|
-
`Loaded sessions: ${contexts.length}`,
|
|
4602
|
-
`Current queue: ${queueLength}`,
|
|
4603
|
-
`Current progress: ${progress?.status ?? "idle"}`,
|
|
4604
|
-
].join("\n");
|
|
4605
|
-
}
|
|
4606
|
-
function renderDiagnosticsHTML(config, registry, health, authenticated, role, queueLength, progress, runtime) {
|
|
4607
|
-
const contexts = registry.listContexts();
|
|
4608
|
-
return [
|
|
4609
|
-
"<b>Diagnostics:</b>",
|
|
4610
|
-
`<b>Status:</b> <code>${escapeHTML(health.state.status ?? "unknown")}</code>`,
|
|
4611
|
-
`<b>Version:</b> <code>${escapeHTML(health.version)}</code>`,
|
|
4612
|
-
`<b>Role:</b> <code>${escapeHTML(role)}</code>`,
|
|
4613
|
-
`<b>Auth:</b> <code>${authenticated ? "yes" : "no"} (${escapeHTML(health.state.authMethod ?? "-")})</code>`,
|
|
4614
|
-
`<b>PID:</b> <code>${escapeHTML(String(health.state.pid ?? "-"))} (${health.pidRunning ? "running" : "not running"})</code>`,
|
|
4615
|
-
`<b>App PID:</b> <code>${escapeHTML(String(health.state.appPid ?? "-"))} (${health.appPidRunning ? "running" : "not running"})</code>`,
|
|
4616
|
-
`<b>Workspace:</b> <code>${escapeHTML(config.workspace)}</code>`,
|
|
4617
|
-
`<b>State backend:</b> <code>${escapeHTML(config.stateBackend)}</code>`,
|
|
4618
|
-
`<b>Telegram transport:</b> <code>${escapeHTML(config.telegramTransport)}</code>`,
|
|
4619
|
-
`<b>Codex CLI:</b> <code>${escapeHTML(health.codexCli)}</code>`,
|
|
4620
|
-
`<b>Pi CLI:</b> <code>${escapeHTML(health.piCli)}</code>`,
|
|
4621
|
-
`<b>Hermes CLI:</b> <code>${escapeHTML(health.hermesCli)}</code>`,
|
|
4622
|
-
`<b>OpenClaw CLI:</b> <code>${escapeHTML(health.openClawCli)}</code>`,
|
|
4623
|
-
`<b>Claude Code CLI:</b> <code>${escapeHTML(health.claudeCodeCli)}</code>`,
|
|
4624
|
-
`<b>Hermes API:</b> <code>${escapeHTML(config.hermesApiBaseUrl)}</code>`,
|
|
4625
|
-
`<b>OpenClaw Gateway:</b> <code>${escapeHTML(config.openClawGatewayUrl)}</code>`,
|
|
4626
|
-
`<b>Enabled agents/default:</b> <code>${escapeHTML(`${enabledAgents(config).join(", ")} / ${config.defaultAgent}`)}</code>`,
|
|
4627
|
-
`<b>State DB:</b> <code>${escapeHTML(health.databasePath ?? "-")}</code>`,
|
|
4628
|
-
`<b>Log file:</b> <code>${escapeHTML(health.logFile)}</code>`,
|
|
4629
|
-
`<b>Log format:</b> <code>${escapeHTML(config.logFormat)}</code>`,
|
|
4630
|
-
`<b>Tool verbosity:</b> <code>${escapeHTML(config.toolVerbosity)}</code>`,
|
|
4631
|
-
`<b>Telegram rate limit queued/running/retries/429:</b> <code>${runtime.rateLimit.queued}/${runtime.rateLimit.running}/${runtime.rateLimit.retries}/${runtime.rateLimit.rateLimitHits}</code>`,
|
|
4632
|
-
`<b>Telegram last retry_after:</b> <code>${escapeHTML(String(runtime.rateLimit.lastRetryAfterSeconds ?? "-"))}s</code>`,
|
|
4633
|
-
`<b>CLI mirror mode/update:</b> <code>${escapeHTML(runtime.mirrorMode)} / ${config.telegramMirrorMinUpdateMs} ms</code>`,
|
|
4634
|
-
`<b>Notify/quiet:</b> <code>${escapeHTML(runtime.notifyMode)} / ${escapeHTML(runtime.quietHours)}</code>`,
|
|
4635
|
-
`<b>Voice:</b> <code>${escapeHTML(runtime.voiceBackend)} / ${escapeHTML(runtime.voiceLanguage)} / transcribe-only ${runtime.voiceTranscribeOnly ? "on" : "off"}</code>`,
|
|
4636
|
-
`<b>Sync interval:</b> <code>${config.codexSyncIntervalMs} ms</code>`,
|
|
4637
|
-
`<b>External busy check/stale:</b> <code>${config.codexExternalBusyCheckMs} ms / ${config.codexExternalBusyStaleMs} ms</code>`,
|
|
4638
|
-
`<b>External mirrors/timers/status messages:</b> <code>${runtime.externalMirrors}/${runtime.externalQueueTimers}/${runtime.queueStatusMessages}</code>`,
|
|
4639
|
-
`<b>Auto-send artifacts:</b> <code>${config.telegramAutoSendArtifacts ? "yes" : "no"}</code>`,
|
|
4640
|
-
`<b>Artifact ignore dirs/globs:</b> <code>${config.artifactIgnoreDirs.length}/${config.artifactIgnoreGlobs.length}</code>`,
|
|
4641
|
-
`<b>Artifact retention:</b> <code>${config.artifactRetentionDays}d / ${config.artifactMaxTurnDirs} turns / ${config.artifactMaxInboxDirs} inbox dirs</code>`,
|
|
4642
|
-
`<b>Workspace allowed/warn roots:</b> <code>${config.workspaceAllowedRoots.length}/${config.workspaceWarnRoots.length}</code>`,
|
|
4643
|
-
`<b>Allowed users/chats/admins/readonly:</b> <code>${config.telegramAllowedUserIds.length}/${config.telegramAllowedChatIds.length}/${config.telegramAdminUserIds.length}/${config.telegramReadOnlyUserIds.length}</code>`,
|
|
4644
|
-
`<b>Session lock TTL:</b> <code>${config.sessionLockTtlMs} ms</code>`,
|
|
4645
|
-
`<b>Audit max events:</b> <code>${config.auditMaxEvents}</code>`,
|
|
4646
|
-
`<b>Loaded sessions:</b> <code>${contexts.length}</code>`,
|
|
4647
|
-
`<b>Current queue:</b> <code>${queueLength}</code>`,
|
|
4648
|
-
`<b>Current progress:</b> <code>${escapeHTML(progress?.status ?? "idle")}</code>`,
|
|
4649
|
-
].join("\n");
|
|
4650
|
-
}
|
|
4651
|
-
function renderHealthPlain(health, authenticated, role) {
|
|
4652
|
-
return [
|
|
4653
|
-
`Status: ${health.state.status ?? "unknown"}`,
|
|
4654
|
-
`Version: ${health.version}`,
|
|
4655
|
-
`Role: ${role}`,
|
|
4656
|
-
`Auth: ${authenticated ? "yes" : "no"}`,
|
|
4657
|
-
`PID: ${health.state.pid ?? "-"} (${health.pidRunning ? "running" : "not running"})`,
|
|
4658
|
-
`App PID: ${health.state.appPid ?? "-"} (${health.appPidRunning ? "running" : "not running"})`,
|
|
4659
|
-
`Uptime: ${formatDuration(health.uptimeSeconds)}`,
|
|
4660
|
-
`Workspace: ${health.state.workspace ?? "-"}`,
|
|
4661
|
-
`Codex CLI: ${health.codexCli}`,
|
|
4662
|
-
`Pi CLI: ${health.piCli}`,
|
|
4663
|
-
`Hermes CLI: ${health.hermesCli}`,
|
|
4664
|
-
`OpenClaw CLI: ${health.openClawCli}`,
|
|
4665
|
-
`Claude Code CLI: ${health.claudeCodeCli}`,
|
|
4666
|
-
`Codex state DB: ${health.databasePath ?? "-"}`,
|
|
4667
|
-
`Log: ${health.logFile}`,
|
|
4668
|
-
].join("\n");
|
|
4669
|
-
}
|
|
4670
|
-
function renderHealthHTML(health, authenticated, role) {
|
|
4671
|
-
return [
|
|
4672
|
-
`<b>Status:</b> <code>${escapeHTML(health.state.status ?? "unknown")}</code>`,
|
|
4673
|
-
`<b>Version:</b> <code>${escapeHTML(health.version)}</code>`,
|
|
4674
|
-
`<b>Role:</b> <code>${escapeHTML(role)}</code>`,
|
|
4675
|
-
`<b>Auth:</b> <code>${authenticated ? "yes" : "no"}</code>`,
|
|
4676
|
-
`<b>PID:</b> <code>${escapeHTML(String(health.state.pid ?? "-"))} (${health.pidRunning ? "running" : "not running"})</code>`,
|
|
4677
|
-
`<b>App PID:</b> <code>${escapeHTML(String(health.state.appPid ?? "-"))} (${health.appPidRunning ? "running" : "not running"})</code>`,
|
|
4678
|
-
`<b>Uptime:</b> <code>${escapeHTML(formatDuration(health.uptimeSeconds))}</code>`,
|
|
4679
|
-
`<b>Workspace:</b> <code>${escapeHTML(health.state.workspace ?? "-")}</code>`,
|
|
4680
|
-
`<b>Codex CLI:</b> <code>${escapeHTML(health.codexCli)}</code>`,
|
|
4681
|
-
`<b>Pi CLI:</b> <code>${escapeHTML(health.piCli)}</code>`,
|
|
4682
|
-
`<b>Hermes CLI:</b> <code>${escapeHTML(health.hermesCli)}</code>`,
|
|
4683
|
-
`<b>OpenClaw CLI:</b> <code>${escapeHTML(health.openClawCli)}</code>`,
|
|
4684
|
-
`<b>Claude Code CLI:</b> <code>${escapeHTML(health.claudeCodeCli)}</code>`,
|
|
4685
|
-
`<b>Codex state DB:</b> <code>${escapeHTML(health.databasePath ?? "-")}</code>`,
|
|
4686
|
-
`<b>Log:</b> <code>${escapeHTML(health.logFile)}</code>`,
|
|
4687
|
-
].join("\n");
|
|
4688
|
-
}
|
|
4689
|
-
function parseFastModeArgument(argument, currentValue) {
|
|
4690
|
-
if (!argument) {
|
|
4691
|
-
return !currentValue;
|
|
4692
|
-
}
|
|
4693
|
-
const normalized = argument.toLowerCase();
|
|
4694
|
-
if (["on", "enable", "enabled", "true", "1"].includes(normalized)) {
|
|
4695
|
-
return true;
|
|
4696
|
-
}
|
|
4697
|
-
if (["off", "disable", "disabled", "false", "0"].includes(normalized)) {
|
|
4698
|
-
return false;
|
|
4699
|
-
}
|
|
4700
|
-
return undefined;
|
|
4701
|
-
}
|
|
4702
|
-
function parseToggle(argument) {
|
|
4703
|
-
const normalized = argument.trim().toLowerCase();
|
|
4704
|
-
if (["on", "enable", "enabled", "true", "1", "yes"].includes(normalized)) {
|
|
4705
|
-
return true;
|
|
4706
|
-
}
|
|
4707
|
-
if (["off", "disable", "disabled", "false", "0", "no"].includes(normalized)) {
|
|
4708
|
-
return false;
|
|
4709
|
-
}
|
|
4710
|
-
return undefined;
|
|
4711
|
-
}
|
|
4712
|
-
function parseDurationToMs(value) {
|
|
4713
|
-
const match = value.trim().match(/^(\d+)(s|m|h|d)?$/i);
|
|
4714
|
-
if (!match) {
|
|
4715
|
-
return undefined;
|
|
4716
|
-
}
|
|
4717
|
-
const amount = Number(match[1]);
|
|
4718
|
-
const unit = (match[2] ?? "m").toLowerCase();
|
|
4719
|
-
const multiplier = unit === "s"
|
|
4720
|
-
? 1000
|
|
4721
|
-
: unit === "h"
|
|
4722
|
-
? 60 * 60 * 1000
|
|
4723
|
-
: unit === "d"
|
|
4724
|
-
? 24 * 60 * 60 * 1000
|
|
4725
|
-
: 60 * 1000;
|
|
4726
|
-
return amount * multiplier;
|
|
4727
|
-
}
|
|
4728
|
-
function extractCommandName(text) {
|
|
4729
|
-
const match = text.trim().match(/^\/([a-zA-Z0-9_-]+)(?:@\w+)?(?:\s|$)/);
|
|
4730
|
-
return match?.[1]?.toLowerCase();
|
|
4731
|
-
}
|
|
4732
|
-
function isPromptEnvelopeLike(value) {
|
|
4733
|
-
return typeof value === "object" && value !== null && "input" in value && "description" in value;
|
|
4734
|
-
}
|
|
4735
|
-
function isQueuedPromptLike(value) {
|
|
4736
|
-
return "id" in value &&
|
|
4737
|
-
"contextKey" in value &&
|
|
4738
|
-
"createdAt" in value &&
|
|
4739
|
-
typeof value.id === "string" &&
|
|
4740
|
-
typeof value.contextKey === "string" &&
|
|
4741
|
-
typeof value.createdAt === "number";
|
|
4742
|
-
}
|
|
4743
|
-
function capabilitiesOf(info) {
|
|
4744
|
-
return info.capabilities ?? CODEX_AGENT_CAPABILITIES;
|
|
4745
|
-
}
|
|
4746
|
-
function labelOf(info) {
|
|
4747
|
-
return info.agentLabel ?? agentLabel(info.agentId ?? "codex");
|
|
4748
|
-
}
|
|
4749
|
-
function idOf(info) {
|
|
4750
|
-
return info.agentId ?? "codex";
|
|
4751
|
-
}
|
|
4752
|
-
function authHelpText(info) {
|
|
4753
|
-
const agentId = idOf(info);
|
|
4754
|
-
if (agentId === "pi") {
|
|
4755
|
-
return "Configure the required Pi provider environment variable on the host.";
|
|
4756
|
-
}
|
|
4757
|
-
if (agentId === "hermes") {
|
|
4758
|
-
return "Start the Hermes API Server, configure HERMES_API_KEY when required, or use /login to start Hermes CLI auth.";
|
|
4759
|
-
}
|
|
4760
|
-
if (agentId === "openclaw") {
|
|
4761
|
-
return "Start the OpenClaw Gateway and configure OPENCLAW_GATEWAY_TOKEN or OPENCLAW_GATEWAY_PASSWORD when the gateway requires one.";
|
|
4762
|
-
}
|
|
4763
|
-
if (agentId === "claude-code") {
|
|
4764
|
-
return "Use /login to start Claude Code CLI auth, or run 'claude auth login' on the host.";
|
|
4765
|
-
}
|
|
4766
|
-
return "Use /login to start authentication, or set CODEX_API_KEY on the host.";
|
|
4767
|
-
}
|
|
4768
|
-
function formatAgentSettingScope(info, appliedToActiveThread) {
|
|
4769
|
-
const agentId = idOf(info);
|
|
4770
|
-
if (agentId === "hermes") {
|
|
4771
|
-
return appliedToActiveThread
|
|
4772
|
-
? "applies to the next Hermes run in this session"
|
|
4773
|
-
: "applies to new Hermes sessions";
|
|
4774
|
-
}
|
|
4775
|
-
if (agentId === "pi") {
|
|
4776
|
-
return appliedToActiveThread
|
|
4777
|
-
? "applied to the current idle Pi session and future turns"
|
|
4778
|
-
: "applies to new Pi sessions";
|
|
4779
|
-
}
|
|
4780
|
-
if (agentId === "openclaw") {
|
|
4781
|
-
return appliedToActiveThread
|
|
4782
|
-
? "applies to the next OpenClaw run in this session"
|
|
4783
|
-
: "applies to new OpenClaw sessions";
|
|
4784
|
-
}
|
|
4785
|
-
if (agentId === "claude-code") {
|
|
4786
|
-
return appliedToActiveThread
|
|
4787
|
-
? "applies to the next Claude Code run in this session"
|
|
4788
|
-
: "applies to new Claude Code sessions";
|
|
4789
|
-
}
|
|
4790
|
-
return appliedToActiveThread
|
|
4791
|
-
? "applied to the current idle thread and future threads"
|
|
4792
|
-
: "applies to new threads";
|
|
4793
|
-
}
|
|
4794
|
-
function requiresTurnApproval(info) {
|
|
4795
|
-
return info.unsafeLaunch || info.approvalPolicy !== "never";
|
|
4796
|
-
}
|
|
4797
|
-
function formatDuration(totalSeconds) {
|
|
4798
|
-
const seconds = Math.max(0, Math.floor(totalSeconds));
|
|
4799
|
-
const days = Math.floor(seconds / 86400);
|
|
4800
|
-
const hours = Math.floor((seconds % 86400) / 3600);
|
|
4801
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
4802
|
-
if (days > 0) {
|
|
4803
|
-
return `${days}d ${hours}h`;
|
|
4804
|
-
}
|
|
4805
|
-
if (hours > 0) {
|
|
4806
|
-
return `${hours}h ${minutes}m`;
|
|
4807
|
-
}
|
|
4808
|
-
return `${minutes}m`;
|
|
4809
|
-
}
|
|
4810
|
-
function formatDurationSeconds(totalSeconds) {
|
|
4811
|
-
const seconds = Math.max(0, Math.floor(totalSeconds));
|
|
4812
|
-
if (seconds < 60) {
|
|
4813
|
-
return `${seconds}s`;
|
|
4814
|
-
}
|
|
4815
|
-
const minutes = Math.floor(seconds / 60);
|
|
4816
|
-
const remainingSeconds = seconds % 60;
|
|
4817
|
-
if (minutes < 60) {
|
|
4818
|
-
return `${minutes}m ${remainingSeconds}s`;
|
|
4819
|
-
}
|
|
4820
|
-
const hours = Math.floor(minutes / 60);
|
|
4821
|
-
return `${hours}h ${minutes % 60}m`;
|
|
4822
|
-
}
|
|
4823
|
-
function renderToolStartMessage(toolName) {
|
|
4824
|
-
return {
|
|
4825
|
-
text: `<b>🔧 Running:</b> <code>${escapeHTML(toolName)}</code>`,
|
|
4826
|
-
fallbackText: `🔧 Running: ${toolName}`,
|
|
4827
|
-
parseMode: "HTML",
|
|
4828
|
-
};
|
|
4829
|
-
}
|
|
4830
|
-
function renderToolEndMessage(toolName, partialResult, isError) {
|
|
4831
|
-
const preview = summarizeToolOutput(partialResult);
|
|
4832
|
-
const icon = isError ? "❌" : "✅";
|
|
4833
|
-
const htmlLines = [`<b>${icon}</b> <code>${escapeHTML(toolName)}</code>`];
|
|
4834
|
-
const plainLines = [`${icon} ${toolName}`];
|
|
4835
|
-
if (preview) {
|
|
4836
|
-
htmlLines.push(`<pre>${escapeHTML(preview)}</pre>`);
|
|
4837
|
-
plainLines.push(preview);
|
|
4838
|
-
}
|
|
4839
|
-
return {
|
|
4840
|
-
text: htmlLines.join("\n"),
|
|
4841
|
-
fallbackText: plainLines.join("\n"),
|
|
4842
|
-
parseMode: "HTML",
|
|
4843
|
-
};
|
|
4844
|
-
}
|
|
4845
|
-
export function formatToolSummaryLine(toolCounts) {
|
|
4846
|
-
if (toolCounts.size === 0) {
|
|
4847
|
-
return "";
|
|
4848
|
-
}
|
|
4849
|
-
const summarizedCounts = new Map();
|
|
4850
|
-
for (const [toolName, count] of toolCounts.entries()) {
|
|
4851
|
-
const summaryName = summarizeToolName(toolName);
|
|
4852
|
-
summarizedCounts.set(summaryName, (summarizedCounts.get(summaryName) ?? 0) + count);
|
|
4853
|
-
}
|
|
4854
|
-
const entries = [...summarizedCounts.entries()].sort((left, right) => {
|
|
4855
|
-
const countDelta = right[1] - left[1];
|
|
4856
|
-
return countDelta !== 0 ? countDelta : left[0].localeCompare(right[0]);
|
|
4857
|
-
});
|
|
4858
|
-
const tools = entries
|
|
4859
|
-
.map(([name, count]) => formatSummaryEntry(name, count))
|
|
4860
|
-
.join(", ");
|
|
4861
|
-
return `Tools used: ${tools}`;
|
|
4862
|
-
}
|
|
4863
|
-
function renderTodoList(items) {
|
|
4864
|
-
const lines = items.map((item) => {
|
|
4865
|
-
const icon = item.completed ? "✅" : "⬜";
|
|
4866
|
-
return `${icon} ${escapeHTML(item.text)}`;
|
|
4867
|
-
});
|
|
4868
|
-
return `📋 <b>Plan</b>\n${lines.join("\n")}`;
|
|
4869
|
-
}
|
|
4870
|
-
export function formatTurnUsageLine(usage) {
|
|
4871
|
-
return `🪙 in: ${usage.inputTokens} · cached: ${usage.cachedInputTokens} · out: ${usage.outputTokens}`;
|
|
4872
|
-
}
|
|
4873
|
-
export function summarizeToolName(toolName) {
|
|
4874
|
-
if (toolName.startsWith("🔍 ")) {
|
|
4875
|
-
return "web_fetch";
|
|
4876
|
-
}
|
|
4877
|
-
if (toolName === "file_change") {
|
|
4878
|
-
return "file_change";
|
|
4879
|
-
}
|
|
4880
|
-
if (toolName === "⚠️ error") {
|
|
4881
|
-
return "error";
|
|
4882
|
-
}
|
|
4883
|
-
if (toolName.startsWith("mcp:")) {
|
|
4884
|
-
const tool = toolName.split("/").at(-1) ?? toolName;
|
|
4885
|
-
if (SUBAGENT_TOOL_NAMES.has(tool)) {
|
|
4886
|
-
return "subagent";
|
|
4887
|
-
}
|
|
4888
|
-
return tool;
|
|
4889
|
-
}
|
|
4890
|
-
return "bash";
|
|
4891
|
-
}
|
|
4892
|
-
function formatSummaryEntry(name, count) {
|
|
4893
|
-
if (count <= 1) {
|
|
4894
|
-
return name;
|
|
4895
|
-
}
|
|
4896
|
-
const label = name === "subagent" ? "subagents" : name;
|
|
4897
|
-
return `${count}x ${label}`;
|
|
4898
|
-
}
|
|
4899
|
-
const SUBAGENT_TOOL_NAMES = new Set(["spawn_agent", "send_input", "wait_agent", "close_agent", "resume_agent"]);
|
|
4900
|
-
async function safeReply(ctx, text, options = {}) {
|
|
4901
|
-
const chatId = ctx.chat?.id;
|
|
4902
|
-
if (!chatId) {
|
|
4903
|
-
return;
|
|
4904
|
-
}
|
|
4905
|
-
const parseMode = options.parseMode !== undefined ? options.parseMode : "HTML";
|
|
4906
|
-
const messageThreadId = options.messageThreadId ?? ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id;
|
|
4907
|
-
const chunks = splitTelegramText(redactText(text));
|
|
4908
|
-
const fallbackChunks = options.fallbackText ? splitTelegramText(redactText(options.fallbackText)) : [];
|
|
4909
|
-
for (const [index, chunk] of chunks.entries()) {
|
|
4910
|
-
await sendTextMessage(ctx.api, chatId, chunk, {
|
|
4911
|
-
parseMode,
|
|
4912
|
-
fallbackText: fallbackChunks[index] ?? chunk,
|
|
4913
|
-
replyMarkup: index === 0 ? options.replyMarkup : undefined,
|
|
4914
|
-
messageThreadId,
|
|
4915
|
-
});
|
|
4916
|
-
}
|
|
4917
|
-
}
|
|
4918
|
-
async function sendTextMessage(api, chatId, text, options = {}) {
|
|
4919
|
-
const parseMode = Object.prototype.hasOwnProperty.call(options, "parseMode") ? options.parseMode : "HTML";
|
|
4920
|
-
const safeText = redactText(text);
|
|
4921
|
-
const safeFallbackText = options.fallbackText === undefined ? undefined : redactText(options.fallbackText);
|
|
4922
|
-
const bucket = chatBucket(chatId);
|
|
4923
|
-
try {
|
|
4924
|
-
return await telegramRateLimiter.run(bucket, "sendMessage", () => api.sendMessage(chatId, safeText, {
|
|
4925
|
-
...(parseMode ? { parse_mode: parseMode } : {}),
|
|
4926
|
-
...(options.messageThreadId ? { message_thread_id: options.messageThreadId } : {}),
|
|
4927
|
-
reply_markup: options.replyMarkup,
|
|
4928
|
-
}));
|
|
4929
|
-
}
|
|
4930
|
-
catch (error) {
|
|
4931
|
-
if (parseMode && safeFallbackText !== undefined && isTelegramParseError(error)) {
|
|
4932
|
-
return await telegramRateLimiter.run(bucket, "sendMessage", () => api.sendMessage(chatId, safeFallbackText, {
|
|
4933
|
-
...(options.messageThreadId ? { message_thread_id: options.messageThreadId } : {}),
|
|
4934
|
-
reply_markup: options.replyMarkup,
|
|
4935
|
-
}));
|
|
4936
|
-
}
|
|
4937
|
-
throw error;
|
|
4938
|
-
}
|
|
4939
|
-
}
|
|
4940
|
-
async function safeEditMessage(bot, chatId, messageId, text, options = {}) {
|
|
4941
|
-
const parseMode = Object.prototype.hasOwnProperty.call(options, "parseMode") ? options.parseMode : "HTML";
|
|
4942
|
-
const safeText = redactText(text);
|
|
4943
|
-
const safeFallbackText = options.fallbackText === undefined ? undefined : redactText(options.fallbackText);
|
|
4944
|
-
const bucket = `${chatBucket(chatId)}:${messageId}`;
|
|
4945
|
-
try {
|
|
4946
|
-
await telegramRateLimiter.run(bucket, "editMessageText", () => bot.api.editMessageText(chatId, messageId, safeText, {
|
|
4947
|
-
...(parseMode ? { parse_mode: parseMode } : {}),
|
|
4948
|
-
reply_markup: options.replyMarkup,
|
|
4949
|
-
}));
|
|
4950
|
-
}
|
|
4951
|
-
catch (error) {
|
|
4952
|
-
if (isMessageNotModifiedError(error)) {
|
|
4953
|
-
return;
|
|
4954
|
-
}
|
|
4955
|
-
if (parseMode && safeFallbackText !== undefined && isTelegramParseError(error)) {
|
|
4956
|
-
await telegramRateLimiter.run(bucket, "editMessageText", () => bot.api.editMessageText(chatId, messageId, safeFallbackText, {
|
|
4957
|
-
reply_markup: options.replyMarkup,
|
|
4958
|
-
}));
|
|
4959
|
-
return;
|
|
4960
|
-
}
|
|
4961
|
-
throw error;
|
|
4962
|
-
}
|
|
4963
|
-
}
|
|
4964
|
-
async function safeEditReplyMarkup(bot, chatId, messageId, replyMarkup) {
|
|
4965
|
-
try {
|
|
4966
|
-
await telegramRateLimiter.run(`${chatBucket(chatId)}:${messageId}`, "editMessageReplyMarkup", () => bot.api.editMessageReplyMarkup(chatId, messageId, {
|
|
4967
|
-
reply_markup: replyMarkup ?? new InlineKeyboard(),
|
|
4968
|
-
}));
|
|
4969
|
-
}
|
|
4970
|
-
catch (error) {
|
|
4971
|
-
if (!isMessageNotModifiedError(error)) {
|
|
4972
|
-
throw error;
|
|
4973
|
-
}
|
|
4974
|
-
}
|
|
4975
|
-
}
|
|
4976
|
-
async function sendChatActionSafe(api, chatId, action, messageThreadId) {
|
|
4977
|
-
await telegramRateLimiter.run(chatBucket(chatId), "sendChatAction", () => api.sendChatAction(chatId, action, {
|
|
4978
|
-
...(messageThreadId ? { message_thread_id: messageThreadId } : {}),
|
|
4979
|
-
}));
|
|
4980
|
-
}
|
|
4981
|
-
function chatBucket(chatId) {
|
|
4982
|
-
return `chat:${String(chatId)}`;
|
|
4983
|
-
}
|
|
4984
|
-
async function downloadTelegramFile(api, token, fileId, maxBytes = MAX_AUDIO_FILE_SIZE) {
|
|
4985
|
-
const file = await api.getFile(fileId);
|
|
4986
|
-
if (!file.file_path) {
|
|
4987
|
-
throw new Error("Telegram did not return a file path");
|
|
4988
|
-
}
|
|
4989
|
-
if (file.file_size && file.file_size > maxBytes) {
|
|
4990
|
-
throw new Error(`Telegram file too large (${Math.round(file.file_size / 1024 / 1024)} MB, max ${Math.round(maxBytes / 1024 / 1024)} MB)`);
|
|
4991
|
-
}
|
|
4992
|
-
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
|
4993
|
-
const response = await fetch(url);
|
|
4994
|
-
if (!response.ok) {
|
|
4995
|
-
throw new Error(`Failed to download Telegram file: ${response.status}`);
|
|
4996
|
-
}
|
|
4997
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
4998
|
-
const extension = path.extname(file.file_path) || ".bin";
|
|
4999
|
-
const tempPath = path.join(tmpdir(), `nordrelay-file-${randomUUID()}${extension}`);
|
|
5000
|
-
await writeFile(tempPath, buffer);
|
|
5001
|
-
return tempPath;
|
|
5002
|
-
}
|
|
5003
|
-
function splitTelegramText(text) {
|
|
5004
|
-
if (text.length <= TELEGRAM_MESSAGE_LIMIT) {
|
|
5005
|
-
return [text];
|
|
5006
|
-
}
|
|
5007
|
-
const chunks = [];
|
|
5008
|
-
let remaining = text;
|
|
5009
|
-
while (remaining.length > TELEGRAM_MESSAGE_LIMIT) {
|
|
5010
|
-
let cut = remaining.lastIndexOf("\n", TELEGRAM_MESSAGE_LIMIT);
|
|
5011
|
-
if (cut < TELEGRAM_MESSAGE_LIMIT * 0.5) {
|
|
5012
|
-
cut = remaining.lastIndexOf(" ", TELEGRAM_MESSAGE_LIMIT);
|
|
5013
|
-
}
|
|
5014
|
-
if (cut < TELEGRAM_MESSAGE_LIMIT * 0.5) {
|
|
5015
|
-
cut = TELEGRAM_MESSAGE_LIMIT;
|
|
5016
|
-
}
|
|
5017
|
-
chunks.push(remaining.slice(0, cut).trimEnd());
|
|
5018
|
-
remaining = remaining.slice(cut).trimStart();
|
|
5019
|
-
}
|
|
5020
|
-
if (remaining) {
|
|
5021
|
-
chunks.push(remaining);
|
|
5022
|
-
}
|
|
5023
|
-
return chunks.length > 0 ? chunks : [""];
|
|
5024
|
-
}
|
|
5025
|
-
function splitMarkdownForTelegram(markdown) {
|
|
5026
|
-
if (!markdown) {
|
|
5027
|
-
return [];
|
|
5028
|
-
}
|
|
5029
|
-
const chunks = [];
|
|
5030
|
-
let remaining = markdown;
|
|
5031
|
-
while (remaining) {
|
|
5032
|
-
const maxLength = Math.min(remaining.length, FORMATTED_CHUNK_TARGET);
|
|
5033
|
-
const initialCut = findPreferredSplitIndex(remaining, maxLength);
|
|
5034
|
-
const candidate = remaining.slice(0, initialCut) || remaining.slice(0, 1);
|
|
5035
|
-
const rendered = renderMarkdownChunkWithinLimit(candidate);
|
|
5036
|
-
chunks.push(rendered);
|
|
5037
|
-
remaining = remaining.slice(rendered.sourceText.length).trimStart();
|
|
5038
|
-
}
|
|
5039
|
-
return chunks;
|
|
5040
|
-
}
|
|
5041
|
-
function renderMarkdownChunkWithinLimit(markdown) {
|
|
5042
|
-
if (!markdown) {
|
|
5043
|
-
return {
|
|
5044
|
-
text: "",
|
|
5045
|
-
fallbackText: "",
|
|
5046
|
-
parseMode: "HTML",
|
|
5047
|
-
sourceText: "",
|
|
5048
|
-
};
|
|
5049
|
-
}
|
|
5050
|
-
let sourceText = markdown;
|
|
5051
|
-
let rendered = formatMarkdownMessage(sourceText);
|
|
5052
|
-
while (rendered.text.length > TELEGRAM_MESSAGE_LIMIT && sourceText.length > 1) {
|
|
5053
|
-
const nextLength = Math.max(1, sourceText.length - Math.max(100, Math.ceil(sourceText.length * 0.1)));
|
|
5054
|
-
sourceText = sourceText.slice(0, nextLength).trimEnd() || sourceText.slice(0, nextLength);
|
|
5055
|
-
rendered = formatMarkdownMessage(sourceText);
|
|
5056
|
-
}
|
|
5057
|
-
return {
|
|
5058
|
-
...rendered,
|
|
5059
|
-
sourceText,
|
|
5060
|
-
};
|
|
5061
|
-
}
|
|
5062
|
-
function formatMarkdownMessage(markdown) {
|
|
5063
|
-
try {
|
|
5064
|
-
return {
|
|
5065
|
-
text: formatTelegramHTML(markdown),
|
|
5066
|
-
fallbackText: markdown,
|
|
5067
|
-
parseMode: "HTML",
|
|
5068
|
-
};
|
|
5069
|
-
}
|
|
5070
|
-
catch (error) {
|
|
5071
|
-
console.error("Failed to format Telegram HTML, falling back to plain text", error);
|
|
5072
|
-
return {
|
|
5073
|
-
text: markdown,
|
|
5074
|
-
fallbackText: markdown,
|
|
5075
|
-
parseMode: undefined,
|
|
5076
|
-
};
|
|
5077
|
-
}
|
|
5078
|
-
}
|
|
5079
|
-
function findPreferredSplitIndex(text, maxLength) {
|
|
5080
|
-
if (text.length <= maxLength) {
|
|
5081
|
-
return Math.max(1, text.length);
|
|
5082
|
-
}
|
|
5083
|
-
const newlineIndex = text.lastIndexOf("\n", maxLength);
|
|
5084
|
-
if (newlineIndex >= maxLength * 0.5) {
|
|
5085
|
-
return Math.max(1, newlineIndex);
|
|
5086
|
-
}
|
|
5087
|
-
const spaceIndex = text.lastIndexOf(" ", maxLength);
|
|
5088
|
-
if (spaceIndex >= maxLength * 0.5) {
|
|
5089
|
-
return Math.max(1, spaceIndex);
|
|
5090
|
-
}
|
|
5091
|
-
return Math.max(1, maxLength);
|
|
5092
|
-
}
|
|
5093
|
-
function buildStreamingPreview(text) {
|
|
5094
|
-
if (text.length <= STREAMING_PREVIEW_LIMIT) {
|
|
5095
|
-
return text;
|
|
5096
|
-
}
|
|
5097
|
-
return `${text.slice(0, STREAMING_PREVIEW_LIMIT)}\n\n… streaming (preview truncated)`;
|
|
5098
|
-
}
|
|
5099
|
-
function appendWithCap(base, addition, cap) {
|
|
5100
|
-
const combined = `${base}${addition}`;
|
|
5101
|
-
return combined.length <= cap ? combined : combined.slice(-cap);
|
|
5102
|
-
}
|
|
5103
|
-
function summarizeToolOutput(text) {
|
|
5104
|
-
const trimmed = text.trim();
|
|
5105
|
-
if (!trimmed) {
|
|
5106
|
-
return "";
|
|
5107
|
-
}
|
|
5108
|
-
return trimmed.length <= TOOL_OUTPUT_PREVIEW_LIMIT ? trimmed : `${trimmed.slice(-TOOL_OUTPUT_PREVIEW_LIMIT)}\n…`;
|
|
5109
|
-
}
|
|
5110
|
-
function trimLine(text, maxLength) {
|
|
5111
|
-
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
5112
|
-
if (singleLine.length <= maxLength) {
|
|
5113
|
-
return singleLine;
|
|
5114
|
-
}
|
|
5115
|
-
return `${singleLine.slice(0, maxLength - 1)}…`;
|
|
5116
|
-
}
|
|
5117
|
-
function getWorkspaceShortName(workspace) {
|
|
5118
|
-
return workspace.split(/[\\/]/).filter(Boolean).pop() ?? workspace;
|
|
5119
|
-
}
|
|
5120
|
-
function formatRelativeTime(date) {
|
|
5121
|
-
const deltaMs = Date.now() - date.getTime();
|
|
5122
|
-
const deltaSeconds = Math.max(0, Math.floor(deltaMs / 1000));
|
|
5123
|
-
if (deltaSeconds < 60) {
|
|
5124
|
-
return "just now";
|
|
5125
|
-
}
|
|
5126
|
-
const deltaMinutes = Math.floor(deltaSeconds / 60);
|
|
5127
|
-
if (deltaMinutes < 60) {
|
|
5128
|
-
return `${deltaMinutes}m ago`;
|
|
5129
|
-
}
|
|
5130
|
-
const deltaHours = Math.floor(deltaMinutes / 60);
|
|
5131
|
-
if (deltaHours < 48) {
|
|
5132
|
-
return `${deltaHours}h ago`;
|
|
5133
|
-
}
|
|
5134
|
-
const deltaDays = Math.floor(deltaHours / 24);
|
|
5135
|
-
if (deltaDays < 14) {
|
|
5136
|
-
return `${deltaDays}d ago`;
|
|
5137
|
-
}
|
|
5138
|
-
const deltaWeeks = Math.floor(deltaDays / 7);
|
|
5139
|
-
return `${deltaWeeks}w ago`;
|
|
5140
|
-
}
|
|
5141
|
-
function filterSessions(sessions, query) {
|
|
5142
|
-
const normalized = query.trim().toLowerCase();
|
|
5143
|
-
if (!normalized) {
|
|
5144
|
-
return sessions;
|
|
5145
|
-
}
|
|
5146
|
-
return sessions.filter((session) => [
|
|
5147
|
-
session.id,
|
|
5148
|
-
session.title ?? "",
|
|
5149
|
-
session.cwd,
|
|
5150
|
-
session.model ?? "",
|
|
5151
|
-
session.firstUserMessage ?? "",
|
|
5152
|
-
].some((value) => value.toLowerCase().includes(normalized)));
|
|
5153
|
-
}
|
|
5154
|
-
function orderPinnedSessions(sessions, pinnedThreadIds) {
|
|
5155
|
-
const pinnedIndex = new Map(pinnedThreadIds.map((threadId, index) => [threadId, index]));
|
|
5156
|
-
return [...sessions].sort((left, right) => {
|
|
5157
|
-
const leftPinned = pinnedIndex.get(left.id);
|
|
5158
|
-
const rightPinned = pinnedIndex.get(right.id);
|
|
5159
|
-
if (leftPinned !== undefined && rightPinned !== undefined) {
|
|
5160
|
-
return leftPinned - rightPinned;
|
|
5161
|
-
}
|
|
5162
|
-
if (leftPinned !== undefined) {
|
|
5163
|
-
return -1;
|
|
5164
|
-
}
|
|
5165
|
-
if (rightPinned !== undefined) {
|
|
5166
|
-
return 1;
|
|
5167
|
-
}
|
|
5168
|
-
return 0;
|
|
5169
|
-
});
|
|
5170
|
-
}
|
|
5171
|
-
function isMessageNotModifiedError(error) {
|
|
5172
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
5173
|
-
return message.includes("message is not modified");
|
|
5174
|
-
}
|
|
5175
|
-
function isTelegramParseError(error) {
|
|
5176
|
-
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
5177
|
-
return (message.includes("can't parse entities") ||
|
|
5178
|
-
message.includes("unsupported start tag") ||
|
|
5179
|
-
message.includes("unexpected end tag") ||
|
|
5180
|
-
message.includes("entity name") ||
|
|
5181
|
-
message.includes("parse entities"));
|
|
5182
|
-
}
|
|
5183
|
-
function renderPromptFailure(accumulatedText, error) {
|
|
5184
|
-
const message = friendlyErrorText(error);
|
|
5185
|
-
return accumulatedText.trim() ? `${accumulatedText.trim()}\n\n⚠️ ${message}` : `⚠️ ${message}`;
|
|
5186
|
-
}
|
|
5187
|
-
function formatError(error) {
|
|
5188
|
-
return error instanceof Error ? error.message : String(error);
|
|
5189
|
-
}
|