@nordbyte/nordrelay 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +16 -10
  2. package/dist/access-control.js +2 -0
  3. package/dist/agent-updates.js +43 -8
  4. package/dist/bot-ui.js +1 -0
  5. package/dist/bot.js +108 -1063
  6. package/dist/channel-actions.js +8 -8
  7. package/dist/operations.js +63 -9
  8. package/dist/relay-artifact-service.js +126 -0
  9. package/dist/relay-external-activity-monitor.js +216 -0
  10. package/dist/relay-queue-service.js +66 -0
  11. package/dist/relay-runtime-types.js +1 -0
  12. package/dist/relay-runtime.js +77 -359
  13. package/dist/support-bundle.js +205 -0
  14. package/dist/telegram-agent-commands.js +212 -0
  15. package/dist/telegram-artifact-commands.js +139 -0
  16. package/dist/telegram-command-menu.js +1 -0
  17. package/dist/telegram-command-types.js +1 -0
  18. package/dist/telegram-diagnostics-command.js +102 -0
  19. package/dist/telegram-general-commands.js +52 -0
  20. package/dist/telegram-operational-commands.js +153 -0
  21. package/dist/telegram-preference-commands.js +198 -0
  22. package/dist/telegram-queue-commands.js +278 -0
  23. package/dist/telegram-support-command.js +53 -0
  24. package/dist/telegram-update-commands.js +6 -1
  25. package/dist/web-api-contract.js +79 -31
  26. package/dist/web-api-types.js +1 -0
  27. package/dist/web-dashboard-access-routes.js +163 -0
  28. package/dist/web-dashboard-artifact-routes.js +65 -0
  29. package/dist/web-dashboard-assets.js +2 -0
  30. package/dist/web-dashboard-http.js +143 -0
  31. package/dist/web-dashboard-pages.js +257 -0
  32. package/dist/web-dashboard-runtime-routes.js +92 -0
  33. package/dist/web-dashboard-session-routes.js +209 -0
  34. package/dist/web-dashboard.js +43 -882
  35. package/dist/webui-assets/dashboard.css +74 -4
  36. package/dist/webui-assets/dashboard.js +163 -24
  37. package/dist/zip-writer.js +83 -0
  38. package/package.json +10 -4
package/dist/bot.js CHANGED
@@ -1,46 +1,49 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFile, unlink, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
2
+ import { readFile, unlink } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { autoRetry } from "@grammyjs/auto-retry";
6
5
  import { Bot, InlineKeyboard, InputFile } from "grammy";
7
6
  import { ADMIN_GROUP_ID } from "./access-control.js";
8
7
  import { buildFileInstructions, outboxPath, stageFile, } from "./attachments.js";
9
- import { collectArtifactReport, collectRecentWorkspaceArtifacts, createArtifactZipBundle, ensureOutDir, formatArtifactSummary, getArtifactTurnReport, isTelegramImagePreview, listRecentArtifactReports, persistWorkspaceArtifactReport, pruneConnectorTurnDirs, removeArtifactTurn, telegramArtifactFilename, totalArtifactSize, } from "./artifacts.js";
10
- import { listAgentAdapterDescriptors } from "./agent-adapter.js";
8
+ import { collectArtifactReport, collectRecentWorkspaceArtifacts, createArtifactZipBundle, ensureOutDir, formatArtifactSummary, isTelegramImagePreview, persistWorkspaceArtifactReport, pruneConnectorTurnDirs, telegramArtifactFilename, totalArtifactSize, } from "./artifacts.js";
11
9
  import { AgentUpdateManager } from "./agent-updates.js";
12
10
  import { AuditLogStore } from "./audit-log.js";
13
- import { formatSessionLabel, renderHelpMessage, renderWelcomeFirstTime, renderWelcomeReturning, } from "./bot-ui.js";
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";
16
- import { listChannelDescriptors } from "./channel-adapter.js";
11
+ import { formatSessionLabel } from "./bot-ui.js";
12
+ import { BotPreferencesStore, isQuietNow, } from "./bot-preferences.js";
13
+ import { renderAgentUpdateJobAction } from "./channel-actions.js";
17
14
  import { deliverChannelAction } from "./channel-runtime.js";
