@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.
- package/README.md +16 -10
- package/dist/access-control.js +2 -0
- package/dist/agent-updates.js +43 -8
- package/dist/bot-ui.js +1 -0
- package/dist/bot.js +108 -1063
- package/dist/channel-actions.js +8 -8
- package/dist/operations.js +63 -9
- package/dist/relay-artifact-service.js +126 -0
- package/dist/relay-external-activity-monitor.js +216 -0
- package/dist/relay-queue-service.js +66 -0
- package/dist/relay-runtime-types.js +1 -0
- package/dist/relay-runtime.js +77 -359
- package/dist/support-bundle.js +205 -0
- package/dist/telegram-agent-commands.js +212 -0
- package/dist/telegram-artifact-commands.js +139 -0
- package/dist/telegram-command-menu.js +1 -0
- package/dist/telegram-command-types.js +1 -0
- package/dist/telegram-diagnostics-command.js +102 -0
- package/dist/telegram-general-commands.js +52 -0
- package/dist/telegram-operational-commands.js +153 -0
- package/dist/telegram-preference-commands.js +198 -0
- package/dist/telegram-queue-commands.js +278 -0
- package/dist/telegram-support-command.js +53 -0
- package/dist/telegram-update-commands.js +6 -1
- package/dist/web-api-contract.js +79 -31
- package/dist/web-api-types.js +1 -0
- package/dist/web-dashboard-access-routes.js +163 -0
- package/dist/web-dashboard-artifact-routes.js +65 -0
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-http.js +143 -0
- package/dist/web-dashboard-pages.js +257 -0
- package/dist/web-dashboard-runtime-routes.js +92 -0
- package/dist/web-dashboard-session-routes.js +209 -0
- package/dist/web-dashboard.js +43 -882
- package/dist/webui-assets/dashboard.css +74 -4
- package/dist/webui-assets/dashboard.js +163 -24
- package/dist/zip-writer.js +83 -0
- 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
|
|
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,
|
|
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
|
|
14
|
-
import { BotPreferencesStore,
|
|
15
|
-
import {
|
|
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 {
|
|
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 {
|
|
29
|
+
import { renderSessionInfoHTML, renderSessionInfoPlain, } from "./session-format.js";
|
|
35
30
|
import { SessionRegistry } from "./session-registry.js";
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
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,
|
|
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:
|
|
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)}
|
|
244
|
-
|
|
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
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
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
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
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
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
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
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
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) {
|