18
15
  import { agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
19
- import { getAgentActivityLog, getAgentDiagnostics, getExternalActivityForSession, getExternalSnapshotForSession, } from "./agent-activity.js";
20
- import { enabledAgents } from "./agent-factory.js";
16
+ import { getExternalActivityForSession, getExternalSnapshotForSession, } from "./agent-activity.js";
21
17
  import { checkAuthStatus, clearAuthCache, startLogin as startCodexLogin, startLogout as startCodexLogout } from "./codex-auth.js";
22
18
  import { checkClaudeCodeAuthStatus, startClaudeCodeLogin, startClaudeCodeLogout } from "./claude-code-auth.js";
23
19
  import { formatLaunchProfileBehavior } from "./codex-launch.js";
24
20
  import { contextKeyFromCtx, isTelegramContextKey, isTopicContextKey, parseContextKey } from "./context-key.js";
25
21
  import { friendlyErrorText } from "./error-messages.js";
26
22
  import { escapeHTML } from "./format.js";
27
- import { getConnectorHealth, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, } from "./operations.js";
28
23
  import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
29
24
  import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
30
25
  import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
31
26
  import { checkPiAuthStatus } from "./pi-auth.js";
32
27
  import { configureRedaction, redactText } from "./redaction.js";
33
28
  import { canWriteWithLock, SessionLockStore } from "./session-locks.js";
34
- import { renderLaunchSummaryHTML, renderLaunchSummaryPlain, renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
29
+ import { renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
35
30
  import { SessionRegistry } from "./session-registry.js";
36
- import { getAvailableBackends, transcribeAudio } from "./voice.js";
37
- import { getTelegramRateLimitMetrics, telegramRateLimiter } from "./telegram-rate-limit.js";
31
+ import { transcribeAudio } from "./voice.js";
32
+ import { telegramRateLimiter } from "./telegram-rate-limit.js";
38
33
  import { chatBucket, downloadTelegramFile, isMessageNotModifiedError, renderMarkdownChunkWithinLimit, safeEditMessage, safeEditReplyMarkup, safeReply, sendChatActionSafe, sendTextMessage, splitMarkdownForTelegram, } from "./telegram-output.js";
39
34
  import { NOOP_PAGE_CALLBACK_DATA, TelegramBotChannelRuntime, paginateKeyboard, telegramChannelContextFromCtx, } from "./telegram-channel-runtime.js";
40
35
  import { createTelegramAccessMiddleware } from "./telegram-access-middleware.js";
41
36
  import { registerTelegramAccessCommands } from "./telegram-access-commands.js";
37
+ import { registerTelegramAgentCommands } from "./telegram-agent-commands.js";
38
+ import { registerTelegramArtifactCommands } from "./telegram-artifact-commands.js";
39
+ import { registerTelegramDiagnosticsCommands } from "./telegram-diagnostics-command.js";
40
+ import { registerTelegramGeneralCommands } from "./telegram-general-commands.js";
41
+ import { registerTelegramOperationalCommands } from "./telegram-operational-commands.js";
42
+ import { registerTelegramPreferenceCommands } from "./telegram-preference-commands.js";
43
+ import { createQueuedPromptCancelKeyboard, registerTelegramQueueCommands, } from "./telegram-queue-commands.js";
44
+ import { registerTelegramSupportCommands } from "./telegram-support-command.js";
42
45
  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";
46
+ import { appendWithCap, authHelpText, buildStreamingPreview, capabilitiesOf, filterSessions, formatAgentLaunchProfileLabel, formatAgentSettingScope, formatDurationSeconds, formatError, formatLocalDateTime, formatLockOwner, formatModelButtonLabel, formatRelativeTime, formatTelegramName, formatToolSummaryLine, formatTurnUsageLine, getWorkspaceShortName, idOf, isEmptyArtifactReport, isPromptEnvelopeLike, isQueuedPromptLike, labelOf, orderPinnedSessions, parseFastModeArgument, renderExternalMirrorEvent, renderExternalMirrorStatus, renderPromptFailure, renderTodoList, renderToolEndMessage, renderToolStartMessage, requiresTurnApproval, trimLine, } from "./bot-rendering.js";
44
47
  import { UserStore } from "./user-management.js";
45
48
  import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "./workspace-policy.js";
46
49
  export { formatToolSummaryLine, formatTurnUsageLine, summarizeToolName } from "./bot-rendering.js";
@@ -222,9 +225,9 @@ export function createBot(config, registry) {
222
225
  openClawCliPath: config.openClawCliPath,
223
226
  claudeCodeCliPath: config.claudeCodeCliPath,
224
227
  });
225
- const startTelegramAgentUpdate = async (ctx, agentId) => {
228
+ const startTelegramAgentUpdate = async (ctx, agentId, operation = "update") => {
226
229
  try {
227
- const job = agentUpdates.start(agentId, agentUpdateContext());
230
+ const job = agentUpdates.start(agentId, agentUpdateContext(), operation);
228
231
  const contextKey = contextKeyFromCtx(ctx);
229
232
  if (contextKey) {
230
233
  audit({
@@ -232,7 +235,7 @@ export function createBot(config, registry) {
232
235
  status: "ok",
233
236
  contextKey,
234
237
  agentId,
235
- description: `update ${agentId}`,
238
+ description: `${operation} ${agentId}`,
236
239
  detail: job.summary,
237
240
  });
238
241
  }
@@ -240,8 +243,9 @@ export function createBot(config, registry) {
240
243
  await replyChannelAction(ctx, rendered);
241
244
  }
242
245
  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 });
246
+ const message = `Failed to start ${agentLabel(agentId)} ${operation}: ${friendlyErrorText(error)}`;
247
+ const label = operation === "install" ? "Install" : "Update";
248
+ await safeReply(ctx, `<b>${label} failed:</b> ${escapeHTML(message)}`, { fallbackText: message });
245
249
  }
246
250
  };
247
251
  const startAgentLogin = (info) => {
@@ -344,28 +348,6 @@ export function createBot(config, registry) {
344
348
  fallbackText: "Still working on previous message...",
345
349
  });
346
350
  };
347
- const queueCancelCallbackData = (action, contextKey, queueId) => `queue_${action}:${contextKey}:${queueId}`;
348
- const createQueuedPromptCancelKeyboard = (contextKey, queueId, label = "Cancel queued message") => new InlineKeyboard().text(label, queueCancelCallbackData("cancel", contextKey, queueId));
349
- const renderQueueList = (contextKey, queue) => {
350
- const paused = promptStore.isPaused(contextKey);
351
- const rendered = renderQueueListAction(queue, paused);
352
- if (queue.length === 0) {
353
- return rendered;
354
- }
355
- const keyboard = new InlineKeyboard();
356
- queue.forEach((item, index) => {
357
- keyboard
358
- .text(`Run ${index + 1}`, queueCancelCallbackData("run", contextKey, item.id))
359
- .text("Top", queueCancelCallbackData("top", contextKey, item.id))
360
- .text("Cancel", queueCancelCallbackData("remove", contextKey, item.id))
361
- .row();
362
- keyboard
363
- .text("Up", queueCancelCallbackData("up", contextKey, item.id))
364
- .text("Down", queueCancelCallbackData("down", contextKey, item.id))
365
- .row();
366
- });
367
- return { ...rendered, keyboard };
368
- };
369
351
  const createSystemContext = (contextKey) => {
370
352
  const parsed = parseContextKey(contextKey);
371
353
  return {
@@ -1603,616 +1585,78 @@ export function createBot(config, registry) {
1603
1585
  };
1604
1586
  bot.use(createTelegramAccessMiddleware({ userStore, contextUsers, audit }));
1605
1587
  registerTelegramAccessCommands({ bot, userStore, contextUsers, linkAttempts, audit, getUserRole });
1606
- bot.command("start", async (ctx) => {
1607
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1608
- if (!contextSession) {
1609
- return;
1610
- }
1611
- const { contextKey, session } = contextSession;
1612
- const info = session.getInfo();
1613
- const authStatus = capabilitiesOf(info).auth ? await checkAgentAuthStatus(info) : null;
1614
- const authWarning = authStatus && !authStatus.authenticated
1615
- ? [`${labelOf(info)} is not authenticated.`, authStatus.detail, authHelpText(info)].filter(Boolean).join(" ")
1616
- : undefined;
1617
- const isReturning = registry.hasMetadata(contextKey);
1618
- if (isReturning) {
1619
- const welcome = renderWelcomeReturning(renderSessionInfoHTML(info), renderSessionInfoPlain(info), isTopicContext(contextKey), authWarning);
1620
- await safeReply(ctx, welcome.html, { fallbackText: welcome.plain });
1621
- }
1622
- else {
1623
- const welcome = renderWelcomeFirstTime(authWarning);
1624
- await safeReply(ctx, [welcome.html, "", renderLaunchSummaryHTML(info)].join("\n"), {
1625
- fallbackText: [welcome.plain, "", renderLaunchSummaryPlain(info)].join("\n"),
1626
- });
1627
- }
1628
- });
1629
- bot.command("help", async (ctx) => {
1630
- const help = renderHelpMessage();
1631
- await safeReply(ctx, help.html, { fallbackText: help.plain });
1632
- });
1633
- bot.command("channels", async (ctx) => {
1634
- const rendered = renderChannelsAction(listChannelDescriptors());
1635
- await replyChannelAction(ctx, rendered);
1636
- });
1637
- bot.command("agents", async (ctx) => {
1638
- const rendered = renderAgentsAction(listAgentAdapterDescriptors(), enabledAgents(config));
1639
- await replyChannelAction(ctx, rendered);
1640
- });
1641
- bot.command("agent", async (ctx) => {
1642
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1643
- if (!contextSession) {
1644
- return;
1645
- }
1646
- const { contextKey, session } = contextSession;
1647
- if (isBusy(contextKey)) {
1648
- await safeReply(ctx, escapeHTML("Cannot switch agent while a prompt is running."), {
1649
- fallbackText: "Cannot switch agent while a prompt is running.",
1650
- });
1651
- return;
1652
- }
1653
- const availableAgents = enabledAgents(config);
1654
- const currentAgent = idOf(session.getInfo());
1655
- if (availableAgents.length <= 1) {
1656
- const only = agentLabel(availableAgents[0] ?? currentAgent);
1657
- await safeReply(ctx, `<b>Current agent:</b> <code>${escapeHTML(only)}</code>\nNo other agents are enabled.`, {
1658
- fallbackText: `Current agent: ${only}\nNo other agents are enabled.`,
1659
- });
1660
- return;
1661
- }
1662
- pendingAgentPicks.set(contextKey, availableAgents);
1663
- const keyboard = new InlineKeyboard();
1664
- for (const availableAgent of availableAgents) {
1665
- keyboard.text(`${agentLabel(availableAgent)}${availableAgent === currentAgent ? " ✓" : ""}`, `agent_${availableAgent}`).row();
1666
- }
1667
- await safeReply(ctx, `<b>Current agent:</b> <code>${escapeHTML(agentLabel(currentAgent))}</code>\nSelect agent for this Telegram context:`, {
1668
- fallbackText: `Current agent: ${agentLabel(currentAgent)}\nSelect agent for this Telegram context:`,
1669
- replyMarkup: keyboard,
1670
- });
1671
- });
1672
- bot.command("auth", async (ctx) => {
1673
- if (!ctx.chat) {
1674
- return;
1675
- }
1676
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1677
- const info = contextSession?.session.getInfo();
1678
- if (info && !capabilitiesOf(info).auth) {
1679
- const text = `${labelOf(info)} uses its local CLI authentication. Run its login flow on the host if needed.`;
1680
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
1681
- return;
1682
- }
1683
- const authStatus = info ? await checkAgentAuthStatus(info) : await checkAuthStatus(config.codexApiKey);
1684
- const icon = authStatus.authenticated ? "✅" : "❌";
1685
- const html = [
1686
- `<b>${icon} Auth status:</b> ${authStatus.authenticated ? "authenticated" : "not authenticated"}`,
1687
- `<b>Method:</b> <code>${escapeHTML(authStatus.method)}</code>`,
1688
- `<b>Detail:</b> <code>${escapeHTML(authStatus.detail)}</code>`,
1689
- ].join("\n");
1690
- const plain = [
1691
- `${icon} Auth status: ${authStatus.authenticated ? "authenticated" : "not authenticated"}`,
1692
- `Method: ${authStatus.method}`,
1693
- `Detail: ${authStatus.detail}`,
1694
- ].join("\n");
1695
- await safeReply(ctx, html, { fallbackText: plain });
1696
- });
1697
- bot.command("login", async (ctx) => {
1698
- if (!ctx.chat) {
1699
- return;
1700
- }
1701
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1702
- const info = contextSession?.session.getInfo();
1703
- if (info && !capabilitiesOf(info).login) {
1704
- const text = `${labelOf(info)} login is not managed by NordRelay. Run the CLI login flow on the host.`;
1705
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
1706
- return;
1707
- }
1708
- const authStatus = await checkLoginAuthStatus(info);
1709
- if (agentIdForAuth(info) !== "hermes" && authStatus.authenticated) {
1710
- await safeReply(ctx, `<b>✅ Already authenticated</b> via <code>${escapeHTML(authStatus.method)}</code>.`, {
1711
- fallbackText: `✅ Already authenticated via ${authStatus.method}.`,
1712
- });
1713
- return;
1714
- }
1715
- if (!config.enableTelegramLogin) {
1716
- await safeReply(ctx, [
1717
- "<b>Telegram-initiated login is disabled.</b>",
1718
- "",
1719
- `Run <code>${escapeHTML(hostLoginCommand(info))}</code> on the host.`,
1720
- ].join("\n"), {
1721
- fallbackText: [
1722
- "Telegram-initiated login is disabled.",
1723
- "",
1724
- `Run '${hostLoginCommand(info)}' on the host.`,
1725
- ].join("\n"),
1726
- });
1727
- return;
1728
- }
1729
- const result = await startAgentLogin(info);
1730
- if (result.success) {
1731
- await safeReply(ctx, `<b>🔑 Login initiated.</b>\n\n<code>${escapeHTML(result.message)}</code>`, {
1732
- fallbackText: `🔑 Login initiated.\n\n${result.message}`,
1733
- });
1734
- return;
1735
- }
1736
- await safeReply(ctx, `<b>❌ Login failed.</b>\n\n<code>${escapeHTML(result.message)}</code>`, {
1737
- fallbackText: `❌ Login failed.\n\n${result.message}`,
1738
- });
1739
- });
1740
- bot.command("logout", async (ctx) => {
1741
- if (!ctx.chat) {
1742
- return;
1743
- }
1744
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1745
- const info = contextSession?.session.getInfo();
1746
- if (info && !capabilitiesOf(info).logout) {
1747
- const text = `${labelOf(info)} logout is not managed by NordRelay. Run the CLI logout flow on the host.`;
1748
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
1749
- return;
1750
- }
1751
- const authStatus = await checkLoginAuthStatus(info);
1752
- if (authStatus.method === "api-key") {
1753
- await safeReply(ctx, [
1754
- `<b>Cannot logout via Telegram when ${escapeHTML(labelForAuth(info))} uses API-key authentication.</b>`,
1755
- "",
1756
- "Remove the API key from .env to use CLI-based auth instead.",
1757
- ].join("\n"), {
1758
- fallbackText: [
1759
- `Cannot logout via Telegram when ${labelForAuth(info)} uses API-key authentication.`,
1760
- "",
1761
- "Remove the API key from .env to use CLI-based auth instead.",
1762
- ].join("\n"),
1763
- });
1764
- return;
1765
- }
1766
- if (!config.enableTelegramLogin) {
1767
- await safeReply(ctx, [
1768
- "<b>Telegram-initiated auth management is disabled.</b>",
1769
- "",
1770
- `Run <code>${escapeHTML(hostLogoutCommand(info))}</code> on the host.`,
1771
- ].join("\n"), {
1772
- fallbackText: [
1773
- "Telegram-initiated auth management is disabled.",
1774
- "",
1775
- `Run '${hostLogoutCommand(info)}' on the host.`,
1776
- ].join("\n"),
1777
- });
1778
- return;
1779
- }
1780
- if (agentIdForAuth(info) !== "hermes" && !authStatus.authenticated) {
1781
- await safeReply(ctx, escapeHTML("Not currently authenticated."), {
1782
- fallbackText: "Not currently authenticated.",
1783
- });
1784
- return;
1785
- }
1786
- const result = await startAgentLogout(info);
1787
- if (result.success) {
1788
- await safeReply(ctx, `<b>🔓 Logged out.</b>\n\n${escapeHTML(result.message)}`, {
1789
- fallbackText: `🔓 Logged out.\n\n${result.message}`,
1790
- });
1791
- return;
1792
- }
1793
- await safeReply(ctx, `<b>❌ Logout failed.</b>\n\n<code>${escapeHTML(result.message)}</code>`, {
1794
- fallbackText: `❌ Logout failed.\n\n${result.message}`,
1795
- });
1796
- });
1797
- bot.command("mirror", async (ctx) => {
1798
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1799
- if (!contextSession) {
1800
- return;
1801
- }
1802
- const { contextKey, session } = contextSession;
1803
- if (!capabilitiesOf(session.getInfo()).cliMirror) {
1804
- const text = `CLI mirroring is not supported for ${labelOf(session.getInfo())} yet.`;
1805
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
1806
- return;
1807
- }
1808
- const argument = (ctx.message?.text ?? "").replace(/^\/mirror(?:@\w+)?\s*/i, "").trim();
1809
- if (argument) {
1810
- const mode = parseMirrorMode(argument, getEffectiveMirrorMode(contextKey));
1811
- if (!["off", "status", "final", "full"].includes(argument.toLowerCase())) {
1812
- await safeReply(ctx, escapeHTML("Usage: /mirror [off|status|final|full]"), {
1813
- fallbackText: "Usage: /mirror [off|status|final|full]",
1814
- });
1815
- return;
1816
- }
1817
- preferencesStore.update(contextKey, { mirrorMode: mode });
1818
- }
1819
- const mode = getEffectiveMirrorMode(contextKey);
1820
- const plain = [
1821
- `CLI mirroring: ${mode}`,
1822
- `Minimum update interval: ${config.telegramMirrorMinUpdateMs} ms`,
1823
- "Modes: off, status, final, full",
1824
- ].join("\n");
1825
- const html = [
1826
- `<b>CLI mirroring:</b> <code>${escapeHTML(mode)}</code>`,
1827
- `<b>Minimum update interval:</b> <code>${config.telegramMirrorMinUpdateMs} ms</code>`,
1828
- "<b>Modes:</b> <code>off</code>, <code>status</code>, <code>final</code>, <code>full</code>",
1829
- ].join("\n");
1830
- await safeReply(ctx, html, { fallbackText: plain });
1831
- });
1832
- bot.command("notify", async (ctx) => {
1833
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1834
- if (!contextSession) {
1835
- return;
1836
- }
1837
- const { contextKey } = contextSession;
1838
- const argument = (ctx.message?.text ?? "").replace(/^\/notify(?:@\w+)?\s*/i, "").trim();
1839
- if (argument) {
1840
- const quietMatch = argument.match(/^quiet\s+(.+)$/i);
1841
- if (quietMatch) {
1842
- let quietHours;
1843
- try {
1844
- quietHours = quietMatch[1].toLowerCase() === "off" ? null : parseQuietHours(quietMatch[1]);
1845
- }
1846
- catch (error) {
1847
- await safeReply(ctx, escapeHTML(`Invalid quiet hours: ${friendlyErrorText(error)}`), {
1848
- fallbackText: `Invalid quiet hours: ${friendlyErrorText(error)}`,
1849
- });
1850
- return;
1851
- }
1852
- preferencesStore.update(contextKey, { quietHours });
1853
- }
1854
- else {
1855
- const mode = parseNotifyMode(argument, getEffectiveNotifyMode(contextKey));
1856
- if (!["off", "minimal", "all"].includes(argument.toLowerCase())) {
1857
- await safeReply(ctx, escapeHTML("Usage: /notify [off|minimal|all] or /notify quiet HH-HH"), {
1858
- fallbackText: "Usage: /notify [off|minimal|all] or /notify quiet HH-HH",
1859
- });
1860
- return;
1861
- }
1862
- preferencesStore.update(contextKey, { notifyMode: mode });
1863
- }
1864
- }
1865
- const mode = getEffectiveNotifyMode(contextKey);
1866
- const quietHours = getEffectiveQuietHours(contextKey);
1867
- const plain = [
1868
- `Notifications: ${mode}`,
1869
- `Quiet hours: ${formatQuietHours(quietHours)}`,
1870
- `Currently quiet: ${isQuietNow(quietHours) ? "yes" : "no"}`,
1871
- ].join("\n");
1872
- const html = [
1873
- `<b>Notifications:</b> <code>${escapeHTML(mode)}</code>`,
1874
- `<b>Quiet hours:</b> <code>${escapeHTML(formatQuietHours(quietHours))}</code>`,
1875
- `<b>Currently quiet:</b> <code>${isQuietNow(quietHours) ? "yes" : "no"}</code>`,
1876
- ].join("\n");
1877
- await safeReply(ctx, html, { fallbackText: plain });
1588
+ registerTelegramGeneralCommands({
1589
+ bot,
1590
+ config,
1591
+ registry,
1592
+ getContextSession,
1593
+ checkAgentAuthStatus,
1594
+ isTopicContext,
1595
+ replyChannelAction,
1878
1596
  });
1879
- bot.command("workspaces", async (ctx) => {
1880
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1881
- if (!contextSession) {
1882
- return;
1883
- }
1884
- const { session } = contextSession;
1885
- const agentName = labelOf(session.getInfo());
1886
- const workspaces = filterAllowedWorkspaces(session.listWorkspaces(), config);
1887
- const currentWorkspace = session.getInfo().workspace;
1888
- const lines = workspaces.slice(0, 20).map((workspace, index) => {
1889
- const prefix = workspace === currentWorkspace ? "*" : `${index + 1}.`;
1890
- const policy = renderWorkspacePolicyLine(workspace, config);
1891
- return `${prefix} ${workspace}${policy ? ` (${policy})` : ""}`;
1892
- });
1893
- const currentPolicy = evaluateWorkspacePolicy(currentWorkspace, config);
1894
- const header = [
1895
- "Workspaces:",
1896
- `Current: ${currentWorkspace}`,
1897
- currentPolicy.warning ? `Current warning: ${currentPolicy.warning}` : undefined,
1898
- config.workspaceAllowedRoots.length > 0 ? `Allowed roots: ${config.workspaceAllowedRoots.join(", ")}` : "Allowed roots: unrestricted",
1899
- "",
1900
- ].filter((line) => Boolean(line));
1901
- const plain = [...header, ...(lines.length > 0 ? lines : [`No workspaces found in ${agentName} state.`])].join("\n");
1902
- const html = [
1903
- "<b>Workspaces:</b>",
1904
- `<b>Current:</b> <code>${escapeHTML(currentWorkspace)}</code>`,
1905
- currentPolicy.warning ? `<b>Current warning:</b> <code>${escapeHTML(currentPolicy.warning)}</code>` : undefined,
1906
- `<b>Allowed roots:</b> <code>${escapeHTML(config.workspaceAllowedRoots.length > 0 ? config.workspaceAllowedRoots.join(", ") : "unrestricted")}</code>`,
1907
- "",
1908
- ...(lines.length > 0 ? lines.map((line) => `<code>${escapeHTML(line)}</code>`) : [`<code>No workspaces found in ${escapeHTML(agentName)} state.</code>`]),
1909
- ].filter((line) => Boolean(line)).join("\n");
1910
- await safeReply(ctx, html, { fallbackText: plain });
1597
+ registerTelegramAgentCommands({
1598
+ bot,
1599
+ config,
1600
+ registry,
1601
+ pendingAgentPicks,
1602
+ getContextSession,
1603
+ isBusy,
1604
+ checkAgentAuthStatus,
1605
+ checkLoginAuthStatus,
1606
+ agentIdForAuth,
1607
+ labelForAuth,
1608
+ startAgentLogin,
1609
+ startAgentLogout,
1610
+ hostLoginCommand,
1611
+ hostLogoutCommand,
1911
1612
  });
1912
- bot.command("voice", async (ctx) => {
1913
- if (!ctx.chat) {
1914
- return;
1915
- }
1916
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1917
- if (!contextSession) {
1918
- return;
1919
- }
1920
- const { contextKey } = contextSession;
1921
- const argument = (ctx.message?.text ?? "").replace(/^\/voice(?:@\w+)?\s*/i, "").trim();
1922
- if (argument) {
1923
- const parts = argument.split(/\s+/);
1924
- const key = parts[0]?.toLowerCase();
1925
- const value = parts.slice(1).join(" ").trim();
1926
- if (key === "backend" && value) {
1927
- preferencesStore.update(contextKey, { voiceBackend: parseVoiceBackendPreference(value) });
1928
- }
1929
- else if (key === "language") {
1930
- preferencesStore.update(contextKey, { voiceLanguage: value && value.toLowerCase() !== "auto" ? value : null });
1931
- }
1932
- else if (key === "transcribe_only" || key === "transcribe-only") {
1933
- const enabled = parseToggle(value);
1934
- if (enabled === undefined) {
1935
- await safeReply(ctx, escapeHTML("Usage: /voice transcribe_only on|off"), {
1936
- fallbackText: "Usage: /voice transcribe_only on|off",
1937
- });
1938
- return;
1939
- }
1940
- preferencesStore.update(contextKey, { voiceTranscribeOnly: enabled });
1941
- }
1942
- else {
1943
- await safeReply(ctx, escapeHTML("Usage: /voice, /voice backend auto|parakeet|faster-whisper|openai, /voice language auto|<code>, /voice transcribe_only on|off"), {
1944
- fallbackText: "Usage: /voice, /voice backend auto|parakeet|faster-whisper|openai, /voice language auto|<code>, /voice transcribe_only on|off",
1945
- });
1946
- return;
1947
- }
1948
- }
1949
- const backends = await getAvailableBackends().catch(() => []);
1950
- if (backends.length === 0) {
1951
- await safeReply(ctx, [
1952
- "<b>Voice transcription is not available.</b>",
1953
- "",
1954
- "Install <code>faster-whisper</code> + ffmpeg, install <code>parakeet-coreml</code> on macOS Apple Silicon, or set <code>OPENAI_API_KEY</code>.",
1955
- "<i>Cloud transcription uses OPENAI_API_KEY, not CODEX_API_KEY.</i>",
1956
- ].join("\n"), {
1957
- fallbackText: [
1958
- "Voice transcription is not available.",
1959
- "",
1960
- "Install faster-whisper + ffmpeg, install parakeet-coreml on macOS Apple Silicon, or set OPENAI_API_KEY.",
1961
- "Cloud transcription uses OPENAI_API_KEY, not CODEX_API_KEY.",
1962
- ].join("\n"),
1963
- });
1964
- return;
1965
- }
1966
- const joined = backends.join(" + ");
1967
- const backendPreference = getEffectiveVoiceBackend(contextKey);
1968
- const language = getEffectiveVoiceLanguage(contextKey);
1969
- const transcribeOnly = isVoiceTranscribeOnly(contextKey);
1970
- const plain = [
1971
- `Voice backends: ${joined}`,
1972
- `Preferred backend: ${backendPreference}`,
1973
- `Language: ${language ?? "auto"}`,
1974
- `Transcribe only: ${transcribeOnly ? "on" : "off"}`,
1975
- ].join("\n");
1976
- const html = [
1977
- `<b>Voice backends:</b> <code>${escapeHTML(joined)}</code>`,
1978
- `<b>Preferred backend:</b> <code>${escapeHTML(backendPreference)}</code>`,
1979
- `<b>Language:</b> <code>${escapeHTML(language ?? "auto")}</code>`,
1980
- `<b>Transcribe only:</b> <code>${transcribeOnly ? "on" : "off"}</code>`,
1981
- ].join("\n");
1982
- await safeReply(ctx, html, {
1983
- fallbackText: plain,
1984
- });
1613
+ registerTelegramPreferenceCommands({
1614
+ bot,
1615
+ config,
1616
+ preferencesStore,
1617
+ getContextSession,
1618
+ getEffectiveMirrorMode,
1619
+ getEffectiveNotifyMode,
1620
+ getEffectiveQuietHours,
1621
+ getEffectiveVoiceBackend,
1622
+ getEffectiveVoiceLanguage,
1623
+ isVoiceTranscribeOnly,
1985
1624
  });
1986
- bot.command(["status", "health"], async (ctx) => {
1987
- const health = await getConnectorHealth({ piCliPath: config.piCliPath, hermesCliPath: config.hermesCliPath, openClawCliPath: config.openClawCliPath, claudeCodeCliPath: config.claudeCodeCliPath });
1988
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
1989
- const authStatus = contextSession
1990
- ? await checkAgentAuthStatus(contextSession.session.getInfo())
1991
- : await checkAuthStatus(config.codexApiKey);
1992
- const html = renderHealthHTML(health, authStatus.authenticated, getUserRole(ctx));
1993
- const plain = renderHealthPlain(health, authStatus.authenticated, getUserRole(ctx));
1994
- await safeReply(ctx, html, { fallbackText: plain });
1625
+ registerTelegramDiagnosticsCommands({
1626
+ bot,
1627
+ config,
1628
+ registry,
1629
+ promptStore,
1630
+ turnProgress,
1631
+ externalMirrors,
1632
+ externalQueueTimers,
1633
+ queueStatusMessages,
1634
+ getContextSession,
1635
+ checkAgentAuthStatus,
1636
+ getUserRole,
1637
+ getEffectiveMirrorMode,
1638
+ getEffectiveNotifyMode,
1639
+ getEffectiveQuietHours,
1640
+ getEffectiveVoiceBackend,
1641
+ getEffectiveVoiceLanguage,
1642
+ isVoiceTranscribeOnly,
1643
+ replyChannelAction,
1995
1644
  });
1996
- bot.command("version", async (ctx) => {
1997
- const health = await getConnectorHealth({ piCliPath: config.piCliPath, hermesCliPath: config.hermesCliPath, openClawCliPath: config.openClawCliPath, claudeCodeCliPath: config.claudeCodeCliPath });
1998
- const state = await readConnectorState();
1999
- const versions = await getVersionChecks({ piCliPath: config.piCliPath, hermesCliPath: config.hermesCliPath, openClawCliPath: config.openClawCliPath, claudeCodeCliPath: config.claudeCodeCliPath });
2000
- const plain = [
2001
- renderVersionCheckPlain(versions.nordrelay),
2002
- `Runtime status: ${state.status ?? "unknown"}`,
2003
- formatCliPathPlain("Codex CLI", health.codexCliPath, health.codexCli),
2004
- renderVersionCheckPlain(versions.codex),
2005
- formatCliPathPlain("Pi CLI", health.piCliPath, health.piCli),
2006
- renderVersionCheckPlain(versions.pi),
2007
- formatCliPathPlain("Hermes CLI", health.hermesCliPath, health.hermesCli),
2008
- renderVersionCheckPlain(versions.hermes),
2009
- formatCliPathPlain("OpenClaw CLI", health.openClawCliPath, health.openClawCli),
2010
- renderVersionCheckPlain(versions.openclaw),
2011
- formatCliPathPlain("Claude Code CLI", health.claudeCodeCliPath, health.claudeCodeCli),
2012
- renderVersionCheckPlain(versions.claudeCode),
2013
- ].join("\n");
2014
- const html = [
2015
- renderVersionCheckHTML(versions.nordrelay),
2016
- `<b>Runtime status:</b> <code>${escapeHTML(state.status ?? "unknown")}</code>`,
2017
- formatCliPathHTML("Codex CLI", health.codexCliPath, health.codexCli),
2018
- renderVersionCheckHTML(versions.codex),
2019
- formatCliPathHTML("Pi CLI", health.piCliPath, health.piCli),
2020
- renderVersionCheckHTML(versions.pi),
2021
- formatCliPathHTML("Hermes CLI", health.hermesCliPath, health.hermesCli),
2022
- renderVersionCheckHTML(versions.hermes),
2023
- formatCliPathHTML("OpenClaw CLI", health.openClawCliPath, health.openClawCli),
2024
- renderVersionCheckHTML(versions.openclaw),
2025
- formatCliPathHTML("Claude Code CLI", health.claudeCodeCliPath, health.claudeCodeCli),
2026
- renderVersionCheckHTML(versions.claudeCode),
2027
- ].join("\n");
2028
- await safeReply(ctx, html, { fallbackText: plain });
2029
- });
2030
- bot.command(["tasks", "progress"], async (ctx) => {
2031
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
2032
- if (!contextSession) {
2033
- return;
2034
- }
2035
- const progress = turnProgress.get(contextSession.contextKey);
2036
- const queue = promptStore.list(contextSession.contextKey);
2037
- const externalActivity = getExternalActivity(contextSession.session);
2038
- const busyState = {
2039
- ...getBusyState(contextSession.contextKey),
2040
- external: Boolean(externalActivity?.active),
2041
- };
2042
- const info = contextSession.session.getInfo();
2043
- const plain = renderProgressPlain(progress, queue.length, busyState, info);
2044
- const html = renderProgressHTML(progress, queue.length, busyState, info);
2045
- await safeReply(ctx, html, { fallbackText: plain });
2046
- });
2047
- bot.command("activity", async (ctx) => {
2048
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
2049
- if (!contextSession) {
2050
- return;
2051
- }
2052
- const info = contextSession.session.getInfo();
2053
- if (!capabilitiesOf(info).activityLog) {
2054
- const text = `${labelOf(info)} activity timelines are not available yet.`;
2055
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
2056
- return;
2057
- }
2058
- const threadId = contextSession.session.getActiveThreadId();
2059
- if (!threadId) {
2060
- await safeReply(ctx, escapeHTML("No active thread yet."), { fallbackText: "No active thread yet." });
2061
- return;
2062
- }
2063
- const options = parseActivityOptions((ctx.message?.text ?? "").replace(/^\/activity(?:@\w+)?\s*/i, "").trim());
2064
- const events = filterActivityEvents(getAgentActivityLog(contextSession.session, config, options.exportFile ? 200 : options.limit), options);
2065
- const rendered = renderActivityTimeline(threadId, events, options);
2066
- if (options.exportFile && ctx.chat) {
2067
- const exportPath = path.join(tmpdir(), `nordrelay-activity-${threadId}-${randomUUID().slice(0, 8)}.txt`);
2068
- await writeFile(exportPath, rendered.plain, "utf8");
2069
- try {
2070
- await telegramRateLimiter.run(chatBucket(ctx.chat.id), "sendDocument", () => ctx.api.sendDocument(ctx.chat.id, new InputFile(exportPath, path.basename(exportPath)), {
2071
- ...(ctx.message?.message_thread_id ? { message_thread_id: ctx.message.message_thread_id } : {}),
2072
- }));
2073
- }
2074
- finally {
2075
- await unlink(exportPath).catch(() => { });
2076
- }
2077
- return;
2078
- }
2079
- await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
2080
- });
2081
- bot.command("audit", async (ctx) => {
2082
- const rawText = ctx.message?.text ?? "";
2083
- const limitArg = rawText.replace(/^\/audit(?:@\w+)?\s*/i, "").trim();
2084
- const limit = /^\d+$/.test(limitArg) ? Number(limitArg) : 20;
2085
- const events = auditLog.list(limit);
2086
- const rendered = renderAuditEvents(events);
2087
- await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
2088
- });
2089
- bot.command("lock", async (ctx) => {
2090
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
2091
- if (!contextSession || !ctx.from) {
2092
- return;
2093
- }
2094
- const { contextKey, session } = contextSession;
2095
- const existing = lockStore.get(contextKey);
2096
- if (existing && existing.ownerId !== ctx.from.id && !isAdminUser(ctx)) {
2097
- const text = `Session is already locked by ${formatLockOwner(existing)}.`;
2098
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
2099
- return;
2100
- }
2101
- const lock = lockStore.set(contextKey, ctx.from.id, formatTelegramName(ctx), config.sessionLockTtlMs);
2102
- auditContext(ctx, contextKey, session, {
2103
- action: "lock_updated",
2104
- status: "ok",
2105
- detail: `locked by ${lock.ownerId}`,
2106
- });
2107
- const text = `Session locked by ${formatLockOwner(lock)}${lock.expiresAt ? ` until ${formatLocalDateTime(new Date(lock.expiresAt))}` : ""}.`;
2108
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
2109
- });
2110
- bot.command("unlock", async (ctx) => {
2111
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
2112
- if (!contextSession) {
2113
- return;
2114
- }
2115
- const { contextKey, session } = contextSession;
2116
- const lock = lockStore.get(contextKey);
2117
- if (lock && lock.ownerId !== ctx.from?.id && !isAdminUser(ctx)) {
2118
- const text = `Only ${formatLockOwner(lock)} or an admin can unlock this session.`;
2119
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
2120
- return;
2121
- }
2122
- const removed = lockStore.clear(contextKey);
2123
- auditContext(ctx, contextKey, session, {
2124
- action: "lock_updated",
2125
- status: "ok",
2126
- detail: removed ? "unlocked" : "no lock",
2127
- });
2128
- const text = removed ? "Session lock released." : "No active lock for this session.";
2129
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
2130
- });
2131
- bot.command("locks", async (ctx) => {
2132
- const locks = lockStore.list();
2133
- const rendered = renderSessionLocks(locks);
2134
- await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
2135
- });
2136
- bot.command("diagnostics", async (ctx) => {
2137
- const health = await getConnectorHealth({ piCliPath: config.piCliPath, hermesCliPath: config.hermesCliPath, openClawCliPath: config.openClawCliPath, claudeCodeCliPath: config.claudeCodeCliPath });
2138
- const contextKey = contextKeyFromCtx(ctx);
2139
- const queueLength = contextKey ? promptStore.list(contextKey).length : 0;
2140
- const progress = contextKey ? turnProgress.get(contextKey) : undefined;
2141
- const contextSession = contextKey ? await getContextSession(ctx, { deferThreadStart: true }) : null;
2142
- const authStatus = contextSession
2143
- ? await checkAgentAuthStatus(contextSession.session.getInfo())
2144
- : await checkAuthStatus(config.codexApiKey);
2145
- const agentDiagnostics = contextSession
2146
- ? renderAgentDiagnostics(getAgentDiagnostics(contextSession.session, config))
2147
- : { plain: "Agent state: no context", html: "<b>Agent state:</b> <code>no context</code>" };
2148
- const runtime = {
2149
- rateLimit: getTelegramRateLimitMetrics(),
2150
- externalMirrors: externalMirrors.size,
2151
- externalQueueTimers: externalQueueTimers.size,
2152
- queueStatusMessages: queueStatusMessages.size,
2153
- mirrorMode: contextKey ? getEffectiveMirrorMode(contextKey) : config.telegramMirrorMode,
2154
- notifyMode: contextKey ? getEffectiveNotifyMode(contextKey) : config.telegramNotifyMode,
2155
- quietHours: formatQuietHours(contextKey ? getEffectiveQuietHours(contextKey) : config.telegramQuietHours),
2156
- voiceBackend: contextKey ? getEffectiveVoiceBackend(contextKey) : config.voicePreferredBackend,
2157
- voiceLanguage: contextKey ? getEffectiveVoiceLanguage(contextKey) ?? "auto" : config.voiceDefaultLanguage ?? "auto",
2158
- voiceTranscribeOnly: contextKey ? isVoiceTranscribeOnly(contextKey) : config.voiceTranscribeOnly,
2159
- };
2160
- const plain = `${renderDiagnosticsPlain(config, registry, health, authStatus.authenticated, getUserRole(ctx), queueLength, progress, runtime)}\n${agentDiagnostics.plain}`;
2161
- const html = `${renderDiagnosticsHTML(config, registry, health, authStatus.authenticated, getUserRole(ctx), queueLength, progress, runtime)}\n${agentDiagnostics.html}`;
2162
- await safeReply(ctx, html, { fallbackText: plain });
2163
- });
2164
- bot.command("sync", async (ctx) => {
2165
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
2166
- if (!contextSession) {
2167
- return;
2168
- }
2169
- const sessionInfo = contextSession.session.getInfo();
2170
- if (!capabilitiesOf(sessionInfo).externalActivity) {
2171
- const plain = [`${labelOf(sessionInfo)} has no external CLI state watcher to sync.`, "", renderSessionInfoPlain(sessionInfo)].join("\n");
2172
- const html = [`<b>${escapeHTML(labelOf(sessionInfo))} has no external CLI state watcher to sync.</b>`, "", renderSessionInfoHTML(sessionInfo)].join("\n");
2173
- await safeReply(ctx, html, { fallbackText: plain });
2174
- return;
2175
- }
2176
- const result = contextSession.session.syncFromAgentState({ reattach: true });
2177
- if (result.changed) {
2178
- updateSessionMetadata(contextSession.contextKey, contextSession.session);
2179
- }
2180
- const fields = result.changedFields.length > 0 ? result.changedFields.join(", ") : "none";
2181
- const plain = [
2182
- result.changed ? `Synced from ${labelOf(sessionInfo)} state.` : "Already in sync.",
2183
- `Changed: ${fields}`,
2184
- `Reattached: ${result.reattached ? "yes" : "no"}`,
2185
- "",
2186
- renderSessionInfoPlain(result.info),
2187
- ].join("\n");
2188
- const html = [
2189
- result.changed ? `<b>Synced from ${escapeHTML(labelOf(sessionInfo))} state.</b>` : "<b>Already in sync.</b>",
2190
- `<b>Changed:</b> <code>${escapeHTML(fields)}</code>`,
2191
- `<b>Reattached:</b> <code>${result.reattached ? "yes" : "no"}</code>`,
2192
- "",
2193
- renderSessionInfoHTML(result.info),
2194
- ].join("\n");
2195
- await safeReply(ctx, html, { fallbackText: plain });
2196
- });
2197
- bot.command("logs", async (ctx) => {
2198
- const rawText = ctx.message?.text ?? "";
2199
- const argument = rawText.replace(/^\/logs(?:@\w+)?\s*/i, "").trim();
2200
- const logRequest = parseLogsCommand(argument);
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);
2207
- });
2208
- bot.command("restart", async (ctx) => {
2209
- await safeReply(ctx, escapeHTML("Restarting connector..."), {
2210
- fallbackText: "Restarting connector...",
2211
- });
2212
- setTimeout(() => {
2213
- spawnConnectorRestart();
2214
- }, 300);
1645
+ registerTelegramOperationalCommands({
1646
+ bot,
1647
+ config,
1648
+ promptStore,
1649
+ auditLog,
1650
+ lockStore,
1651
+ turnProgress,
1652
+ getContextSession,
1653
+ getBusyState,
1654
+ getExternalActivity,
1655
+ isAdminUser,
1656
+ auditContext,
1657
+ updateSessionMetadata,
2215
1658
  });
1659
+ registerTelegramSupportCommands({ bot, config, auditLog, agentUpdates, getUserRole, audit });
2216
1660
  registerTelegramUpdateCommands({ bot, agentUpdates, replyChannelAction, startTelegramAgentUpdate });
2217
1661
  bot.command("new", async (ctx) => {
2218
1662
  const chatId = ctx.chat?.id;
@@ -2322,233 +1766,24 @@ export function createBot(config, registry) {
2322
1766
  await clearReaction(ctx);
2323
1767
  }
2324
1768
  });
2325
- bot.command("queue", async (ctx) => {
2326
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
2327
- if (!contextSession) {
2328
- return;
2329
- }
2330
- const chatId = ctx.chat?.id;
2331
- const { contextKey, session } = contextSession;
2332
- const rawText = ctx.message?.text ?? "";
2333
- const argument = rawText.replace(/^\/queue(?:@\w+)?\s*/i, "").trim();
2334
- const laterMatch = argument.match(/^later\s+(\d+)(?:m|min|minutes?)?\s+([\s\S]+)$/i);
2335
- if (laterMatch) {
2336
- const minutes = Math.min(7 * 24 * 60, Math.max(1, Number(laterMatch[1])));
2337
- const text = laterMatch[2].trim();
2338
- const notBefore = Date.now() + minutes * 60 * 1000;
2339
- const item = promptStore.enqueue(contextKey, toPromptEnvelope(text), { notBefore });
2340
- const message = `Queued prompt ${item.id} for ${formatLocalDateTime(new Date(notBefore))}.`;
2341
- await safeReply(ctx, escapeHTML(message), {
2342
- fallbackText: message,
2343
- replyMarkup: createQueuedPromptCancelKeyboard(contextKey, item.id),
2344
- });
2345
- auditContext(ctx, contextKey, session, {
2346
- action: "prompt_queued",
2347
- status: "ok",
2348
- promptId: item.id,
2349
- description: item.description,
2350
- detail: "scheduled",
2351
- });
2352
- return;
2353
- }
2354
- const inspectMatch = argument.match(/^inspect\s+([a-z0-9]+)$/i);
2355
- if (inspectMatch) {
2356
- const item = promptStore.get(contextKey, inspectMatch[1]);
2357
- if (!item) {
2358
- await safeReply(ctx, escapeHTML(`No queued prompt found with id ${inspectMatch[1]}.`), {
2359
- fallbackText: `No queued prompt found with id ${inspectMatch[1]}.`,
2360
- });
2361
- return;
2362
- }
2363
- const rendered = renderQueuedPromptDetailAction(item);
2364
- await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
2365
- return;
2366
- }
2367
- if (/^pause$/i.test(argument)) {
2368
- promptStore.pause(contextKey);
2369
- const message = `Queue paused. ${promptStore.list(contextKey).length} queued.`;
2370
- await safeReply(ctx, escapeHTML(message), { fallbackText: message });
2371
- await updateQueueStatusMessage(contextKey, message);
2372
- return;
2373
- }
2374
- if (/^resume$/i.test(argument)) {
2375
- promptStore.resume(contextKey);
2376
- const message = `Queue resumed. ${promptStore.list(contextKey).length} queued.`;
2377
- await safeReply(ctx, escapeHTML(message), { fallbackText: message });
2378
- if (chatId) {
2379
- void drainQueuedPrompts(ctx, contextKey, chatId, session).catch((error) => {
2380
- console.error("Failed to drain queue after resume:", error);
2381
- });
2382
- }
2383
- return;
2384
- }
2385
- const moveMatch = argument.match(/^move\s+([a-z0-9]+)\s+(top|up|down)$/i);
2386
- if (moveMatch) {
2387
- const direction = moveMatch[2].toLowerCase();
2388
- const item = direction === "top"
2389
- ? promptStore.moveToTop(contextKey, moveMatch[1])
2390
- : direction === "up"
2391
- ? promptStore.moveUp(contextKey, moveMatch[1])
2392
- : promptStore.moveDown(contextKey, moveMatch[1]);
2393
- if (!item) {
2394
- await safeReply(ctx, escapeHTML(`No queued prompt found with id ${moveMatch[1]}.`), {
2395
- fallbackText: `No queued prompt found with id ${moveMatch[1]}.`,
2396
- });
2397
- return;
2398
- }
2399
- const message = `Moved queued prompt ${item.id} ${direction}.`;
2400
- await safeReply(ctx, escapeHTML(message), { fallbackText: message });
2401
- return;
2402
- }
2403
- const runMatch = argument.match(/^run\s+([a-z0-9]+)$/i);
2404
- if (runMatch) {
2405
- const item = promptStore.remove(contextKey, runMatch[1]);
2406
- if (!item) {
2407
- await safeReply(ctx, escapeHTML(`No queued prompt found with id ${runMatch[1]}.`), {
2408
- fallbackText: `No queued prompt found with id ${runMatch[1]}.`,
2409
- });
2410
- return;
2411
- }
2412
- promptStore.enqueueFront(contextKey, item);
2413
- promptStore.resume(contextKey);
2414
- if (!chatId) {
2415
- return;
2416
- }
2417
- const busy = getBusyReason(contextKey);
2418
- if (busy.busy) {
2419
- const message = `Queued prompt ${item.id} moved to top and will run when the current task finishes.`;
2420
- await safeReply(ctx, escapeHTML(message), { fallbackText: message });
2421
- if (busy.kind === "external") {
2422
- scheduleExternalQueueDrain(ctx, contextKey, chatId, session);
2423
- }
2424
- return;
2425
- }
2426
- const next = promptStore.dequeue(contextKey);
2427
- if (next) {
2428
- await handleUserPrompt(ctx, contextKey, chatId, session, next, { fromQueue: true });
2429
- }
2430
- return;
2431
- }
2432
- if (argument) {
2433
- await safeReply(ctx, escapeHTML("Usage: /queue, /queue pause, /queue resume, /queue later <minutes> <prompt>, /queue inspect <id>, /queue move <id> top|up|down, /queue run <id>"), {
2434
- fallbackText: "Usage: /queue, /queue pause, /queue resume, /queue later <minutes> <prompt>, /queue inspect <id>, /queue move <id> top|up|down, /queue run <id>",
2435
- });
2436
- return;
2437
- }
2438
- const queue = promptStore.list(contextKey);
2439
- if (queue.length === 0) {
2440
- const rendered = renderQueueList(contextKey, queue);
2441
- await safeReply(ctx, rendered.html, { fallbackText: rendered.plain });
2442
- return;
2443
- }
2444
- const rendered = renderQueueList(contextKey, queue);
2445
- await safeReply(ctx, rendered.html, {
2446
- fallbackText: rendered.plain,
2447
- replyMarkup: rendered.keyboard,
2448
- });
1769
+ registerTelegramQueueCommands({
1770
+ bot,
1771
+ promptStore,
1772
+ getContextSession,
1773
+ getBusyReason,
1774
+ getSession: (contextKey) => registry.get(contextKey),
1775
+ updateQueueStatusMessage,
1776
+ scheduleExternalQueueDrain,
1777
+ drainQueuedPrompts,
1778
+ handleUserPrompt,
1779
+ auditContext,
2449
1780
  });
2450
- bot.command("clearqueue", async (ctx) => {
2451
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
2452
- if (!contextSession) {
2453
- return;
2454
- }
2455
- const count = promptStore.clear(contextSession.contextKey);
2456
- const message = `Cleared ${count} queued prompt${count === 1 ? "" : "s"}.`;
2457
- await safeReply(ctx, escapeHTML(message), { fallbackText: message });
2458
- });
2459
- bot.command("cancel", async (ctx) => {
2460
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
2461
- if (!contextSession) {
2462
- return;
2463
- }
2464
- const rawText = ctx.message?.text ?? "";
2465
- const id = rawText.replace(/^\/cancel(?:@\w+)?\s*/i, "").trim();
2466
- if (!id) {
2467
- await safeReply(ctx, escapeHTML("Usage: /cancel <queue-id>"), {
2468
- fallbackText: "Usage: /cancel <queue-id>",
2469
- });
2470
- return;
2471
- }
2472
- const removed = promptStore.remove(contextSession.contextKey, id);
2473
- if (!removed) {
2474
- await safeReply(ctx, escapeHTML(`No queued prompt found with id ${id}.`), {
2475
- fallbackText: `No queued prompt found with id ${id}.`,
2476
- });
2477
- return;
2478
- }
2479
- await safeReply(ctx, escapeHTML(`Cancelled queued prompt ${removed.id}.`), {
2480
- fallbackText: `Cancelled queued prompt ${removed.id}.`,
2481
- });
2482
- });
2483
- bot.command("artifacts", async (ctx) => {
2484
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
2485
- if (!contextSession || !ctx.chat) {
2486
- return;
2487
- }
2488
- const workspace = contextSession.session.getInfo().workspace;
2489
- const rawText = ctx.message?.text ?? "";
2490
- const argument = rawText.replace(/^\/artifacts(?:@\w+)?\s*/i, "").trim();
2491
- const reports = await listRecentArtifactReports(workspace, 10, config.maxFileSize);
2492
- if (reports.length === 0) {
2493
- await safeReply(ctx, escapeHTML("No generated artifacts found for this workspace."), {
2494
- fallbackText: "No generated artifacts found for this workspace.",
2495
- });
2496
- return;
2497
- }
2498
- if (argument) {
2499
- const parts = argument.split(/\s+/).filter(Boolean);
2500
- if (parts[0]?.toLowerCase() === "delete" && parts[1]) {
2501
- const selected = reports.find((report) => report.turnId === parts[1] || report.turnId.startsWith(parts[1]));
2502
- if (!selected) {
2503
- await safeReply(ctx, escapeHTML(`No artifact turn found for "${parts[1]}".`), {
2504
- fallbackText: `No artifact turn found for "${parts[1]}".`,
2505
- });
2506
- return;
2507
- }
2508
- const removed = await removeArtifactTurn(workspace, selected.turnId);
2509
- const text = removed ? `Deleted artifact turn: ${selected.turnId}` : `Artifact turn not found: ${selected.turnId}`;
2510
- await safeReply(ctx, escapeHTML(text), { fallbackText: text });
2511
- return;
2512
- }
2513
- const filtered = filterArtifactReports(reports, argument);
2514
- if (filtered) {
2515
- if (filtered.length === 0) {
2516
- await safeReply(ctx, escapeHTML(`No artifacts matched "${argument}".`), {
2517
- fallbackText: `No artifacts matched "${argument}".`,
2518
- });
2519
- return;
2520
- }
2521
- const rendered = renderArtifactReportsAction(filtered);
2522
- await safeReply(ctx, rendered.html, {
2523
- fallbackText: rendered.plain,
2524
- replyMarkup: buildArtifactActionsKeyboard(filtered),
2525
- });
2526
- return;
2527
- }
2528
- const shouldZip = parts[0]?.toLowerCase() === "zip";
2529
- const requestedTurn = shouldZip ? parts[1] : parts[0];
2530
- const selected = !requestedTurn || requestedTurn.toLowerCase() === "latest"
2531
- ? reports[0]
2532
- : reports.find((report) => report.turnId === requestedTurn || report.turnId.startsWith(requestedTurn));
2533
- if (!selected) {
2534
- await safeReply(ctx, escapeHTML(`No artifact turn found for "${argument}".`), {
2535
- fallbackText: `No artifact turn found for "${argument}".`,
2536
- });
2537
- return;
2538
- }
2539
- if (shouldZip) {
2540
- await deliverArtifactReportZip(ctx, ctx.chat.id, selected, ctx.message?.message_thread_id);
2541
- }
2542
- else {
2543
- await deliverArtifactReport(ctx, ctx.chat.id, selected, ctx.message?.message_thread_id);
2544
- }
2545
- return;
2546
- }
2547
- const { html, plain } = renderArtifactReportsAction(reports);
2548
- await safeReply(ctx, html, {
2549
- fallbackText: plain,
2550
- replyMarkup: buildArtifactActionsKeyboard(reports),
2551
- });
1781
+ registerTelegramArtifactCommands({
1782
+ bot,
1783
+ config,
1784
+ getContextSession,
1785
+ deliverArtifactReport,
1786
+ deliverArtifactReportZip,
2552
1787
  });
2553
1788
  bot.command("session", async (ctx) => {
2554
1789
  const contextSession = await getContextSession(ctx, { deferThreadStart: true });
@@ -3065,49 +2300,6 @@ export function createBot(config, registry) {
3065
2300
  });
3066
2301
  };
3067
2302
  bot.command(["effort", "reasoning"], openReasoningPicker);
3068
- bot.callbackQuery(/^agent_(codex|pi|hermes|openclaw|claude-code)$/, async (ctx) => {
3069
- const chatId = ctx.chat?.id;
3070
- const messageId = ctx.callbackQuery.message?.message_id;
3071
- const selectedAgent = ctx.match?.[1];
3072
- const contextKey = contextKeyFromCtx(ctx);
3073
- if (!chatId || !contextKey || !selectedAgent) {
3074
- await ctx.answerCallbackQuery();
3075
- return;
3076
- }
3077
- const picks = pendingAgentPicks.get(contextKey);
3078
- if (!picks?.includes(selectedAgent)) {
3079
- await ctx.answerCallbackQuery({ text: "Expired, run /agent again" });
3080
- return;
3081
- }
3082
- if (isBusy(contextKey)) {
3083
- await ctx.answerCallbackQuery({ text: "Wait for the current prompt to finish" });
3084
- return;
3085
- }
3086
- await ctx.answerCallbackQuery({ text: `Switching to ${agentLabel(selectedAgent)}...` });
3087
- pendingAgentPicks.delete(contextKey);
3088
- try {
3089
- const session = await registry.switchAgent(contextKey, selectedAgent);
3090
- const info = session.getInfo();
3091
- const html = [`<b>Agent switched to ${escapeHTML(labelOf(info))}.</b>`, "", renderSessionInfoHTML(info)].join("\n");
3092
- const plain = [`Agent switched to ${labelOf(info)}.`, "", renderSessionInfoPlain(info)].join("\n");
3093
- if (messageId) {
3094
- await safeEditMessage(bot, chatId, messageId, html, { fallbackText: plain });
3095
- }
3096
- else {
3097
- await safeReply(ctx, html, { fallbackText: plain });
3098
- }
3099
- }
3100
- catch (error) {
3101
- const html = `<b>Failed:</b> ${escapeHTML(friendlyErrorText(error))}`;
3102
- const plain = `Failed: ${friendlyErrorText(error)}`;
3103
- if (messageId) {
3104
- await safeEditMessage(bot, chatId, messageId, html, { fallbackText: plain });
3105
- }
3106
- else {
3107
- await safeReply(ctx, html, { fallbackText: plain });
3108
- }
3109
- }
3110
- });
3111
2303
  bot.callbackQuery(NOOP_PAGE_CALLBACK_DATA, async (ctx) => {
3112
2304
  await ctx.answerCallbackQuery();
3113
2305
  });
@@ -3130,94 +2322,6 @@ export function createBot(config, registry) {
3130
2322
  await ctx.answerCallbackQuery({ text: "Aborting..." });
3131
2323
  await session.abort();
3132
2324
  });
3133
- bot.callbackQuery(/^queue_(cancel|remove|top|up|down|run):(-?\d+(?::\d+)?):([a-z0-9]+)$/, async (ctx) => {
3134
- const action = ctx.match?.[1];
3135
- const contextKey = ctx.match?.[2];
3136
- const queueId = ctx.match?.[3];
3137
- if (!action || !contextKey || !queueId) {
3138
- await ctx.answerCallbackQuery();
3139
- return;
3140
- }
3141
- const currentContextKey = contextKeyFromCtx(ctx);
3142
- if (currentContextKey && currentContextKey !== contextKey) {
3143
- await ctx.answerCallbackQuery({ text: "This queue button belongs to another chat or topic." });
3144
- return;
3145
- }
3146
- const chatId = ctx.chat?.id;
3147
- const messageId = ctx.callbackQuery.message?.message_id;
3148
- if (action === "top" || action === "up" || action === "down") {
3149
- const item = action === "top"
3150
- ? promptStore.moveToTop(contextKey, queueId)
3151
- : action === "up"
3152
- ? promptStore.moveUp(contextKey, queueId)
3153
- : promptStore.moveDown(contextKey, queueId);
3154
- await ctx.answerCallbackQuery({ text: item ? `Moved ${queueId} ${action}.` : "Queued prompt not found." });
3155
- if (chatId && messageId) {
3156
- const rendered = renderQueueList(contextKey, promptStore.list(contextKey));
3157
- await safeEditMessage(bot, chatId, messageId, rendered.html, {
3158
- fallbackText: rendered.plain,
3159
- replyMarkup: rendered.keyboard,
3160
- });
3161
- }
3162
- return;
3163
- }
3164
- if (action === "run") {
3165
- const item = promptStore.remove(contextKey, queueId);
3166
- if (!item) {
3167
- await ctx.answerCallbackQuery({ text: "Queued prompt already started or was cancelled." });
3168
- return;
3169
- }
3170
- promptStore.enqueueFront(contextKey, item);
3171
- promptStore.resume(contextKey);
3172
- await ctx.answerCallbackQuery({ text: `Queued prompt ${queueId} moved to next.` });
3173
- if (chatId && messageId) {
3174
- const rendered = renderQueueList(contextKey, promptStore.list(contextKey));
3175
- await safeEditMessage(bot, chatId, messageId, rendered.html, {
3176
- fallbackText: rendered.plain,
3177
- replyMarkup: rendered.keyboard,
3178
- });
3179
- }
3180
- const session = registry.get(contextKey);
3181
- if (chatId && session && !getBusyReason(contextKey).busy) {
3182
- void drainQueuedPrompts(ctx, contextKey, chatId, session).catch((error) => {
3183
- console.error("Failed to drain queue after run-now callback:", error);
3184
- });
3185
- }
3186
- return;
3187
- }
3188
- const removed = promptStore.remove(contextKey, queueId);
3189
- if (!removed) {
3190
- await ctx.answerCallbackQuery({ text: "Queued prompt already started or was cancelled." });
3191
- if (chatId && messageId) {
3192
- if (action === "remove") {
3193
- const rendered = renderQueueList(contextKey, promptStore.list(contextKey));
3194
- await safeEditMessage(bot, chatId, messageId, rendered.html, {
3195
- fallbackText: rendered.plain,
3196
- replyMarkup: rendered.keyboard,
3197
- });
3198
- }
3199
- else {
3200
- const message = `Queued prompt ${queueId} is no longer queued.`;
3201
- await safeEditMessage(bot, chatId, messageId, escapeHTML(message), { fallbackText: message });
3202
- }
3203
- }
3204
- return;
3205
- }
3206
- const message = `Cancelled queued prompt ${removed.id}.`;
3207
- await ctx.answerCallbackQuery({ text: message });
3208
- if (!chatId || !messageId) {
3209
- return;
3210
- }
3211
- if (action === "remove") {
3212
- const rendered = renderQueueList(contextKey, promptStore.list(contextKey));
3213
- await safeEditMessage(bot, chatId, messageId, rendered.html, {
3214
- fallbackText: rendered.plain,
3215
- replyMarkup: rendered.keyboard,
3216
- });
3217
- return;
3218
- }
3219
- await safeEditMessage(bot, chatId, messageId, escapeHTML(message), { fallbackText: message });
3220
- });
3221
2325
  bot.callbackQuery(/^approval_(yes|no):([a-z0-9]+)$/, async (ctx) => {
3222
2326
  const action = ctx.match?.[1];
3223
2327
  const approvalId = ctx.match?.[2];
@@ -3627,65 +2731,6 @@ export function createBot(config, registry) {
3627
2731
  fallbackText: `⚡ ${label} set to ${effort} — ${scope}.`,
3628
2732
  });
3629
2733
  });
3630
- bot.callbackQuery(/^artifact_(send|zip|delete|delete_confirm):([a-zA-Z0-9._-]+)$/, async (ctx) => {
3631
- const action = ctx.match?.[1];
3632
- const turnId = ctx.match?.[2];
3633
- const chatId = ctx.chat?.id;
3634
- const messageId = ctx.callbackQuery.message?.message_id;
3635
- if (!action || !turnId || !chatId) {
3636
- await ctx.answerCallbackQuery();
3637
- return;
3638
- }
3639
- const contextSession = await getContextSession(ctx, { deferThreadStart: true });
3640
- if (!contextSession) {
3641
- await ctx.answerCallbackQuery({ text: "No context" });
3642
- return;
3643
- }
3644
- const workspace = contextSession.session.getInfo().workspace;
3645
- if (action === "delete") {
3646
- await ctx.answerCallbackQuery({ text: "Confirm deletion" });
3647
- const keyboard = new InlineKeyboard()
3648
- .text("Delete artifacts", `artifact_delete_confirm:${turnId}`)
3649
- .row()
3650
- .text("Cancel", NOOP_PAGE_CALLBACK_DATA);
3651
- const html = `<b>Delete artifact turn?</b>\n<code>${escapeHTML(turnId)}</code>`;
3652
- const plain = `Delete artifact turn?\n${turnId}`;
3653
- if (messageId) {
3654
- await safeEditMessage(bot, chatId, messageId, html, { fallbackText: plain, replyMarkup: keyboard });
3655
- }
3656
- else {
3657
- await safeReply(ctx, html, { fallbackText: plain, replyMarkup: keyboard });
3658
- }
3659
- return;
3660
- }
3661
- if (action === "delete_confirm") {
3662
- const removed = await removeArtifactTurn(workspace, turnId);
3663
- await ctx.answerCallbackQuery({ text: removed ? "Deleted" : "Already gone" });
3664
- const html = removed
3665
- ? `<b>Deleted artifact turn:</b> <code>${escapeHTML(turnId)}</code>`
3666
- : `<b>Artifact turn not found:</b> <code>${escapeHTML(turnId)}</code>`;
3667
- const plain = removed ? `Deleted artifact turn: ${turnId}` : `Artifact turn not found: ${turnId}`;
3668
- if (messageId) {
3669
- await safeEditMessage(bot, chatId, messageId, html, { fallbackText: plain });
3670
- }
3671
- else {
3672
- await safeReply(ctx, html, { fallbackText: plain });
3673
- }
3674
- return;
3675
- }
3676
- const report = await getArtifactTurnReport(workspace, turnId, config.maxFileSize);
3677
- if (!report) {
3678
- await ctx.answerCallbackQuery({ text: "Artifact turn not found" });
3679
- return;
3680
- }
3681
- await ctx.answerCallbackQuery({ text: action === "zip" ? "Sending ZIP..." : "Sending artifacts..." });
3682
- if (action === "zip") {
3683
- await deliverArtifactReportZip(ctx, chatId, report, ctx.callbackQuery.message?.message_thread_id);
3684
- }
3685
- else {
3686
- await deliverArtifactReport(ctx, chatId, report, ctx.callbackQuery.message?.message_thread_id);
3687
- }
3688
- });
3689
2734
  bot.on("message:text", async (ctx) => {
3690
2735
  const contextSession = await getContextSession(ctx);
3691
2736
  if (!contextSession) {