@openacp/cli 0.6.9 → 2026.41.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 (133) hide show
  1. package/README.md +116 -152
  2. package/dist/cli.d.ts +11 -0
  3. package/dist/cli.js +27740 -415
  4. package/dist/cli.js.map +1 -1
  5. package/dist/data/registry-snapshot.json +1 -1
  6. package/dist/index.d.ts +1944 -463
  7. package/dist/index.js +17365 -102
  8. package/dist/index.js.map +1 -1
  9. package/package.json +13 -7
  10. package/dist/action-detect-P7ZE4NEM.js +0 -16
  11. package/dist/action-detect-P7ZE4NEM.js.map +0 -1
  12. package/dist/adapter-LNEGLMOE.js +0 -799
  13. package/dist/adapter-LNEGLMOE.js.map +0 -1
  14. package/dist/admin-6SYB6XCZ.js +0 -23
  15. package/dist/admin-6SYB6XCZ.js.map +0 -1
  16. package/dist/agent-catalog-FC3HGDEQ.js +0 -11
  17. package/dist/agent-catalog-FC3HGDEQ.js.map +0 -1
  18. package/dist/agent-dependencies-4OWBMZWZ.js +0 -24
  19. package/dist/agent-dependencies-4OWBMZWZ.js.map +0 -1
  20. package/dist/agent-registry-WT4NXPYG.js +0 -9
  21. package/dist/agent-registry-WT4NXPYG.js.map +0 -1
  22. package/dist/agent-store-VZLFPTZU.js +0 -9
  23. package/dist/agent-store-VZLFPTZU.js.map +0 -1
  24. package/dist/agents-QO7DKARJ.js +0 -15
  25. package/dist/agents-QO7DKARJ.js.map +0 -1
  26. package/dist/api-client-CFQT5U7D.js +0 -14
  27. package/dist/api-client-CFQT5U7D.js.map +0 -1
  28. package/dist/autostart-X33OGMX6.js +0 -23
  29. package/dist/autostart-X33OGMX6.js.map +0 -1
  30. package/dist/chunk-2CJ46J3C.js +0 -154
  31. package/dist/chunk-2CJ46J3C.js.map +0 -1
  32. package/dist/chunk-2HMQOC7N.js +0 -134
  33. package/dist/chunk-2HMQOC7N.js.map +0 -1
  34. package/dist/chunk-33RP6K2O.js +0 -435
  35. package/dist/chunk-33RP6K2O.js.map +0 -1
  36. package/dist/chunk-34M4OS5P.js +0 -83
  37. package/dist/chunk-34M4OS5P.js.map +0 -1
  38. package/dist/chunk-4CTX774K.js +0 -265
  39. package/dist/chunk-4CTX774K.js.map +0 -1
  40. package/dist/chunk-7QJS2XBD.js +0 -92
  41. package/dist/chunk-7QJS2XBD.js.map +0 -1
  42. package/dist/chunk-BDYVCIBH.js +0 -735
  43. package/dist/chunk-BDYVCIBH.js.map +0 -1
  44. package/dist/chunk-BN3X7UXB.js +0 -738
  45. package/dist/chunk-BN3X7UXB.js.map +0 -1
  46. package/dist/chunk-BNLGTZ34.js +0 -122
  47. package/dist/chunk-BNLGTZ34.js.map +0 -1
  48. package/dist/chunk-GAK6PIBW.js +0 -224
  49. package/dist/chunk-GAK6PIBW.js.map +0 -1
  50. package/dist/chunk-H5P2C6H4.js +0 -4740
  51. package/dist/chunk-H5P2C6H4.js.map +0 -1
  52. package/dist/chunk-I7WC6E5S.js +0 -71
  53. package/dist/chunk-I7WC6E5S.js.map +0 -1
  54. package/dist/chunk-J4SJTKIK.js +0 -203
  55. package/dist/chunk-J4SJTKIK.js.map +0 -1
  56. package/dist/chunk-JHYXKVV2.js +0 -183
  57. package/dist/chunk-JHYXKVV2.js.map +0 -1
  58. package/dist/chunk-JKBFUAJK.js +0 -282
  59. package/dist/chunk-JKBFUAJK.js.map +0 -1
  60. package/dist/chunk-JUYDFUSN.js +0 -673
  61. package/dist/chunk-JUYDFUSN.js.map +0 -1
  62. package/dist/chunk-KIRH7TUJ.js +0 -219
  63. package/dist/chunk-KIRH7TUJ.js.map +0 -1
  64. package/dist/chunk-LBIKITQT.js +0 -22
  65. package/dist/chunk-LBIKITQT.js.map +0 -1
  66. package/dist/chunk-LGP2YGRL.js +0 -4880
  67. package/dist/chunk-LGP2YGRL.js.map +0 -1
  68. package/dist/chunk-NAMYZIS5.js +0 -1
  69. package/dist/chunk-NAMYZIS5.js.map +0 -1
  70. package/dist/chunk-NVPG6JCL.js +0 -724
  71. package/dist/chunk-NVPG6JCL.js.map +0 -1
  72. package/dist/chunk-O7CPGUAI.js +0 -298
  73. package/dist/chunk-O7CPGUAI.js.map +0 -1
  74. package/dist/chunk-S64CB6J3.js +0 -98
  75. package/dist/chunk-S64CB6J3.js.map +0 -1
  76. package/dist/chunk-UKT3G5IA.js +0 -484
  77. package/dist/chunk-UKT3G5IA.js.map +0 -1
  78. package/dist/chunk-V5GZQEIY.js +0 -101
  79. package/dist/chunk-V5GZQEIY.js.map +0 -1
  80. package/dist/chunk-VOIJ6OY4.js +0 -63
  81. package/dist/chunk-VOIJ6OY4.js.map +0 -1
  82. package/dist/chunk-VUNV25KB.js +0 -16
  83. package/dist/chunk-VUNV25KB.js.map +0 -1
  84. package/dist/chunk-W3EYKZNQ.js +0 -45
  85. package/dist/chunk-W3EYKZNQ.js.map +0 -1
  86. package/dist/chunk-WTZDAYZX.js +0 -172
  87. package/dist/chunk-WTZDAYZX.js.map +0 -1
  88. package/dist/chunk-XANPHG7W.js +0 -145
  89. package/dist/chunk-XANPHG7W.js.map +0 -1
  90. package/dist/config-6S355X75.js +0 -15
  91. package/dist/config-6S355X75.js.map +0 -1
  92. package/dist/config-editor-RVLWZLVB.js +0 -13
  93. package/dist/config-editor-RVLWZLVB.js.map +0 -1
  94. package/dist/config-registry-AHYI4MYL.js +0 -18
  95. package/dist/config-registry-AHYI4MYL.js.map +0 -1
  96. package/dist/daemon-4CS6HMB5.js +0 -30
  97. package/dist/daemon-4CS6HMB5.js.map +0 -1
  98. package/dist/discord-7IVQKB2H.js +0 -2083
  99. package/dist/discord-7IVQKB2H.js.map +0 -1
  100. package/dist/dist-UHQK5CXN.js +0 -21151
  101. package/dist/dist-UHQK5CXN.js.map +0 -1
  102. package/dist/doctor-HZZ5BSHB.js +0 -10
  103. package/dist/doctor-HZZ5BSHB.js.map +0 -1
  104. package/dist/doctor-OLYBO3V3.js +0 -15
  105. package/dist/doctor-OLYBO3V3.js.map +0 -1
  106. package/dist/install-cloudflared-Z7VCGOVG.js +0 -33
  107. package/dist/install-cloudflared-Z7VCGOVG.js.map +0 -1
  108. package/dist/install-jq-HUYSQWKR.js +0 -32
  109. package/dist/install-jq-HUYSQWKR.js.map +0 -1
  110. package/dist/integrate-PNEHRY2I.js +0 -373
  111. package/dist/integrate-PNEHRY2I.js.map +0 -1
  112. package/dist/log-NXABYJTT.js +0 -24
  113. package/dist/log-NXABYJTT.js.map +0 -1
  114. package/dist/main-ZK4MPMBG.js +0 -238
  115. package/dist/main-ZK4MPMBG.js.map +0 -1
  116. package/dist/menu-YY5MKHEK.js +0 -16
  117. package/dist/menu-YY5MKHEK.js.map +0 -1
  118. package/dist/new-session-FEO4J4VU.js +0 -17
  119. package/dist/new-session-FEO4J4VU.js.map +0 -1
  120. package/dist/post-upgrade-CJG5I7M2.js +0 -80
  121. package/dist/post-upgrade-CJG5I7M2.js.map +0 -1
  122. package/dist/session-IUSI7P5S.js +0 -20
  123. package/dist/session-IUSI7P5S.js.map +0 -1
  124. package/dist/settings-RQPAM4KC.js +0 -14
  125. package/dist/settings-RQPAM4KC.js.map +0 -1
  126. package/dist/setup-3GQSYBE4.js +0 -35
  127. package/dist/setup-3GQSYBE4.js.map +0 -1
  128. package/dist/suggest-7D6B542M.js +0 -38
  129. package/dist/suggest-7D6B542M.js.map +0 -1
  130. package/dist/tunnel-service-CJLUH6SZ.js +0 -1174
  131. package/dist/tunnel-service-CJLUH6SZ.js.map +0 -1
  132. package/dist/version-NQZBM5M7.js +0 -16
  133. package/dist/version-NQZBM5M7.js.map +0 -1
@@ -1,4740 +0,0 @@
1
- import {
2
- PRODUCT_GUIDE,
3
- STATUS_ICONS,
4
- dispatchMessage,
5
- evaluateNoise,
6
- extractContentText,
7
- formatTokens,
8
- formatToolSummary,
9
- formatToolTitle,
10
- progressBar,
11
- splitMessage,
12
- stripCodeFences,
13
- truncateContent
14
- } from "./chunk-JUYDFUSN.js";
15
- import {
16
- CheckpointReader,
17
- DEFAULT_MAX_TOKENS
18
- } from "./chunk-LGP2YGRL.js";
19
- import {
20
- ChannelAdapter
21
- } from "./chunk-LBIKITQT.js";
22
- import {
23
- DoctorEngine
24
- } from "./chunk-NVPG6JCL.js";
25
- import {
26
- buildMenuKeyboard,
27
- buildSkillMessages,
28
- handleClear,
29
- handleHelp,
30
- handleMenu
31
- } from "./chunk-7QJS2XBD.js";
32
- import {
33
- getConfigValue,
34
- getSafeFields,
35
- isHotReloadable,
36
- resolveOptions
37
- } from "./chunk-JHYXKVV2.js";
38
- import {
39
- createChildLogger
40
- } from "./chunk-GAK6PIBW.js";
41
-
42
- // src/adapters/telegram/adapter.ts
43
- import { Bot, InputFile } from "grammy";
44
-
45
- // src/adapters/telegram/topics.ts
46
- async function ensureTopics(bot, chatId, config, saveConfig) {
47
- let notificationTopicId = config.notificationTopicId;
48
- let assistantTopicId = config.assistantTopicId;
49
- if (notificationTopicId === null) {
50
- const topic = await bot.api.createForumTopic(chatId, "\u{1F4CB} Notifications");
51
- notificationTopicId = topic.message_thread_id;
52
- await saveConfig({ notificationTopicId });
53
- }
54
- if (assistantTopicId === null) {
55
- const topic = await bot.api.createForumTopic(chatId, "\u{1F916} Assistant");
56
- assistantTopicId = topic.message_thread_id;
57
- await saveConfig({ assistantTopicId });
58
- }
59
- return { notificationTopicId, assistantTopicId };
60
- }
61
- async function createSessionTopic(bot, chatId, name) {
62
- const topic = await bot.api.createForumTopic(chatId, name);
63
- return topic.message_thread_id;
64
- }
65
- async function renameSessionTopic(bot, chatId, threadId, name) {
66
- try {
67
- await bot.api.editForumTopic(chatId, threadId, { name });
68
- } catch {
69
- }
70
- }
71
- async function deleteSessionTopic(bot, chatId, threadId) {
72
- await bot.api.deleteForumTopic(chatId, threadId);
73
- }
74
- function buildDeepLink(chatId, threadId, messageId) {
75
- const cleanId = String(chatId).replace("-100", "");
76
- if (messageId && messageId !== threadId) {
77
- return `https://t.me/c/${cleanId}/${threadId}/${messageId}`;
78
- }
79
- return `https://t.me/c/${cleanId}/${threadId}`;
80
- }
81
-
82
- // src/adapters/telegram/commands/new-session.ts
83
- import { InlineKeyboard as InlineKeyboard2 } from "grammy";
84
-
85
- // src/adapters/telegram/formatting.ts
86
- function escapeHtml(text) {
87
- if (!text) return "";
88
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
89
- }
90
- function markdownToTelegramHtml(md) {
91
- const codeBlocks = [];
92
- const inlineCodes = [];
93
- let text = md.replace(
94
- /```(\w*)\n?([\s\S]*?)```/g,
95
- (_match, lang, code) => {
96
- const index = codeBlocks.length;
97
- const escapedCode = escapeHtml(code);
98
- const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : "";
99
- codeBlocks.push(`<pre><code${langAttr}>${escapedCode}</code></pre>`);
100
- return `\0CODE_BLOCK_${index}\0`;
101
- }
102
- );
103
- text = text.replace(/`([^`]+)`/g, (_match, code) => {
104
- const index = inlineCodes.length;
105
- inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
106
- return `\0INLINE_CODE_${index}\0`;
107
- });
108
- text = escapeHtml(text);
109
- text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
110
- text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<i>$1</i>");
111
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
112
- text = text.replace(/\x00CODE_BLOCK_(\d+)\x00/g, (_match, idx) => {
113
- return codeBlocks[parseInt(idx, 10)];
114
- });
115
- text = text.replace(/\x00INLINE_CODE_(\d+)\x00/g, (_match, idx) => {
116
- return inlineCodes[parseInt(idx, 10)];
117
- });
118
- return text;
119
- }
120
- function formatToolCall(tool, verbosity = "medium") {
121
- const si = STATUS_ICONS[tool.status || ""] || "\u{1F527}";
122
- const name = tool.name || "Tool";
123
- const label = verbosity === "low" ? formatToolTitle(name, tool.rawInput) : formatToolSummary(name, tool.rawInput);
124
- let text = `${si} <b>${escapeHtml(label)}</b>`;
125
- text += formatViewerLinks(tool.viewerLinks, tool.viewerFilePath);
126
- if (verbosity === "high" || verbosity === "medium" && !tool.viewerLinks) {
127
- const details = stripCodeFences(extractContentText(tool.content));
128
- if (details) {
129
- text += `
130
- <pre>${escapeHtml(truncateContent(details, 3800))}</pre>`;
131
- }
132
- }
133
- return text;
134
- }
135
- function formatToolUpdate(update, verbosity = "medium") {
136
- const si = STATUS_ICONS[update.status] || "\u{1F527}";
137
- const name = update.name || "Tool";
138
- const label = verbosity === "low" ? formatToolTitle(name, update.rawInput) : formatToolSummary(name, update.rawInput);
139
- let text = `${si} <b>${escapeHtml(label)}</b>`;
140
- text += formatViewerLinks(update.viewerLinks, update.viewerFilePath);
141
- if (verbosity === "high" || verbosity === "medium" && !update.viewerLinks) {
142
- const details = stripCodeFences(extractContentText(update.content));
143
- if (details) {
144
- text += `
145
- <pre>${escapeHtml(truncateContent(details, 3800))}</pre>`;
146
- }
147
- }
148
- return text;
149
- }
150
- function formatViewerLinks(links, filePath) {
151
- if (!links) return "";
152
- const fileName = filePath ? filePath.split("/").pop() || filePath : "";
153
- let text = "\n";
154
- if (links.file)
155
- text += `
156
- \u{1F4C4} <a href="${escapeHtml(links.file)}">View ${escapeHtml(fileName || "file")}</a>`;
157
- if (links.diff)
158
- text += `
159
- \u{1F4DD} <a href="${escapeHtml(links.diff)}">View diff${fileName ? ` \u2014 ${escapeHtml(fileName)}` : ""}</a>`;
160
- return text;
161
- }
162
- function formatUsage(usage) {
163
- const { tokensUsed, contextSize } = usage;
164
- if (tokensUsed == null) return "\u{1F4CA} Usage data unavailable";
165
- if (contextSize == null) return `\u{1F4CA} ${formatTokens(tokensUsed)} tokens`;
166
- const ratio = tokensUsed / contextSize;
167
- const pct = Math.round(ratio * 100);
168
- const bar = progressBar(ratio);
169
- const emoji = pct >= 85 ? "\u26A0\uFE0F" : "\u{1F4CA}";
170
- return `${emoji} ${formatTokens(tokensUsed)} / ${formatTokens(contextSize)} tokens
171
- ${bar} ${pct}%`;
172
- }
173
- var PERIOD_LABEL = {
174
- today: "Today",
175
- week: "This Week",
176
- month: "This Month",
177
- all: "All Time"
178
- };
179
- function formatUsageReport(summaries, budgetStatus) {
180
- const hasData = summaries.some((s) => s.recordCount > 0);
181
- if (!hasData) {
182
- return "\u{1F4CA} <b>Usage Report</b>\n\nNo usage data yet.";
183
- }
184
- const formatCost = (n) => `$${n.toFixed(2)}`;
185
- const lines = ["\u{1F4CA} <b>Usage Report</b>"];
186
- for (const summary of summaries) {
187
- lines.push("");
188
- lines.push(
189
- `\u2500\u2500 <b>${PERIOD_LABEL[summary.period] ?? summary.period}</b> \u2500\u2500`
190
- );
191
- lines.push(
192
- `\u{1F4B0} ${formatCost(summary.totalCost)} \xB7 \u{1F524} ${formatTokens(summary.totalTokens)} tokens \xB7 \u{1F4CB} ${summary.sessionCount} sessions`
193
- );
194
- if (summary.period === "month" && budgetStatus.budget > 0) {
195
- const bar = progressBar(budgetStatus.used / budgetStatus.budget);
196
- lines.push(
197
- `Budget: ${formatCost(budgetStatus.used)} / ${formatCost(budgetStatus.budget)} (${budgetStatus.percent}%)`
198
- );
199
- lines.push(`${bar} ${budgetStatus.percent}%`);
200
- }
201
- }
202
- return lines.join("\n");
203
- }
204
- function formatSummary(summary, sessionName) {
205
- const header = sessionName ? `\u{1F4CB} <b>Summary \u2014 ${escapeHtml(sessionName)}</b>` : "\u{1F4CB} <b>Session Summary</b>";
206
- return `${header}
207
-
208
- ${escapeHtml(summary)}`;
209
- }
210
- function splitMessage2(text, maxLength = 3800) {
211
- return splitMessage(text, maxLength);
212
- }
213
-
214
- // src/adapters/telegram/commands/admin.ts
215
- import { InlineKeyboard } from "grammy";
216
- var log = createChildLogger({ module: "telegram-cmd-admin" });
217
- function setupDangerousModeCallbacks(bot, core) {
218
- bot.callbackQuery(/^d:/, async (ctx) => {
219
- const sessionId = ctx.callbackQuery.data.slice(2);
220
- const session = core.sessionManager.getSession(sessionId);
221
- if (session) {
222
- session.dangerousMode = !session.dangerousMode;
223
- log.info(
224
- { sessionId, dangerousMode: session.dangerousMode },
225
- "Dangerous mode toggled via button"
226
- );
227
- core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
228
- });
229
- const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
230
- try {
231
- await ctx.answerCallbackQuery({ text: toastText2 });
232
- } catch {
233
- }
234
- try {
235
- await ctx.editMessageReplyMarkup({
236
- reply_markup: buildSessionControlKeyboard(
237
- sessionId,
238
- session.dangerousMode,
239
- session.voiceMode === "on"
240
- )
241
- });
242
- } catch {
243
- }
244
- return;
245
- }
246
- const record = core.sessionManager.getSessionRecord(sessionId);
247
- if (!record || record.status === "cancelled" || record.status === "error") {
248
- try {
249
- await ctx.answerCallbackQuery({
250
- text: "\u26A0\uFE0F Session not found or already ended."
251
- });
252
- } catch {
253
- }
254
- return;
255
- }
256
- const newDangerousMode = !(record.dangerousMode ?? false);
257
- core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
258
- });
259
- log.info(
260
- { sessionId, dangerousMode: newDangerousMode },
261
- "Dangerous mode toggled via button (store-only, session not in memory)"
262
- );
263
- const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
264
- try {
265
- await ctx.answerCallbackQuery({ text: toastText });
266
- } catch {
267
- }
268
- try {
269
- await ctx.editMessageReplyMarkup({
270
- reply_markup: buildSessionControlKeyboard(
271
- sessionId,
272
- newDangerousMode,
273
- false
274
- )
275
- });
276
- } catch {
277
- }
278
- });
279
- }
280
- async function handleEnableDangerous(ctx, core) {
281
- const threadId = ctx.message?.message_thread_id;
282
- if (!threadId) {
283
- await ctx.reply("\u26A0\uFE0F This command only works inside a session topic.", {
284
- parse_mode: "HTML"
285
- });
286
- return;
287
- }
288
- const session = core.sessionManager.getSessionByThread(
289
- "telegram",
290
- String(threadId)
291
- );
292
- if (session) {
293
- if (session.dangerousMode) {
294
- await ctx.reply("\u2620\uFE0F Dangerous mode is already enabled.", {
295
- parse_mode: "HTML"
296
- });
297
- return;
298
- }
299
- session.dangerousMode = true;
300
- core.sessionManager.patchRecord(session.id, { dangerousMode: true }).catch(() => {
301
- });
302
- } else {
303
- const record = core.sessionManager.getRecordByThread(
304
- "telegram",
305
- String(threadId)
306
- );
307
- if (!record || record.status === "cancelled" || record.status === "error") {
308
- await ctx.reply("\u26A0\uFE0F No active session in this topic.", {
309
- parse_mode: "HTML"
310
- });
311
- return;
312
- }
313
- if (record.dangerousMode) {
314
- await ctx.reply("\u2620\uFE0F Dangerous mode is already enabled.", {
315
- parse_mode: "HTML"
316
- });
317
- return;
318
- }
319
- core.sessionManager.patchRecord(record.sessionId, { dangerousMode: true }).catch(() => {
320
- });
321
- }
322
- await ctx.reply(
323
- `\u26A0\uFE0F <b>Dangerous mode enabled</b>
324
-
325
- All permission requests will be auto-approved. Claude can run arbitrary commands without asking.
326
-
327
- Use /disable_dangerous to restore normal behaviour.`,
328
- { parse_mode: "HTML" }
329
- );
330
- }
331
- async function handleDisableDangerous(ctx, core) {
332
- const threadId = ctx.message?.message_thread_id;
333
- if (!threadId) {
334
- await ctx.reply("\u26A0\uFE0F This command only works inside a session topic.", {
335
- parse_mode: "HTML"
336
- });
337
- return;
338
- }
339
- const session = core.sessionManager.getSessionByThread(
340
- "telegram",
341
- String(threadId)
342
- );
343
- if (session) {
344
- if (!session.dangerousMode) {
345
- await ctx.reply("\u{1F510} Dangerous mode is already disabled.", {
346
- parse_mode: "HTML"
347
- });
348
- return;
349
- }
350
- session.dangerousMode = false;
351
- core.sessionManager.patchRecord(session.id, { dangerousMode: false }).catch(() => {
352
- });
353
- } else {
354
- const record = core.sessionManager.getRecordByThread(
355
- "telegram",
356
- String(threadId)
357
- );
358
- if (!record || record.status === "cancelled" || record.status === "error") {
359
- await ctx.reply("\u26A0\uFE0F No active session in this topic.", {
360
- parse_mode: "HTML"
361
- });
362
- return;
363
- }
364
- if (!record.dangerousMode) {
365
- await ctx.reply("\u{1F510} Dangerous mode is already disabled.", {
366
- parse_mode: "HTML"
367
- });
368
- return;
369
- }
370
- core.sessionManager.patchRecord(record.sessionId, { dangerousMode: false }).catch(() => {
371
- });
372
- }
373
- await ctx.reply(
374
- "\u{1F510} <b>Dangerous mode disabled</b>\n\nPermission requests will be shown normally.",
375
- { parse_mode: "HTML" }
376
- );
377
- }
378
- function buildSessionControlKeyboard(sessionId, dangerousMode, voiceMode) {
379
- return new InlineKeyboard().text(
380
- dangerousMode ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
381
- `d:${sessionId}`
382
- ).row().text(
383
- voiceMode ? "\u{1F50A} Text to Speech" : "\u{1F507} Text to Speech",
384
- `v:${sessionId}`
385
- );
386
- }
387
- function setupTTSCallbacks(bot, core) {
388
- bot.callbackQuery(/^v:/, async (ctx) => {
389
- const sessionId = ctx.callbackQuery.data.slice(2);
390
- const session = core.sessionManager.getSession(sessionId);
391
- if (!session) {
392
- try {
393
- await ctx.answerCallbackQuery({
394
- text: "\u26A0\uFE0F Session not found or not active."
395
- });
396
- } catch {
397
- }
398
- return;
399
- }
400
- const newMode = session.voiceMode === "on" ? "off" : "on";
401
- session.setVoiceMode(newMode);
402
- const toastText = newMode === "on" ? "\u{1F50A} Text to Speech enabled" : "\u{1F507} Text to Speech disabled";
403
- try {
404
- await ctx.answerCallbackQuery({ text: toastText });
405
- } catch {
406
- }
407
- try {
408
- await ctx.editMessageReplyMarkup({
409
- reply_markup: buildSessionControlKeyboard(
410
- sessionId,
411
- session.dangerousMode,
412
- newMode === "on"
413
- )
414
- });
415
- } catch {
416
- }
417
- });
418
- }
419
- async function handleTTS(ctx, core) {
420
- const threadId = ctx.message?.message_thread_id;
421
- if (!threadId) {
422
- await ctx.reply("\u26A0\uFE0F This command only works inside a session topic.", {
423
- parse_mode: "HTML"
424
- });
425
- return;
426
- }
427
- const session = await core.getOrResumeSession("telegram", String(threadId));
428
- if (!session) {
429
- await ctx.reply("\u26A0\uFE0F No active session in this topic.", {
430
- parse_mode: "HTML"
431
- });
432
- return;
433
- }
434
- const args = ctx.message?.text?.split(/\s+/).slice(1) ?? [];
435
- const arg = args[0]?.toLowerCase();
436
- if (arg === "on") {
437
- session.setVoiceMode("on");
438
- await ctx.reply("\u{1F50A} Text to Speech enabled for this session.", {
439
- parse_mode: "HTML"
440
- });
441
- } else if (arg === "off") {
442
- session.setVoiceMode("off");
443
- await ctx.reply("\u{1F507} Text to Speech disabled.", { parse_mode: "HTML" });
444
- } else {
445
- session.setVoiceMode("next");
446
- await ctx.reply("\u{1F50A} Text to Speech enabled for the next message.", {
447
- parse_mode: "HTML"
448
- });
449
- }
450
- }
451
- var VERBOSITY_LABELS = {
452
- low: "\u{1F507} Low",
453
- medium: "\u{1F4CA} Medium",
454
- high: "\u{1F4D6} High"
455
- };
456
- async function handleVerbosity(ctx, core) {
457
- const args = ctx.message?.text?.split(/\s+/).slice(1) ?? [];
458
- const arg = args[0]?.toLowerCase();
459
- if (arg === "low" || arg === "medium" || arg === "high") {
460
- await core.configManager.save(
461
- { channels: { telegram: { displayVerbosity: arg } } },
462
- "channels.telegram.displayVerbosity"
463
- );
464
- await ctx.reply(
465
- `${VERBOSITY_LABELS[arg]} Display verbosity set to <b>${arg}</b>.`,
466
- { parse_mode: "HTML" }
467
- );
468
- } else {
469
- const current = core.configManager.get().channels?.telegram?.displayVerbosity ?? "medium";
470
- await ctx.reply(
471
- `\u{1F4CA} Current verbosity: <b>${current}</b>
472
-
473
- Usage: <code>/verbosity low|medium|high</code>
474
-
475
- \u2022 <b>low</b> \u2014 minimal output, title only
476
- \u2022 <b>medium</b> \u2014 balanced (default)
477
- \u2022 <b>high</b> \u2014 full detail with content`,
478
- { parse_mode: "HTML" }
479
- );
480
- }
481
- }
482
- function setupVerbosityCallbacks(bot, core) {
483
- bot.callbackQuery(/^vb:/, async (ctx) => {
484
- const level = ctx.callbackQuery.data.slice(3);
485
- if (level !== "low" && level !== "medium" && level !== "high") return;
486
- await core.configManager.save(
487
- { channels: { telegram: { displayVerbosity: level } } },
488
- "channels.telegram.displayVerbosity"
489
- );
490
- try {
491
- await ctx.answerCallbackQuery({
492
- text: `${VERBOSITY_LABELS[level]} Verbosity: ${level}`
493
- });
494
- } catch {
495
- }
496
- });
497
- }
498
- async function handleUpdate(ctx, core) {
499
- if (!core.requestRestart) {
500
- await ctx.reply(
501
- "\u26A0\uFE0F Update is not available (no restart handler registered).",
502
- { parse_mode: "HTML" }
503
- );
504
- return;
505
- }
506
- const { getCurrentVersion, getLatestVersion, compareVersions, runUpdate } = await import("./version-NQZBM5M7.js");
507
- const current = getCurrentVersion();
508
- const statusMsg = await ctx.reply(
509
- `\u{1F50D} Checking for updates... (current: v${escapeHtml(current)})`,
510
- { parse_mode: "HTML" }
511
- );
512
- const latest = await getLatestVersion();
513
- if (!latest) {
514
- await ctx.api.editMessageText(
515
- ctx.chat.id,
516
- statusMsg.message_id,
517
- "\u274C Could not check for updates.",
518
- { parse_mode: "HTML" }
519
- );
520
- return;
521
- }
522
- if (compareVersions(current, latest) >= 0) {
523
- await ctx.api.editMessageText(
524
- ctx.chat.id,
525
- statusMsg.message_id,
526
- `\u2705 Already up to date (v${escapeHtml(current)}).`,
527
- { parse_mode: "HTML" }
528
- );
529
- return;
530
- }
531
- await ctx.api.editMessageText(
532
- ctx.chat.id,
533
- statusMsg.message_id,
534
- `\u2B07\uFE0F Updating v${escapeHtml(current)} \u2192 v${escapeHtml(latest)}...`,
535
- { parse_mode: "HTML" }
536
- );
537
- const ok = await runUpdate();
538
- if (!ok) {
539
- await ctx.api.editMessageText(
540
- ctx.chat.id,
541
- statusMsg.message_id,
542
- "\u274C Update failed. Try manually: <code>npm install -g @openacp/cli@latest</code>",
543
- { parse_mode: "HTML" }
544
- );
545
- return;
546
- }
547
- await ctx.api.editMessageText(
548
- ctx.chat.id,
549
- statusMsg.message_id,
550
- `\u2705 Updated to v${escapeHtml(latest)}. Restarting...`,
551
- { parse_mode: "HTML" }
552
- );
553
- await new Promise((r) => setTimeout(r, 500));
554
- await core.requestRestart();
555
- }
556
- async function handleRestart(ctx, core) {
557
- if (!core.requestRestart) {
558
- await ctx.reply(
559
- "\u26A0\uFE0F Restart is not available (no restart handler registered).",
560
- { parse_mode: "HTML" }
561
- );
562
- return;
563
- }
564
- await ctx.reply(
565
- "\u{1F504} <b>Restarting OpenACP...</b>\nRebuilding and restarting. Be back shortly.",
566
- { parse_mode: "HTML" }
567
- );
568
- await new Promise((r) => setTimeout(r, 500));
569
- await core.requestRestart();
570
- }
571
-
572
- // src/adapters/telegram/commands/new-session.ts
573
- var log2 = createChildLogger({ module: "telegram-cmd-new-session" });
574
- var pendingNewSessions = /* @__PURE__ */ new Map();
575
- var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
576
- function cleanupPending(userId) {
577
- const pending = pendingNewSessions.get(userId);
578
- if (pending) {
579
- clearTimeout(pending.timer);
580
- pendingNewSessions.delete(userId);
581
- }
582
- }
583
- function botFromCtx(ctx) {
584
- return { api: ctx.api };
585
- }
586
- async function handleNew(ctx, core, chatId, assistant) {
587
- const rawMatch = ctx.match;
588
- const matchStr = typeof rawMatch === "string" ? rawMatch : "";
589
- const args = matchStr.split(" ").filter(Boolean);
590
- const agentName = args[0];
591
- const workspace = args[1];
592
- if (agentName && workspace) {
593
- await createSessionDirect(ctx, core, chatId, agentName, workspace);
594
- return;
595
- }
596
- const currentThreadId = ctx.message?.message_thread_id;
597
- if (assistant && currentThreadId === assistant.topicId) {
598
- const assistantSession = assistant.getSession();
599
- if (assistantSession) {
600
- const prompt = agentName ? `User wants to create a new session with agent "${agentName}" but didn't specify a workspace. Ask them which project directory to use as workspace.` : `User wants to create a new session. Guide them through choosing an agent and workspace (project directory).`;
601
- await assistantSession.enqueuePrompt(prompt);
602
- return;
603
- }
604
- }
605
- await showAgentPicker(ctx, core, chatId, agentName);
606
- }
607
- async function startWorkspaceStep(ctx, core, chatId, userId, agentName) {
608
- const config = core.configManager.get();
609
- const baseDir = config.workspace.baseDir;
610
- const keyboard = new InlineKeyboard2().text(`\u{1F4C1} Use ${baseDir}`, "m:new:ws:default").row().text("\u270F\uFE0F Enter project path", "m:new:ws:custom");
611
- const text = `\u{1F4C1} <b>Where should ${escapeHtml(agentName)} work?</b>
612
-
613
- Enter the path to your project folder \u2014 the agent will read, write, and run code there.
614
-
615
- Or use the default directory below:`;
616
- let msg;
617
- try {
618
- const pending = pendingNewSessions.get(userId);
619
- if (pending?.messageId) {
620
- await ctx.api.editMessageText(chatId, pending.messageId, text, {
621
- parse_mode: "HTML",
622
- reply_markup: keyboard
623
- });
624
- msg = { message_id: pending.messageId };
625
- } else {
626
- msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
627
- }
628
- } catch {
629
- msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
630
- }
631
- cleanupPending(userId);
632
- pendingNewSessions.set(userId, {
633
- agentName,
634
- step: "workspace",
635
- messageId: msg.message_id,
636
- threadId: ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id,
637
- timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
638
- });
639
- }
640
- async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
641
- const keyboard = new InlineKeyboard2().text("\u2705 Create", "m:new:confirm").text("\u274C Cancel", "m:new:cancel");
642
- const text = `\u2705 <b>Ready to create session?</b>
643
-
644
- <b>Agent:</b> ${escapeHtml(agentName)}
645
- <b>Project:</b> <code>${escapeHtml(workspace)}</code>`;
646
- let msg;
647
- try {
648
- const pending = pendingNewSessions.get(userId);
649
- if (pending?.messageId) {
650
- await ctx.api.editMessageText(chatId, pending.messageId, text, {
651
- parse_mode: "HTML",
652
- reply_markup: keyboard
653
- });
654
- msg = { message_id: pending.messageId };
655
- } else {
656
- msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
657
- }
658
- } catch {
659
- msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
660
- }
661
- cleanupPending(userId);
662
- pendingNewSessions.set(userId, {
663
- agentName,
664
- workspace,
665
- step: "confirm",
666
- messageId: msg.message_id,
667
- threadId: ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id,
668
- timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
669
- });
670
- }
671
- async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
672
- log2.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
673
- let threadId;
674
- try {
675
- const topicName = `\u{1F504} New Session`;
676
- threadId = await createSessionTopic(botFromCtx(ctx), chatId, topicName);
677
- await ctx.api.sendMessage(chatId, `\u23F3 Setting up session, please wait...`, {
678
- message_thread_id: threadId,
679
- parse_mode: "HTML"
680
- });
681
- const session = await core.handleNewSession("telegram", agentName, workspace);
682
- session.threadId = String(threadId);
683
- await core.sessionManager.patchRecord(session.id, { platform: { topicId: threadId } });
684
- const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
685
- try {
686
- await ctx.api.editForumTopic(chatId, threadId, { name: finalName });
687
- } catch {
688
- }
689
- await ctx.api.sendMessage(
690
- chatId,
691
- `\u2705 <b>Session started</b>
692
- <b>Agent:</b> ${escapeHtml(session.agentName)}
693
- <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>
694
-
695
- This is your coding session \u2014 chat here to work with the agent.`,
696
- {
697
- message_thread_id: threadId,
698
- parse_mode: "HTML",
699
- reply_markup: buildSessionControlKeyboard(session.id, false, false)
700
- }
701
- );
702
- session.warmup().catch((err) => log2.error({ err }, "Warm-up error"));
703
- return threadId ?? null;
704
- } catch (err) {
705
- log2.error({ err }, "Session creation failed");
706
- if (threadId) {
707
- try {
708
- await ctx.api.deleteForumTopic(chatId, threadId);
709
- } catch {
710
- }
711
- }
712
- const message = err instanceof Error ? err.message : typeof err === "object" ? JSON.stringify(err) : String(err);
713
- await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
714
- return null;
715
- }
716
- }
717
- async function handleNewChat(ctx, core, chatId) {
718
- const threadId = ctx.message?.message_thread_id;
719
- if (!threadId) {
720
- await ctx.reply(
721
- "Use /newchat inside a session topic to inherit its config.",
722
- { parse_mode: "HTML" }
723
- );
724
- return;
725
- }
726
- const currentSession = core.sessionManager.getSessionByThread(
727
- "telegram",
728
- String(threadId)
729
- );
730
- let agentName;
731
- let workspace;
732
- if (currentSession) {
733
- agentName = currentSession.agentName;
734
- workspace = currentSession.workingDirectory;
735
- } else {
736
- const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
737
- if (!record || record.status === "cancelled" || record.status === "error") {
738
- await ctx.reply("No active session in this topic.", {
739
- parse_mode: "HTML"
740
- });
741
- return;
742
- }
743
- agentName = record.agentName;
744
- workspace = record.workingDir;
745
- }
746
- let newThreadId;
747
- try {
748
- const topicName = `\u{1F504} ${agentName} \u2014 New Chat`;
749
- newThreadId = await createSessionTopic(
750
- botFromCtx(ctx),
751
- chatId,
752
- topicName
753
- );
754
- const topicLink = buildDeepLink(chatId, newThreadId);
755
- await ctx.reply(
756
- `\u2705 New chat created \u2192 <a href="${topicLink}">Open topic</a>`,
757
- { parse_mode: "HTML" }
758
- );
759
- await ctx.api.sendMessage(chatId, `\u23F3 Setting up session, please wait...`, {
760
- message_thread_id: newThreadId,
761
- parse_mode: "HTML"
762
- });
763
- const session = await core.handleNewSession(
764
- "telegram",
765
- agentName,
766
- workspace
767
- );
768
- session.threadId = String(newThreadId);
769
- await core.sessionManager.patchRecord(session.id, { platform: {
770
- topicId: newThreadId
771
- } });
772
- await ctx.api.sendMessage(
773
- chatId,
774
- `\u2705 New chat (same agent &amp; workspace)
775
- <b>Agent:</b> ${escapeHtml(session.agentName)}
776
- <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>`,
777
- {
778
- message_thread_id: newThreadId,
779
- parse_mode: "HTML",
780
- reply_markup: buildSessionControlKeyboard(session.id, false, false)
781
- }
782
- );
783
- session.warmup().catch((err) => log2.error({ err }, "Warm-up error"));
784
- } catch (err) {
785
- if (newThreadId) {
786
- try {
787
- await ctx.api.deleteForumTopic(chatId, newThreadId);
788
- } catch {
789
- }
790
- }
791
- const message = err instanceof Error ? err.message : String(err);
792
- await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
793
- }
794
- }
795
- async function executeNewSession(bot, core, chatId, agentName, workspace) {
796
- const threadId = await createSessionTopic(bot, chatId, "\u{1F504} New Session");
797
- const setupMsg = await bot.api.sendMessage(chatId, "\u23F3 Setting up session, please wait...", {
798
- message_thread_id: threadId,
799
- parse_mode: "HTML"
800
- });
801
- const firstMsgId = setupMsg.message_id;
802
- try {
803
- const session = await core.handleNewSession(
804
- "telegram",
805
- agentName,
806
- workspace
807
- );
808
- session.threadId = String(threadId);
809
- await core.sessionManager.patchRecord(session.id, { platform: {
810
- topicId: threadId
811
- } });
812
- const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
813
- await renameSessionTopic(bot, chatId, threadId, finalName);
814
- session.warmup().catch((err) => log2.error({ err }, "Warm-up error"));
815
- return { session, threadId, firstMsgId };
816
- } catch (err) {
817
- try {
818
- await bot.api.deleteForumTopic(chatId, threadId);
819
- } catch {
820
- }
821
- throw err;
822
- }
823
- }
824
- async function handlePendingWorkspaceInput(ctx, core, chatId, assistantTopicId) {
825
- const userId = ctx.from?.id;
826
- if (!userId) return false;
827
- const pending = pendingNewSessions.get(userId);
828
- if (!pending || !ctx.message?.text) return false;
829
- if (pending.step !== "workspace_input" && pending.step !== "workspace") return false;
830
- const threadId = ctx.message.message_thread_id;
831
- if (threadId && threadId !== assistantTopicId) return false;
832
- let workspace = ctx.message.text.trim();
833
- if (!workspace || !pending.agentName) {
834
- await ctx.reply("\u26A0\uFE0F Please enter a valid directory path.", { parse_mode: "HTML" });
835
- return true;
836
- }
837
- if (!workspace.startsWith("/") && !workspace.startsWith("~")) {
838
- const baseDir = core.configManager.get().workspace.baseDir;
839
- workspace = `${baseDir.replace(/\/$/, "")}/${workspace}`;
840
- }
841
- await startConfirmStep(ctx, chatId, userId, pending.agentName, workspace);
842
- return true;
843
- }
844
- async function startInteractiveNewSession(ctx, core, chatId, agentName) {
845
- await showAgentPicker(ctx, core, chatId, agentName);
846
- }
847
- async function showAgentPicker(ctx, core, chatId, agentName) {
848
- const userId = ctx.from?.id;
849
- if (!userId) return;
850
- const installedEntries = core.agentCatalog.getInstalledEntries();
851
- const agentKeys = Object.keys(installedEntries);
852
- const config = core.configManager.get();
853
- if (agentName || agentKeys.length === 1) {
854
- const selectedAgent = agentName || config.defaultAgent;
855
- await startWorkspaceStep(ctx, core, chatId, userId, selectedAgent);
856
- return;
857
- }
858
- const keyboard = new InlineKeyboard2();
859
- for (const key of agentKeys) {
860
- const agent = installedEntries[key];
861
- const label = key === config.defaultAgent ? `${agent.name} (default)` : agent.name;
862
- keyboard.text(label, `m:new:agent:${key}`).row();
863
- }
864
- const msg = await ctx.reply(
865
- `\u{1F916} <b>Choose an agent:</b>`,
866
- { parse_mode: "HTML", reply_markup: keyboard }
867
- );
868
- cleanupPending(userId);
869
- const threadId = ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id;
870
- pendingNewSessions.set(userId, {
871
- step: "agent",
872
- messageId: msg.message_id,
873
- threadId,
874
- timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
875
- });
876
- }
877
- function setupNewSessionCallbacks(bot, core, chatId) {
878
- bot.callbackQuery(/^m:new:/, async (ctx) => {
879
- const data = ctx.callbackQuery.data;
880
- try {
881
- await ctx.answerCallbackQuery();
882
- } catch {
883
- }
884
- if (data.startsWith("m:new:agent:")) {
885
- const agentName = data.replace("m:new:agent:", "");
886
- const userId = ctx.from?.id;
887
- if (userId) await startWorkspaceStep(ctx, core, chatId, userId, agentName);
888
- return;
889
- }
890
- if (data === "m:new:ws:default") {
891
- const userId = ctx.from?.id;
892
- if (!userId) return;
893
- const pending = pendingNewSessions.get(userId);
894
- if (!pending?.agentName) return;
895
- const workspace = core.configManager.get().workspace.baseDir;
896
- await startConfirmStep(ctx, chatId, userId, pending.agentName, workspace);
897
- return;
898
- }
899
- if (data === "m:new:ws:custom") {
900
- const userId = ctx.from?.id;
901
- if (!userId) return;
902
- const pending = pendingNewSessions.get(userId);
903
- if (!pending?.agentName) return;
904
- try {
905
- await ctx.api.editMessageText(
906
- chatId,
907
- pending.messageId,
908
- `\u270F\uFE0F <b>Enter your project path:</b>
909
-
910
- Full path like <code>~/code/my-project</code>
911
- Or just the folder name like <code>my-project</code> (will use ${core.configManager.get().workspace.baseDir}/)`,
912
- { parse_mode: "HTML" }
913
- );
914
- } catch {
915
- await ctx.reply(
916
- `\u270F\uFE0F <b>Enter your project path:</b>`,
917
- { parse_mode: "HTML" }
918
- );
919
- }
920
- clearTimeout(pending.timer);
921
- pending.step = "workspace_input";
922
- pending.timer = setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS);
923
- return;
924
- }
925
- if (data === "m:new:confirm") {
926
- const userId = ctx.from?.id;
927
- if (!userId) return;
928
- const pending = pendingNewSessions.get(userId);
929
- if (!pending?.agentName || !pending?.workspace) return;
930
- cleanupPending(userId);
931
- const confirmMsgId = pending.messageId;
932
- try {
933
- await ctx.api.editMessageText(chatId, confirmMsgId, `\u23F3 Creating session...`, { parse_mode: "HTML" });
934
- } catch {
935
- }
936
- const resultThreadId = await createSessionDirect(ctx, core, chatId, pending.agentName, pending.workspace);
937
- try {
938
- if (resultThreadId) {
939
- const link = buildDeepLink(chatId, resultThreadId);
940
- await ctx.api.editMessageText(chatId, confirmMsgId, `\u2705 Session created \u2192 <a href="${link}">Open topic</a>`, { parse_mode: "HTML" });
941
- } else {
942
- await ctx.api.editMessageText(chatId, confirmMsgId, `\u274C Session creation failed.`, { parse_mode: "HTML" });
943
- }
944
- } catch {
945
- }
946
- return;
947
- }
948
- if (data === "m:new:cancel") {
949
- const userId = ctx.from?.id;
950
- if (userId) cleanupPending(userId);
951
- try {
952
- await ctx.editMessageText("\u274C Session creation cancelled.", { parse_mode: "HTML" });
953
- } catch {
954
- }
955
- return;
956
- }
957
- });
958
- }
959
-
960
- // src/adapters/telegram/commands/session.ts
961
- import { InlineKeyboard as InlineKeyboard3 } from "grammy";
962
- var log3 = createChildLogger({ module: "telegram-cmd-session" });
963
- async function handleCancel(ctx, core, assistant) {
964
- const threadId = ctx.message?.message_thread_id;
965
- if (!threadId) return;
966
- if (assistant && threadId === assistant.topicId) {
967
- const assistantSession = assistant.getSession();
968
- if (assistantSession) {
969
- await assistantSession.enqueuePrompt(
970
- "User wants to cancel a session. Confirm which session to cancel."
971
- );
972
- return;
973
- }
974
- }
975
- const session = core.sessionManager.getSessionByThread(
976
- "telegram",
977
- String(threadId)
978
- );
979
- if (session) {
980
- log3.info({ sessionId: session.id }, "Abort prompt command");
981
- await session.abortPrompt();
982
- await ctx.reply("\u26D4 Prompt aborted. Session is still active \u2014 send a new message to continue.", { parse_mode: "HTML" });
983
- return;
984
- }
985
- const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
986
- if (record && record.status !== "error") {
987
- log3.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
988
- await ctx.reply("\u2139\uFE0F No active prompt to cancel. Send a new message to resume the session.", { parse_mode: "HTML" });
989
- }
990
- }
991
- async function handleStatus(ctx, core) {
992
- const threadId = ctx.message?.message_thread_id;
993
- if (threadId) {
994
- const session = core.sessionManager.getSessionByThread(
995
- "telegram",
996
- String(threadId)
997
- );
998
- if (session) {
999
- await ctx.reply(
1000
- `<b>Session:</b> ${escapeHtml(session.name || session.id)}
1001
- <b>Agent:</b> ${escapeHtml(session.agentName)}
1002
- <b>Status:</b> ${escapeHtml(session.status)}
1003
- <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>
1004
- <b>Queue:</b> ${session.queueDepth} pending`,
1005
- { parse_mode: "HTML" }
1006
- );
1007
- } else {
1008
- const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
1009
- if (record) {
1010
- await ctx.reply(
1011
- `<b>Session:</b> ${escapeHtml(record.name || record.sessionId)}
1012
- <b>Agent:</b> ${escapeHtml(record.agentName)}
1013
- <b>Status:</b> ${escapeHtml(record.status)} (not loaded)
1014
- <b>Workspace:</b> <code>${escapeHtml(record.workingDir)}</code>`,
1015
- { parse_mode: "HTML" }
1016
- );
1017
- } else {
1018
- await ctx.reply("No active session in this topic.", {
1019
- parse_mode: "HTML"
1020
- });
1021
- }
1022
- }
1023
- } else {
1024
- const sessions = core.sessionManager.listSessions("telegram");
1025
- const active = sessions.filter(
1026
- (s) => s.status === "active" || s.status === "initializing"
1027
- );
1028
- await ctx.reply(
1029
- `<b>OpenACP Status</b>
1030
- Active sessions: ${active.length}
1031
- Total sessions: ${sessions.length}`,
1032
- { parse_mode: "HTML" }
1033
- );
1034
- }
1035
- }
1036
- async function handleTopics(ctx, core) {
1037
- try {
1038
- const allRecords = core.sessionManager.listRecords();
1039
- const records = allRecords.filter((r) => {
1040
- const platform = r.platform;
1041
- return !!platform?.topicId;
1042
- });
1043
- const headlessCount = allRecords.length - records.length;
1044
- if (records.length === 0) {
1045
- const extra = headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "";
1046
- await ctx.reply(`No sessions with topics found.${extra}`, { parse_mode: "HTML" });
1047
- return;
1048
- }
1049
- const statusEmoji = {
1050
- active: "\u{1F7E2}",
1051
- initializing: "\u{1F7E1}",
1052
- finished: "\u2705",
1053
- error: "\u274C",
1054
- cancelled: "\u26D4"
1055
- };
1056
- const statusOrder = { active: 0, initializing: 1, error: 2, finished: 3, cancelled: 4 };
1057
- records.sort((a, b) => (statusOrder[a.status] ?? 5) - (statusOrder[b.status] ?? 5));
1058
- const MAX_DISPLAY = 30;
1059
- const displayed = records.slice(0, MAX_DISPLAY);
1060
- const lines = displayed.map((r) => {
1061
- const emoji = statusEmoji[r.status] || "\u26AA";
1062
- const name = r.name?.trim();
1063
- const label = name ? escapeHtml(name) : `<i>${escapeHtml(r.agentName)} session</i>`;
1064
- return `${emoji} ${label} <code>[${r.status}]</code>`;
1065
- });
1066
- const header = `<b>Sessions: ${records.length}</b>` + (headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "");
1067
- const truncated = records.length > MAX_DISPLAY ? `
1068
-
1069
- <i>...and ${records.length - MAX_DISPLAY} more</i>` : "";
1070
- const finishedCount = allRecords.filter((r) => r.status === "finished").length;
1071
- const errorCount = allRecords.filter((r) => r.status === "error" || r.status === "cancelled").length;
1072
- const keyboard = new InlineKeyboard3();
1073
- if (finishedCount > 0) {
1074
- keyboard.text(`Cleanup finished (${finishedCount})`, "m:cleanup:finished").row();
1075
- }
1076
- if (errorCount > 0) {
1077
- keyboard.text(`Cleanup errors (${errorCount})`, "m:cleanup:errors").row();
1078
- }
1079
- if (finishedCount + errorCount > 0) {
1080
- keyboard.text(`Cleanup all non-active (${finishedCount + errorCount})`, "m:cleanup:all").row();
1081
- }
1082
- keyboard.text(`\u26A0\uFE0F Cleanup ALL (${allRecords.length})`, "m:cleanup:everything").row();
1083
- keyboard.text("Refresh", "m:topics");
1084
- await ctx.reply(
1085
- `${header}
1086
-
1087
- ${lines.join("\n")}${truncated}`,
1088
- { parse_mode: "HTML", reply_markup: keyboard }
1089
- );
1090
- } catch (err) {
1091
- log3.error({ err }, "handleTopics error");
1092
- await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
1093
- });
1094
- }
1095
- }
1096
- async function handleCleanup(ctx, core, chatId, statuses) {
1097
- const allRecords = core.sessionManager.listRecords();
1098
- const cleanable = allRecords.filter((r) => statuses.includes(r.status));
1099
- if (cleanable.length === 0) {
1100
- await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
1101
- return;
1102
- }
1103
- let deleted = 0;
1104
- let failed = 0;
1105
- for (const record of cleanable) {
1106
- try {
1107
- const topicId = record.platform?.topicId;
1108
- if (topicId) {
1109
- try {
1110
- await ctx.api.deleteForumTopic(chatId, topicId);
1111
- } catch (err) {
1112
- log3.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
1113
- }
1114
- }
1115
- await core.sessionManager.removeRecord(record.sessionId);
1116
- deleted++;
1117
- } catch (err) {
1118
- log3.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
1119
- failed++;
1120
- }
1121
- }
1122
- await ctx.reply(
1123
- `\u{1F5D1} Cleaned up <b>${deleted}</b> sessions${failed > 0 ? ` (${failed} failed)` : ""}.`,
1124
- { parse_mode: "HTML" }
1125
- );
1126
- }
1127
- async function handleCleanupEverything(ctx, core, chatId, systemTopicIds) {
1128
- const allRecords = core.sessionManager.listRecords();
1129
- const cleanable = allRecords.filter((r) => {
1130
- const platform = r.platform;
1131
- if (systemTopicIds && platform?.topicId && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
1132
- return true;
1133
- });
1134
- if (cleanable.length === 0) {
1135
- await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
1136
- return;
1137
- }
1138
- const statusCounts = /* @__PURE__ */ new Map();
1139
- for (const r of cleanable) {
1140
- statusCounts.set(r.status, (statusCounts.get(r.status) ?? 0) + 1);
1141
- }
1142
- const statusEmoji = {
1143
- active: "\u{1F7E2}",
1144
- initializing: "\u{1F7E1}",
1145
- finished: "\u2705",
1146
- error: "\u274C",
1147
- cancelled: "\u26D4"
1148
- };
1149
- const breakdown = Array.from(statusCounts.entries()).map(([status, count]) => `${statusEmoji[status] ?? "\u26AA"} ${status}: ${count}`).join("\n");
1150
- const activeCount = (statusCounts.get("active") ?? 0) + (statusCounts.get("initializing") ?? 0);
1151
- const activeWarning = activeCount > 0 ? `
1152
-
1153
- \u26A0\uFE0F <b>${activeCount} active session(s) will be cancelled and their agents stopped!</b>` : "";
1154
- const keyboard = new InlineKeyboard3().text("Yes, delete all", "m:cleanup:everything:confirm").text("Cancel", "m:topics");
1155
- await ctx.reply(
1156
- `<b>Delete ${cleanable.length} topics?</b>
1157
-
1158
- This will:
1159
- \u2022 Delete all session topics from this group
1160
- \u2022 Cancel any running agent sessions
1161
- \u2022 Remove all session records
1162
-
1163
- <b>Breakdown:</b>
1164
- ${breakdown}${activeWarning}
1165
-
1166
- <i>Notifications and Assistant topics will NOT be deleted.</i>`,
1167
- { parse_mode: "HTML", reply_markup: keyboard }
1168
- );
1169
- }
1170
- async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicIds) {
1171
- const allRecords = core.sessionManager.listRecords();
1172
- const cleanable = allRecords.filter((r) => {
1173
- const platform = r.platform;
1174
- if (systemTopicIds && platform?.topicId && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
1175
- return true;
1176
- });
1177
- if (cleanable.length === 0) {
1178
- await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
1179
- return;
1180
- }
1181
- let deleted = 0;
1182
- let failed = 0;
1183
- for (const record of cleanable) {
1184
- try {
1185
- if (record.status === "active" || record.status === "initializing") {
1186
- try {
1187
- await core.sessionManager.cancelSession(record.sessionId);
1188
- } catch (err) {
1189
- log3.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
1190
- }
1191
- }
1192
- const topicId = record.platform?.topicId;
1193
- if (topicId) {
1194
- try {
1195
- await ctx.api.deleteForumTopic(chatId, topicId);
1196
- } catch (err) {
1197
- log3.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
1198
- }
1199
- }
1200
- await core.sessionManager.removeRecord(record.sessionId);
1201
- deleted++;
1202
- } catch (err) {
1203
- log3.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
1204
- failed++;
1205
- }
1206
- }
1207
- await ctx.reply(
1208
- `\u{1F5D1} Cleaned up <b>${deleted}</b> sessions${failed > 0 ? ` (${failed} failed)` : ""}.`,
1209
- { parse_mode: "HTML" }
1210
- );
1211
- }
1212
- async function executeCancelSession(core, excludeSessionId) {
1213
- const sessions = core.sessionManager.listSessions("telegram").filter((s) => s.status === "active" && s.id !== excludeSessionId).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1214
- const session = sessions[0];
1215
- if (!session) return null;
1216
- await session.abortPrompt();
1217
- return session;
1218
- }
1219
- function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
1220
- bot.callbackQuery(/^m:cleanup/, async (ctx) => {
1221
- const data = ctx.callbackQuery.data;
1222
- try {
1223
- await ctx.answerCallbackQuery();
1224
- } catch {
1225
- }
1226
- switch (data) {
1227
- case "m:cleanup:finished":
1228
- await handleCleanup(ctx, core, chatId, ["finished"]);
1229
- break;
1230
- case "m:cleanup:errors":
1231
- await handleCleanup(ctx, core, chatId, ["error", "cancelled"]);
1232
- break;
1233
- case "m:cleanup:all":
1234
- await handleCleanup(ctx, core, chatId, ["finished", "error", "cancelled"]);
1235
- break;
1236
- case "m:cleanup:everything":
1237
- await handleCleanupEverything(ctx, core, chatId, systemTopicIds);
1238
- break;
1239
- case "m:cleanup:everything:confirm":
1240
- await handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicIds);
1241
- break;
1242
- }
1243
- });
1244
- }
1245
- async function handleUsage(ctx, core) {
1246
- if (!core.usageStore) {
1247
- await ctx.reply("\u{1F4CA} Usage tracking is disabled.", { parse_mode: "HTML" });
1248
- return;
1249
- }
1250
- const rawMatch = ctx.match;
1251
- const period = typeof rawMatch === "string" ? rawMatch.trim().toLowerCase() : "";
1252
- let summaries;
1253
- if (period === "today" || period === "week" || period === "month") {
1254
- summaries = [core.usageStore.query(period)];
1255
- } else {
1256
- summaries = [
1257
- core.usageStore.query("month"),
1258
- core.usageStore.query("week"),
1259
- core.usageStore.query("today")
1260
- ];
1261
- }
1262
- const budgetStatus = core.usageBudget ? core.usageBudget.getStatus() : { status: "ok", used: 0, budget: 0, percent: 0 };
1263
- const text = formatUsageReport(summaries, budgetStatus);
1264
- await ctx.reply(text, { parse_mode: "HTML" });
1265
- }
1266
- async function handleArchive(ctx, core) {
1267
- const threadId = ctx.message?.message_thread_id;
1268
- if (!threadId) return;
1269
- const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
1270
- const record = !session ? core.sessionManager.getRecordByThread("telegram", String(threadId)) : void 0;
1271
- const identifier = session?.id ?? record?.sessionId ?? `topic:${threadId}`;
1272
- await ctx.reply(
1273
- "\u26A0\uFE0F <b>Archive this session?</b>\n\nThis will:\n\u2022 Delete this topic and all messages\n\u2022 Stop the agent session (if running)\n\u2022 Remove the session record\n\n<i>This action cannot be undone.</i>",
1274
- {
1275
- parse_mode: "HTML",
1276
- reply_markup: new InlineKeyboard3().text("\u{1F5D1} Yes, archive", `ar:yes:${identifier}`).text("\u274C Cancel", `ar:no:${identifier}`)
1277
- }
1278
- );
1279
- }
1280
- async function handleArchiveConfirm(ctx, core, chatId) {
1281
- const data = ctx.callbackQuery?.data;
1282
- if (!data) return;
1283
- try {
1284
- await ctx.answerCallbackQuery();
1285
- } catch {
1286
- }
1287
- const [, action, ...rest] = data.split(":");
1288
- const identifier = rest.join(":");
1289
- if (action === "no") {
1290
- await ctx.editMessageText("Archive cancelled.", { parse_mode: "HTML" });
1291
- return;
1292
- }
1293
- await ctx.editMessageText("\u{1F504} Archiving...", { parse_mode: "HTML" });
1294
- if (identifier.startsWith("topic:")) {
1295
- const topicId = Number(identifier.slice("topic:".length));
1296
- try {
1297
- await ctx.api.deleteForumTopic(chatId, topicId);
1298
- core.notificationManager.notifyAll({
1299
- sessionId: "system",
1300
- sessionName: `Orphan topic #${topicId}`,
1301
- type: "completed",
1302
- summary: `Orphan topic #${topicId} archived and deleted.`
1303
- });
1304
- } catch (err) {
1305
- core.notificationManager.notifyAll({
1306
- sessionId: "system",
1307
- sessionName: `Orphan topic #${topicId}`,
1308
- type: "error",
1309
- summary: `Failed to delete orphan topic #${topicId}: ${err.message}`
1310
- });
1311
- }
1312
- return;
1313
- }
1314
- const result = await core.archiveSession(identifier);
1315
- if (result.ok) {
1316
- core.notificationManager.notifyAll({
1317
- sessionId: identifier,
1318
- type: "completed",
1319
- summary: `Session archived and deleted.`
1320
- });
1321
- } else {
1322
- try {
1323
- await ctx.editMessageText(`\u274C Failed to archive: <code>${escapeHtml(result.error)}</code>`, { parse_mode: "HTML" });
1324
- } catch {
1325
- core.notificationManager.notifyAll({
1326
- sessionId: identifier,
1327
- type: "error",
1328
- summary: `Failed to archive session "${identifier}": ${result.error}`
1329
- });
1330
- }
1331
- }
1332
- }
1333
- async function handleSummary(ctx, core) {
1334
- const threadId = ctx.message?.message_thread_id;
1335
- if (!threadId) return;
1336
- const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
1337
- const record = !session ? core.sessionManager.getRecordByThread("telegram", String(threadId)) : void 0;
1338
- const sessionId = session?.id ?? record?.sessionId;
1339
- if (!sessionId) {
1340
- await ctx.reply(
1341
- "\u2139\uFE0F <b>/summary</b> works in session topics \u2014 it asks the agent to summarize the session.\n\nGo to a session topic and type /summary there.",
1342
- { parse_mode: "HTML" }
1343
- );
1344
- return;
1345
- }
1346
- await ctx.replyWithChatAction("typing");
1347
- const result = await core.summarizeSession(sessionId);
1348
- if (result.ok) {
1349
- await ctx.reply(formatSummary(result.summary, session?.name ?? record?.name), { parse_mode: "HTML" });
1350
- } else {
1351
- await ctx.reply(`\u26A0\uFE0F ${escapeHtml(result.error)}`, { parse_mode: "HTML" });
1352
- }
1353
- }
1354
- async function handleSummaryCallback(ctx, core, chatId) {
1355
- const data = ctx.callbackQuery?.data;
1356
- if (!data) return;
1357
- const sessionId = data.replace("sm:summary:", "");
1358
- try {
1359
- await ctx.answerCallbackQuery();
1360
- } catch {
1361
- }
1362
- const session = core.sessionManager.getSession(sessionId);
1363
- const record = !session ? core.sessionManager.getSessionRecord(sessionId) : void 0;
1364
- const threadId = session ? Number(session.threadId) : record?.platform?.topicId ?? 0;
1365
- if (!threadId) return;
1366
- await ctx.api.sendMessage(chatId, "\u{1F4CB} Generating summary...", {
1367
- message_thread_id: threadId,
1368
- parse_mode: "HTML"
1369
- });
1370
- const result = await core.summarizeSession(sessionId);
1371
- const sessionName = session?.name ?? record?.name;
1372
- if (result.ok) {
1373
- await ctx.api.sendMessage(chatId, formatSummary(result.summary, sessionName), {
1374
- message_thread_id: threadId,
1375
- parse_mode: "HTML"
1376
- });
1377
- } else {
1378
- await ctx.api.sendMessage(chatId, `\u26A0\uFE0F ${escapeHtml(result.error)}`, {
1379
- message_thread_id: threadId,
1380
- parse_mode: "HTML"
1381
- });
1382
- }
1383
- }
1384
-
1385
- // src/adapters/telegram/commands/agents.ts
1386
- import { InlineKeyboard as InlineKeyboard4 } from "grammy";
1387
- var AGENTS_PER_PAGE = 6;
1388
- async function handleAgents(ctx, core, page = 0) {
1389
- const catalog = core.agentCatalog;
1390
- const items = catalog.getAvailable();
1391
- const installed = items.filter((i) => i.installed);
1392
- const available = items.filter((i) => !i.installed);
1393
- let text = "<b>\u{1F916} Agents</b>\n\n";
1394
- if (installed.length > 0) {
1395
- text += "<b>Installed:</b>\n";
1396
- for (const item of installed) {
1397
- text += `\u2705 <b>${escapeHtml(item.name)}</b>`;
1398
- if (item.description) {
1399
- text += ` \u2014 <i>${escapeHtml(truncate(item.description, 50))}</i>`;
1400
- }
1401
- text += "\n";
1402
- }
1403
- text += "\n";
1404
- }
1405
- if (available.length > 0) {
1406
- const totalPages = Math.ceil(available.length / AGENTS_PER_PAGE);
1407
- const safePage = Math.max(0, Math.min(page, totalPages - 1));
1408
- const pageItems = available.slice(safePage * AGENTS_PER_PAGE, (safePage + 1) * AGENTS_PER_PAGE);
1409
- text += `<b>Available to install:</b>`;
1410
- if (totalPages > 1) {
1411
- text += ` (${safePage + 1}/${totalPages})`;
1412
- }
1413
- text += "\n";
1414
- for (const item of pageItems) {
1415
- if (item.available) {
1416
- text += `\u2B07\uFE0F <b>${escapeHtml(item.name)}</b>`;
1417
- } else {
1418
- const deps = item.missingDeps?.join(", ") ?? "requirements not met";
1419
- text += `\u26A0\uFE0F <b>${escapeHtml(item.name)}</b> <i>(needs: ${escapeHtml(deps)})</i>`;
1420
- }
1421
- if (item.description) {
1422
- text += `
1423
- <i>${escapeHtml(truncate(item.description, 60))}</i>`;
1424
- }
1425
- text += "\n";
1426
- }
1427
- const keyboard = new InlineKeyboard4();
1428
- const installable = pageItems.filter((i) => i.available);
1429
- for (let i = 0; i < installable.length; i += 2) {
1430
- const row = installable.slice(i, i + 2);
1431
- for (const item of row) {
1432
- keyboard.text(`\u2B07\uFE0F ${item.name}`, `ag:install:${item.key}`);
1433
- }
1434
- keyboard.row();
1435
- }
1436
- if (totalPages > 1) {
1437
- if (safePage > 0) {
1438
- keyboard.text("\u25C0\uFE0F Prev", `ag:page:${safePage - 1}`);
1439
- }
1440
- if (safePage < totalPages - 1) {
1441
- keyboard.text("Next \u25B6\uFE0F", `ag:page:${safePage + 1}`);
1442
- }
1443
- keyboard.row();
1444
- }
1445
- if (available.some((i) => !i.available)) {
1446
- text += "\n\u{1F4A1} <i>Agents marked \u26A0\uFE0F need additional setup. Use</i> <code>openacp agents info &lt;name&gt;</code> <i>for details.</i>\n";
1447
- }
1448
- await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
1449
- } else {
1450
- text += "<i>All agents are already installed!</i>";
1451
- await ctx.reply(text, { parse_mode: "HTML" });
1452
- }
1453
- }
1454
- async function handleInstall(ctx, core) {
1455
- const text = (ctx.message?.text ?? "").trim();
1456
- const parts = text.split(/\s+/);
1457
- const nameOrId = parts[1];
1458
- if (!nameOrId) {
1459
- await ctx.reply(
1460
- "\u{1F4E6} <b>Install an agent</b>\n\nUsage: <code>/install &lt;agent-name&gt;</code>\nExample: <code>/install gemini</code>\n\nUse /agents to browse available agents.",
1461
- { parse_mode: "HTML" }
1462
- );
1463
- return;
1464
- }
1465
- await installAgentWithProgress(ctx, core, nameOrId);
1466
- }
1467
- async function handleAgentCallback(ctx, core) {
1468
- const data = ctx.callbackQuery?.data ?? "";
1469
- await ctx.answerCallbackQuery();
1470
- if (data.startsWith("ag:install:")) {
1471
- const nameOrId = data.replace("ag:install:", "");
1472
- await installAgentWithProgress(ctx, core, nameOrId);
1473
- return;
1474
- }
1475
- if (data.startsWith("ag:page:")) {
1476
- const page = parseInt(data.replace("ag:page:", ""), 10);
1477
- try {
1478
- const catalog = core.agentCatalog;
1479
- const items = catalog.getAvailable();
1480
- const installed = items.filter((i) => i.installed);
1481
- const available = items.filter((i) => !i.installed);
1482
- let text = "<b>\u{1F916} Agents</b>\n\n";
1483
- if (installed.length > 0) {
1484
- text += "<b>Installed:</b>\n";
1485
- for (const item of installed) {
1486
- text += `\u2705 <b>${escapeHtml(item.name)}</b>`;
1487
- if (item.description) {
1488
- text += ` \u2014 <i>${escapeHtml(truncate(item.description, 50))}</i>`;
1489
- }
1490
- text += "\n";
1491
- }
1492
- text += "\n";
1493
- }
1494
- const totalPages = Math.ceil(available.length / AGENTS_PER_PAGE);
1495
- const safePage = Math.max(0, Math.min(page, totalPages - 1));
1496
- const pageItems = available.slice(safePage * AGENTS_PER_PAGE, (safePage + 1) * AGENTS_PER_PAGE);
1497
- text += `<b>Available to install:</b>`;
1498
- if (totalPages > 1) {
1499
- text += ` (${safePage + 1}/${totalPages})`;
1500
- }
1501
- text += "\n";
1502
- for (const item of pageItems) {
1503
- if (item.available) {
1504
- text += `\u2B07\uFE0F <b>${escapeHtml(item.name)}</b>`;
1505
- } else {
1506
- const deps = item.missingDeps?.join(", ") ?? "requirements not met";
1507
- text += `\u26A0\uFE0F <b>${escapeHtml(item.name)}</b> <i>(needs: ${escapeHtml(deps)})</i>`;
1508
- }
1509
- if (item.description) {
1510
- text += `
1511
- <i>${escapeHtml(truncate(item.description, 60))}</i>`;
1512
- }
1513
- text += "\n";
1514
- }
1515
- const keyboard = new InlineKeyboard4();
1516
- const installable = pageItems.filter((i) => i.available);
1517
- for (let i = 0; i < installable.length; i += 2) {
1518
- const row = installable.slice(i, i + 2);
1519
- for (const item of row) {
1520
- keyboard.text(`\u2B07\uFE0F ${item.name}`, `ag:install:${item.key}`);
1521
- }
1522
- keyboard.row();
1523
- }
1524
- if (totalPages > 1) {
1525
- if (safePage > 0) {
1526
- keyboard.text("\u25C0\uFE0F Prev", `ag:page:${safePage - 1}`);
1527
- }
1528
- if (safePage < totalPages - 1) {
1529
- keyboard.text("Next \u25B6\uFE0F", `ag:page:${safePage + 1}`);
1530
- }
1531
- keyboard.row();
1532
- }
1533
- await ctx.editMessageText(text, { parse_mode: "HTML", reply_markup: keyboard });
1534
- } catch {
1535
- }
1536
- }
1537
- }
1538
- async function installAgentWithProgress(ctx, core, nameOrId) {
1539
- const catalog = core.agentCatalog;
1540
- const msg = await ctx.reply(`\u23F3 Installing <b>${escapeHtml(nameOrId)}</b>...`, { parse_mode: "HTML" });
1541
- let lastEdit = 0;
1542
- const EDIT_THROTTLE_MS = 1500;
1543
- const progress = {
1544
- onStart(_id, _name) {
1545
- },
1546
- async onStep(step) {
1547
- const now = Date.now();
1548
- if (now - lastEdit > EDIT_THROTTLE_MS) {
1549
- lastEdit = now;
1550
- try {
1551
- await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u23F3 <b>${escapeHtml(nameOrId)}</b>: ${escapeHtml(step)}`, { parse_mode: "HTML" });
1552
- } catch {
1553
- }
1554
- }
1555
- },
1556
- async onDownloadProgress(percent) {
1557
- const now = Date.now();
1558
- if (now - lastEdit > EDIT_THROTTLE_MS) {
1559
- lastEdit = now;
1560
- try {
1561
- const bar = buildProgressBar(percent);
1562
- await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u23F3 <b>${escapeHtml(nameOrId)}</b>
1563
- Downloading... ${bar} ${percent}%`, { parse_mode: "HTML" });
1564
- } catch {
1565
- }
1566
- }
1567
- },
1568
- async onSuccess(name) {
1569
- try {
1570
- const keyboard = new InlineKeyboard4().text(`\u{1F680} Start session with ${name}`, `na:${nameOrId}`);
1571
- await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u2705 <b>${escapeHtml(name)}</b> installed!`, { parse_mode: "HTML", reply_markup: keyboard });
1572
- } catch {
1573
- }
1574
- },
1575
- async onError(error) {
1576
- try {
1577
- await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u274C ${escapeHtml(error)}`, { parse_mode: "HTML" });
1578
- } catch {
1579
- }
1580
- }
1581
- };
1582
- const result = await catalog.install(nameOrId, progress);
1583
- if (result.ok) {
1584
- const { getAgentCapabilities } = await import("./agent-dependencies-4OWBMZWZ.js");
1585
- const caps = getAgentCapabilities(result.agentKey);
1586
- if (caps.integration) {
1587
- const { installIntegration } = await import("./integrate-PNEHRY2I.js");
1588
- const intResult = await installIntegration(result.agentKey, caps.integration);
1589
- if (intResult.success) {
1590
- try {
1591
- await ctx.reply(`\u{1F517} Handoff integration installed for <b>${escapeHtml(result.agentKey)}</b>`, { parse_mode: "HTML" });
1592
- } catch {
1593
- }
1594
- }
1595
- }
1596
- }
1597
- if (result.ok && result.setupSteps?.length) {
1598
- let setupText = `\u{1F4CB} <b>Setup for ${escapeHtml(result.agentKey)}:</b>
1599
-
1600
- `;
1601
- for (const step of result.setupSteps) {
1602
- setupText += `\u2192 ${formatSetupStep(step)}
1603
- `;
1604
- }
1605
- setupText += `
1606
- \u{1F4A1} <i>Tap any command to copy it</i>`;
1607
- try {
1608
- await ctx.reply(setupText, { parse_mode: "HTML" });
1609
- } catch {
1610
- }
1611
- }
1612
- }
1613
- function formatSetupStep(step) {
1614
- const colonIdx = step.indexOf(": ");
1615
- if (colonIdx === -1) return escapeHtml(step);
1616
- const label = step.slice(0, colonIdx);
1617
- const rest = step.slice(colonIdx + 2);
1618
- const cmdPrefixes = [
1619
- "npm ",
1620
- "npx ",
1621
- "pip ",
1622
- "uvx ",
1623
- "export ",
1624
- "claude ",
1625
- "codex ",
1626
- "openacp ",
1627
- "goose ",
1628
- "gemini ",
1629
- "cursor ",
1630
- "gh "
1631
- ];
1632
- const looksLikeCommand = cmdPrefixes.some((p) => rest.startsWith(p));
1633
- if (looksLikeCommand) {
1634
- const parenIdx = rest.indexOf(" (");
1635
- if (parenIdx !== -1) {
1636
- const cmd = rest.slice(0, parenIdx);
1637
- const explanation = rest.slice(parenIdx);
1638
- return `${escapeHtml(label)}: <code>${escapeHtml(cmd)}</code> ${escapeHtml(explanation)}`;
1639
- }
1640
- return `${escapeHtml(label)}: <code>${escapeHtml(rest)}</code>`;
1641
- }
1642
- return escapeHtml(step);
1643
- }
1644
- function truncate(text, maxLen) {
1645
- if (text.length <= maxLen) return text;
1646
- return text.slice(0, maxLen - 1) + "\u2026";
1647
- }
1648
- function buildProgressBar(percent) {
1649
- const filled = Math.round(percent / 10);
1650
- const empty = 10 - filled;
1651
- return "\u2588".repeat(filled) + "\u2591".repeat(empty);
1652
- }
1653
-
1654
- // src/adapters/telegram/commands/integrate.ts
1655
- import { InlineKeyboard as InlineKeyboard5 } from "grammy";
1656
- async function handleIntegrate(ctx, _core) {
1657
- const { listIntegrations } = await import("./integrate-PNEHRY2I.js");
1658
- const agents = listIntegrations();
1659
- const keyboard = new InlineKeyboard5();
1660
- for (const agent of agents) {
1661
- keyboard.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
1662
- }
1663
- await ctx.reply(
1664
- `<b>\u{1F517} Integrations</b>
1665
-
1666
- Select an agent to manage its integrations.`,
1667
- { parse_mode: "HTML", reply_markup: keyboard }
1668
- );
1669
- }
1670
- function buildAgentItemsKeyboard(agentName, items) {
1671
- const keyboard = new InlineKeyboard5();
1672
- for (const item of items) {
1673
- const installed = item.isInstalled();
1674
- keyboard.text(
1675
- installed ? `\u2705 ${item.name} \u2014 Uninstall` : `\u{1F4E6} ${item.name} \u2014 Install`,
1676
- installed ? `i:uninstall:${agentName}:${item.id}` : `i:install:${agentName}:${item.id}`
1677
- ).row();
1678
- }
1679
- keyboard.text("\u2190 Back", "i:back").row();
1680
- return keyboard;
1681
- }
1682
- function setupIntegrateCallbacks(bot, core) {
1683
- bot.callbackQuery(/^i:/, async (ctx) => {
1684
- const data = ctx.callbackQuery.data;
1685
- try {
1686
- await ctx.answerCallbackQuery();
1687
- } catch {
1688
- }
1689
- if (data === "i:back") {
1690
- const { listIntegrations } = await import("./integrate-PNEHRY2I.js");
1691
- const agents = listIntegrations();
1692
- const keyboard2 = new InlineKeyboard5();
1693
- for (const agent of agents) {
1694
- keyboard2.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
1695
- }
1696
- try {
1697
- await ctx.editMessageText(
1698
- `<b>\u{1F517} Integrations</b>
1699
-
1700
- Select an agent to manage its integrations.`,
1701
- { parse_mode: "HTML", reply_markup: keyboard2 }
1702
- );
1703
- } catch {
1704
- }
1705
- return;
1706
- }
1707
- const agentMatch = data.match(/^i:agent:(.+)$/);
1708
- if (agentMatch) {
1709
- const agentName2 = agentMatch[1];
1710
- const { getIntegration: getIntegration2 } = await import("./integrate-PNEHRY2I.js");
1711
- const integration2 = getIntegration2(agentName2);
1712
- if (!integration2) {
1713
- await ctx.reply(`\u274C No integration available for '${escapeHtml(agentName2)}'.`, { parse_mode: "HTML" });
1714
- return;
1715
- }
1716
- const keyboard2 = buildAgentItemsKeyboard(agentName2, integration2.items);
1717
- try {
1718
- await ctx.editMessageText(
1719
- `<b>\u{1F517} ${escapeHtml(agentName2)} Integrations</b>
1720
-
1721
- ${integration2.items.map((i) => `\u2022 <b>${escapeHtml(i.name)}</b> \u2014 ${escapeHtml(i.description)}`).join("\n")}`,
1722
- { parse_mode: "HTML", reply_markup: keyboard2 }
1723
- );
1724
- } catch {
1725
- await ctx.reply(
1726
- `<b>\u{1F517} ${escapeHtml(agentName2)} Integrations</b>`,
1727
- { parse_mode: "HTML", reply_markup: keyboard2 }
1728
- );
1729
- }
1730
- return;
1731
- }
1732
- const actionMatch = data.match(/^i:(install|uninstall):([^:]+):(.+)$/);
1733
- if (!actionMatch) return;
1734
- const action = actionMatch[1];
1735
- const agentName = actionMatch[2];
1736
- const itemId = actionMatch[3];
1737
- const { getIntegration } = await import("./integrate-PNEHRY2I.js");
1738
- const integration = getIntegration(agentName);
1739
- if (!integration) return;
1740
- const item = integration.items.find((i) => i.id === itemId);
1741
- if (!item) return;
1742
- const result = action === "install" ? await item.install() : await item.uninstall();
1743
- const installed = action === "install" && result.success;
1744
- await core.configManager.save({
1745
- integrations: {
1746
- [agentName]: {
1747
- installed,
1748
- installedAt: installed ? (/* @__PURE__ */ new Date()).toISOString() : void 0
1749
- }
1750
- }
1751
- });
1752
- const statusEmoji = result.success ? "\u2705" : "\u274C";
1753
- const actionLabel = action === "install" ? "installed" : "uninstalled";
1754
- const logsText = result.logs.map((l) => `<code>${escapeHtml(l)}</code>`).join("\n");
1755
- const resultText = `${statusEmoji} <b>${escapeHtml(item.name)}</b> ${actionLabel}.
1756
-
1757
- ${logsText}`;
1758
- const keyboard = buildAgentItemsKeyboard(agentName, integration.items);
1759
- try {
1760
- await ctx.editMessageText(
1761
- `<b>\u{1F517} ${escapeHtml(agentName)} Integrations</b>
1762
-
1763
- ${resultText}`,
1764
- { parse_mode: "HTML", reply_markup: keyboard }
1765
- );
1766
- } catch {
1767
- await ctx.reply(resultText, { parse_mode: "HTML" });
1768
- }
1769
- });
1770
- }
1771
-
1772
- // src/adapters/telegram/commands/resume.ts
1773
- import * as fs from "fs";
1774
- import * as path from "path";
1775
- import * as os from "os";
1776
- import { InlineKeyboard as InlineKeyboard6 } from "grammy";
1777
- var log4 = createChildLogger({ module: "telegram-cmd-resume" });
1778
- var PENDING_TIMEOUT_MS2 = 5 * 60 * 1e3;
1779
- function botFromCtx2(ctx) {
1780
- return { api: ctx.api };
1781
- }
1782
- var pendingResumes = /* @__PURE__ */ new Map();
1783
- function cleanupPending2(userId) {
1784
- const pending = pendingResumes.get(userId);
1785
- if (pending) {
1786
- clearTimeout(pending.timer);
1787
- pendingResumes.delete(userId);
1788
- }
1789
- }
1790
- function parseResumeArgs(matchStr) {
1791
- const args = matchStr.split(" ").filter(Boolean);
1792
- if (args.length === 0) return { query: { type: "latest", value: "5" } };
1793
- const first = args[0];
1794
- if (first === "pr") return args[1] ? { query: { type: "pr", value: args[1] } } : null;
1795
- if (first === "branch") return args[1] ? { query: { type: "branch", value: args[1] } } : null;
1796
- if (first === "commit") return args[1] ? { query: { type: "commit", value: args[1] } } : null;
1797
- if (CheckpointReader.isCheckpointId(first)) return { query: { type: "checkpoint", value: first } };
1798
- if (CheckpointReader.isSessionId(first)) return { query: { type: "session", value: first } };
1799
- if (first.includes("/pull/")) {
1800
- const prMatch = first.match(/\/pull\/(\d+)/);
1801
- return prMatch ? { query: { type: "pr", value: prMatch[1] } } : null;
1802
- }
1803
- const ghCommitMatch = first.match(/github\.com\/[^/]+\/[^/]+\/commit\/([0-9a-f]+)/);
1804
- if (ghCommitMatch) return { query: { type: "commit", value: ghCommitMatch[1] } };
1805
- const ghBranchMatch = first.match(/github\.com\/[^/]+\/[^/]+\/tree\/(.+?)(?:\?|#|$)/);
1806
- if (ghBranchMatch) return { query: { type: "branch", value: ghBranchMatch[1] } };
1807
- const ghCompareMatch = first.match(/github\.com\/[^/]+\/[^/]+\/compare\/(?:[^.]+\.{2,3})(.+?)(?:\?|#|$)/);
1808
- if (ghCompareMatch) return { query: { type: "branch", value: ghCompareMatch[1] } };
1809
- if (first.match(/github\.com\/[^/]+\/[^/]+\/?$/) && !first.includes("/tree/") && !first.includes("/pull/") && !first.includes("/commit/") && !first.includes("/compare/")) {
1810
- return { query: { type: "latest", value: "5" } };
1811
- }
1812
- const entireCheckpointMatch = first.match(/entire\.io\/gh\/[^/]+\/[^/]+\/checkpoints\/[^/]+\/([0-9a-f]{12})/);
1813
- if (entireCheckpointMatch) return { query: { type: "checkpoint", value: entireCheckpointMatch[1] } };
1814
- const entireCommitMatch = first.match(/entire\.io\/gh\/[^/]+\/[^/]+\/commit\/([0-9a-f]+)/);
1815
- if (entireCommitMatch) return { query: { type: "commit", value: entireCommitMatch[1] } };
1816
- return { query: { type: "latest", value: "5" } };
1817
- }
1818
- function looksLikePath(text) {
1819
- return text.startsWith("/") || text.startsWith("~") || text.startsWith(".");
1820
- }
1821
- function listWorkspaceDirs(baseDir, maxItems = 10) {
1822
- const resolved = baseDir.replace(/^~/, os.homedir());
1823
- try {
1824
- if (!fs.existsSync(resolved)) return [];
1825
- return fs.readdirSync(resolved, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort().slice(0, maxItems);
1826
- } catch {
1827
- return [];
1828
- }
1829
- }
1830
- async function showWorkspacePicker(ctx, core, chatId, userId, query) {
1831
- const config = core.configManager.get();
1832
- const baseDir = config.workspace.baseDir;
1833
- const resolvedBase = baseDir.replace(/^~/, os.homedir());
1834
- const subdirs = listWorkspaceDirs(baseDir);
1835
- const keyboard = new InlineKeyboard6();
1836
- for (const dir of subdirs) {
1837
- const fullPath = path.join(resolvedBase, dir);
1838
- keyboard.text(`\u{1F4C1} ${dir}`, `m:resume:ws:${dir}`).row();
1839
- }
1840
- keyboard.text(`\u{1F4C1} Use ${baseDir}`, "m:resume:ws:default").row();
1841
- keyboard.text("\u270F\uFE0F Enter project path", "m:resume:ws:custom");
1842
- const queryLabel = query.type === "latest" ? "latest sessions" : `${query.type}: ${query.value}`;
1843
- const text = `\u{1F4C1} <b>Select project directory for resume</b>
1844
-
1845
- Query: <code>${escapeHtml(queryLabel)}</code>
1846
-
1847
- Choose the repo that has Entire checkpoints enabled:`;
1848
- const msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
1849
- cleanupPending2(userId);
1850
- pendingResumes.set(userId, {
1851
- query,
1852
- step: "workspace",
1853
- messageId: msg.message_id,
1854
- threadId: ctx.message?.message_thread_id,
1855
- timer: setTimeout(() => pendingResumes.delete(userId), PENDING_TIMEOUT_MS2)
1856
- });
1857
- }
1858
- async function executeResume(ctx, core, chatId, query, repoPath) {
1859
- const provider = await core.contextManager.getProvider(repoPath);
1860
- if (!provider) {
1861
- await ctx.reply(
1862
- `\u26A0\uFE0F <b>Entire not enabled in <code>${escapeHtml(repoPath)}</code></b>
1863
-
1864
- To enable conversation history tracking:
1865
- <code>cd ${escapeHtml(repoPath)} && npx entire enable</code>
1866
-
1867
- Learn more: https://docs.entire.io/getting-started`,
1868
- { parse_mode: "HTML" }
1869
- );
1870
- return;
1871
- }
1872
- const fullQuery = { ...query, repoPath };
1873
- await ctx.reply(`\u{1F50D} Scanning ${query.type === "latest" ? "latest sessions" : `${query.type}: ${escapeHtml(query.value)}`}...`, { parse_mode: "HTML" });
1874
- const listResult = await core.contextManager.listSessions(fullQuery);
1875
- if (!listResult || listResult.sessions.length === 0) {
1876
- await ctx.reply(
1877
- `\u{1F50D} <b>No sessions found</b>
1878
-
1879
- Query: <code>${escapeHtml(query.type)}: ${escapeHtml(query.value)}</code>
1880
- Repo: <code>${escapeHtml(repoPath)}</code>`,
1881
- { parse_mode: "HTML" }
1882
- );
1883
- return;
1884
- }
1885
- const config = core.configManager.get();
1886
- const agentName = config.defaultAgent;
1887
- let threadId;
1888
- try {
1889
- const queryLabel = query.type === "latest" ? "latest" : `${query.type}: ${query.value.slice(0, 20)}`;
1890
- const topicName = `\u{1F4DC} Resume \u2014 ${queryLabel}`;
1891
- threadId = await createSessionTopic(botFromCtx2(ctx), chatId, topicName);
1892
- await ctx.api.sendMessage(chatId, `\u23F3 Loading context and starting session...`, {
1893
- message_thread_id: threadId,
1894
- parse_mode: "HTML"
1895
- });
1896
- const { session, contextResult } = await core.createSessionWithContext({
1897
- channelId: "telegram",
1898
- agentName,
1899
- workingDirectory: repoPath,
1900
- contextQuery: fullQuery,
1901
- contextOptions: { maxTokens: DEFAULT_MAX_TOKENS }
1902
- });
1903
- session.threadId = String(threadId);
1904
- await core.sessionManager.patchRecord(session.id, { platform: { topicId: threadId } });
1905
- const sessionCount = contextResult?.sessionCount ?? listResult.sessions.length;
1906
- const mode = contextResult?.mode ?? "full";
1907
- const tokens = contextResult?.tokenEstimate ?? listResult.estimatedTokens;
1908
- const topicLink = buildDeepLink(chatId, threadId);
1909
- const replyTarget = ctx.message?.message_thread_id;
1910
- if (replyTarget !== threadId) {
1911
- await ctx.reply(
1912
- `\u2705 Session resumed \u2192 <a href="${topicLink}">Open topic</a>`,
1913
- { parse_mode: "HTML" }
1914
- );
1915
- }
1916
- await ctx.api.sendMessage(
1917
- chatId,
1918
- `\u2705 <b>Session resumed with context</b>
1919
- <b>Agent:</b> ${escapeHtml(session.agentName)}
1920
- <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>
1921
- <b>Sessions loaded:</b> ${sessionCount}
1922
- <b>Mode:</b> ${escapeHtml(mode)}
1923
- <b>~Tokens:</b> ${tokens.toLocaleString()}
1924
-
1925
- Context is ready \u2014 chat here to continue working with the agent.`,
1926
- {
1927
- message_thread_id: threadId,
1928
- parse_mode: "HTML",
1929
- reply_markup: buildSessionControlKeyboard(session.id, false, false)
1930
- }
1931
- );
1932
- session.warmup().catch((err) => log4.error({ err }, "Warm-up error"));
1933
- } catch (err) {
1934
- log4.error({ err }, "Resume session creation failed");
1935
- if (threadId) {
1936
- try {
1937
- await ctx.api.deleteForumTopic(chatId, threadId);
1938
- } catch {
1939
- }
1940
- }
1941
- const message = err instanceof Error ? err.message : typeof err === "object" ? JSON.stringify(err) : String(err);
1942
- await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
1943
- }
1944
- }
1945
- async function handleResume(ctx, core, chatId, assistant) {
1946
- const rawMatch = ctx.match;
1947
- const matchStr = typeof rawMatch === "string" ? rawMatch : "";
1948
- const parsed = parseResumeArgs(matchStr);
1949
- if (!parsed) {
1950
- await ctx.reply(
1951
- `\u274C <b>Invalid arguments.</b>
1952
-
1953
- Usage examples:
1954
- \u2022 <code>/resume</code> \u2014 latest 5 sessions
1955
- \u2022 <code>/resume pr 19</code>
1956
- \u2022 <code>/resume branch main</code>
1957
- \u2022 <code>/resume commit e0dd2fa4</code>
1958
- \u2022 <code>/resume f634acf05138</code> \u2014 checkpoint ID
1959
- \u2022 <code>/resume https://entire.io/gh/.../checkpoints/.../2e884e2c402a</code>
1960
- \u2022 <code>/resume https://entire.io/gh/.../commit/e0dd2fa4...</code>`,
1961
- { parse_mode: "HTML" }
1962
- );
1963
- return;
1964
- }
1965
- const { query } = parsed;
1966
- const userId = ctx.from?.id;
1967
- if (!userId) return;
1968
- await showWorkspacePicker(ctx, core, chatId, userId, query);
1969
- }
1970
- async function handlePendingResumeInput(ctx, core, chatId, assistantTopicId) {
1971
- const userId = ctx.from?.id;
1972
- if (!userId) return false;
1973
- const pending = pendingResumes.get(userId);
1974
- if (!pending || !ctx.message?.text) return false;
1975
- if (pending.step !== "workspace_input" && pending.step !== "workspace") return false;
1976
- const threadId = ctx.message.message_thread_id;
1977
- if (threadId && threadId !== assistantTopicId) return false;
1978
- if (pending.step === "workspace" && !looksLikePath(ctx.message.text.trim())) return false;
1979
- let workspace = ctx.message.text.trim();
1980
- if (!workspace) {
1981
- await ctx.reply("\u26A0\uFE0F Please enter a valid directory path.", { parse_mode: "HTML" });
1982
- return true;
1983
- }
1984
- if (!workspace.startsWith("/") && !workspace.startsWith("~")) {
1985
- const baseDir = core.configManager.get().workspace.baseDir;
1986
- workspace = `${baseDir.replace(/\/$/, "")}/${workspace}`;
1987
- }
1988
- const resolved = core.configManager.resolveWorkspace(workspace);
1989
- cleanupPending2(userId);
1990
- await executeResume(ctx, core, chatId, pending.query, resolved);
1991
- return true;
1992
- }
1993
- function setupResumeCallbacks(bot, core, chatId) {
1994
- bot.callbackQuery(/^m:resume:/, async (ctx) => {
1995
- const data = ctx.callbackQuery.data;
1996
- const userId = ctx.from?.id;
1997
- if (!userId) return;
1998
- try {
1999
- await ctx.answerCallbackQuery();
2000
- } catch {
2001
- }
2002
- const pending = pendingResumes.get(userId);
2003
- if (!pending) return;
2004
- if (data === "m:resume:ws:default") {
2005
- const baseDir = core.configManager.get().workspace.baseDir;
2006
- const resolved = core.configManager.resolveWorkspace(baseDir);
2007
- cleanupPending2(userId);
2008
- try {
2009
- await ctx.api.editMessageText(chatId, pending.messageId, `\u23F3 Using <code>${escapeHtml(resolved)}</code>...`, { parse_mode: "HTML" });
2010
- } catch {
2011
- }
2012
- await executeResume(ctx, core, chatId, pending.query, resolved);
2013
- return;
2014
- }
2015
- if (data === "m:resume:ws:custom") {
2016
- try {
2017
- await ctx.api.editMessageText(
2018
- chatId,
2019
- pending.messageId,
2020
- `\u270F\uFE0F <b>Enter project path:</b>
2021
-
2022
- Full path like <code>~/code/my-project</code>
2023
- Or just the folder name (will use workspace baseDir)`,
2024
- { parse_mode: "HTML" }
2025
- );
2026
- } catch {
2027
- await ctx.reply(`\u270F\uFE0F <b>Enter project path:</b>`, { parse_mode: "HTML" });
2028
- }
2029
- clearTimeout(pending.timer);
2030
- pending.step = "workspace_input";
2031
- pending.timer = setTimeout(() => pendingResumes.delete(userId), PENDING_TIMEOUT_MS2);
2032
- return;
2033
- }
2034
- if (data.startsWith("m:resume:ws:")) {
2035
- const dirName = data.replace("m:resume:ws:", "");
2036
- const baseDir = core.configManager.get().workspace.baseDir;
2037
- const resolved = core.configManager.resolveWorkspace(path.join(baseDir.replace(/^~/, os.homedir()), dirName));
2038
- cleanupPending2(userId);
2039
- try {
2040
- await ctx.api.editMessageText(chatId, pending.messageId, `\u23F3 Using <code>${escapeHtml(resolved)}</code>...`, { parse_mode: "HTML" });
2041
- } catch {
2042
- }
2043
- await executeResume(ctx, core, chatId, pending.query, resolved);
2044
- return;
2045
- }
2046
- });
2047
- }
2048
-
2049
- // src/adapters/telegram/commands/settings.ts
2050
- import { InlineKeyboard as InlineKeyboard7 } from "grammy";
2051
- var log5 = createChildLogger({ module: "telegram-settings" });
2052
- function buildSettingsKeyboard(core) {
2053
- const config = core.configManager.get();
2054
- const fields = getSafeFields();
2055
- const kb = new InlineKeyboard7();
2056
- for (const field of fields) {
2057
- const value = getConfigValue(config, field.path);
2058
- const label = formatFieldLabel(field, value);
2059
- if (field.type === "toggle") {
2060
- kb.text(`${label}`, `s:toggle:${field.path}`).row();
2061
- } else if (field.type === "select") {
2062
- kb.text(`${label}`, `s:select:${field.path}`).row();
2063
- } else {
2064
- kb.text(`${label}`, `s:input:${field.path}`).row();
2065
- }
2066
- }
2067
- kb.text("\u25C0\uFE0F Back to Menu", "s:back");
2068
- return kb;
2069
- }
2070
- function formatFieldLabel(field, value) {
2071
- const icons = {
2072
- agent: "\u{1F916}",
2073
- logging: "\u{1F4DD}",
2074
- tunnel: "\u{1F517}",
2075
- security: "\u{1F512}",
2076
- workspace: "\u{1F4C1}",
2077
- storage: "\u{1F4BE}",
2078
- speech: "\u{1F3A4}"
2079
- };
2080
- const icon = icons[field.group] ?? "\u2699\uFE0F";
2081
- if (field.type === "toggle") {
2082
- return `${icon} ${field.displayName}: ${value ? "ON" : "OFF"}`;
2083
- }
2084
- const displayValue = value === null || value === void 0 ? "Not set" : String(value);
2085
- return `${icon} ${field.displayName}: ${displayValue}`;
2086
- }
2087
- async function handleSettings(ctx, core) {
2088
- const kb = buildSettingsKeyboard(core);
2089
- await ctx.reply(`<b>\u2699\uFE0F Settings</b>
2090
- Tap to change:`, {
2091
- parse_mode: "HTML",
2092
- reply_markup: kb
2093
- });
2094
- }
2095
- function setupSettingsCallbacks(bot, core, getAssistantSession) {
2096
- bot.callbackQuery(/^s:toggle:/, async (ctx) => {
2097
- const fieldPath = ctx.callbackQuery.data.replace("s:toggle:", "");
2098
- const config = core.configManager.get();
2099
- const currentValue = getConfigValue(config, fieldPath);
2100
- const newValue = !currentValue;
2101
- try {
2102
- const updates = buildNestedUpdate(fieldPath, newValue);
2103
- await core.configManager.save(updates, fieldPath);
2104
- const toast = isHotReloadable(fieldPath) ? `\u2705 ${fieldPath} = ${newValue}` : `\u2705 ${fieldPath} = ${newValue} (restart needed)`;
2105
- try {
2106
- await ctx.answerCallbackQuery({ text: toast });
2107
- } catch {
2108
- }
2109
- try {
2110
- await ctx.editMessageReplyMarkup({ reply_markup: buildSettingsKeyboard(core) });
2111
- } catch {
2112
- }
2113
- } catch (err) {
2114
- log5.error({ err, fieldPath }, "Failed to toggle config");
2115
- try {
2116
- await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
2117
- } catch {
2118
- }
2119
- }
2120
- });
2121
- bot.callbackQuery(/^s:select:/, async (ctx) => {
2122
- const fieldPath = ctx.callbackQuery.data.replace("s:select:", "");
2123
- const config = core.configManager.get();
2124
- const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
2125
- if (!fieldDef) return;
2126
- const options = resolveOptions(fieldDef, config) ?? [];
2127
- const currentValue = getConfigValue(config, fieldPath);
2128
- const kb = new InlineKeyboard7();
2129
- for (const opt of options) {
2130
- const marker = opt === String(currentValue) ? " \u2713" : "";
2131
- kb.text(`${opt}${marker}`, `s:pick:${fieldPath}:${opt}`).row();
2132
- }
2133
- kb.text("\u25C0\uFE0F Back", "s:back:refresh");
2134
- try {
2135
- await ctx.answerCallbackQuery();
2136
- } catch {
2137
- }
2138
- try {
2139
- await ctx.editMessageText(`<b>\u2699\uFE0F ${fieldDef.displayName}</b>
2140
- Select a value:`, {
2141
- parse_mode: "HTML",
2142
- reply_markup: kb
2143
- });
2144
- } catch {
2145
- }
2146
- });
2147
- bot.callbackQuery(/^s:pick:/, async (ctx) => {
2148
- const parts = ctx.callbackQuery.data.replace("s:pick:", "").split(":");
2149
- const fieldPath = parts.slice(0, -1).join(":");
2150
- const newValue = parts[parts.length - 1];
2151
- try {
2152
- if (fieldPath === "speech.stt.provider") {
2153
- const config = core.configManager.get();
2154
- const providerConfig = config.speech?.stt?.providers?.[newValue];
2155
- if (!providerConfig?.apiKey) {
2156
- const assistant = getAssistantSession();
2157
- if (assistant) {
2158
- try {
2159
- await ctx.answerCallbackQuery({ text: `\u{1F511} API key needed \u2014 check Assistant topic` });
2160
- } catch {
2161
- }
2162
- const prompt = `User wants to enable ${newValue} as Speech-to-Text provider, but no API key is configured yet. Guide them to get a ${newValue} API key and set it up. After they provide the key, run both commands: \`openacp config set speech.stt.providers.${newValue}.apiKey <key>\` and \`openacp config set speech.stt.provider ${newValue}\``;
2163
- await assistant.enqueuePrompt(prompt);
2164
- return;
2165
- }
2166
- try {
2167
- await ctx.answerCallbackQuery({ text: `\u26A0\uFE0F Set API key first: openacp config set speech.stt.providers.${newValue}.apiKey <key>` });
2168
- } catch {
2169
- }
2170
- return;
2171
- }
2172
- }
2173
- const updates = buildNestedUpdate(fieldPath, newValue);
2174
- await core.configManager.save(updates, fieldPath);
2175
- try {
2176
- await ctx.answerCallbackQuery({ text: `\u2705 ${fieldPath} = ${newValue}` });
2177
- } catch {
2178
- }
2179
- try {
2180
- await ctx.editMessageText(`<b>\u2699\uFE0F Settings</b>
2181
- Tap to change:`, {
2182
- parse_mode: "HTML",
2183
- reply_markup: buildSettingsKeyboard(core)
2184
- });
2185
- } catch {
2186
- }
2187
- } catch (err) {
2188
- log5.error({ err, fieldPath }, "Failed to set config");
2189
- try {
2190
- await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
2191
- } catch {
2192
- }
2193
- }
2194
- });
2195
- bot.callbackQuery(/^s:input:/, async (ctx) => {
2196
- const fieldPath = ctx.callbackQuery.data.replace("s:input:", "");
2197
- const config = core.configManager.get();
2198
- const fieldDef = getSafeFields().find((f) => f.path === fieldPath);
2199
- if (!fieldDef) return;
2200
- const currentValue = getConfigValue(config, fieldPath);
2201
- const assistant = getAssistantSession();
2202
- if (!assistant) {
2203
- try {
2204
- await ctx.answerCallbackQuery({ text: "\u26A0\uFE0F Start the assistant first (/assistant)" });
2205
- } catch {
2206
- }
2207
- return;
2208
- }
2209
- try {
2210
- await ctx.answerCallbackQuery({ text: `Delegating to assistant...` });
2211
- } catch {
2212
- }
2213
- const prompt = `User wants to change ${fieldDef.displayName} (config path: ${fieldPath}). Current value: ${JSON.stringify(currentValue)}. Ask them for the new value and apply it using: openacp config set ${fieldPath} <value>`;
2214
- await assistant.enqueuePrompt(prompt);
2215
- });
2216
- bot.callbackQuery("s:back", async (ctx) => {
2217
- try {
2218
- await ctx.answerCallbackQuery();
2219
- } catch {
2220
- }
2221
- const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-YY5MKHEK.js");
2222
- try {
2223
- await ctx.editMessageText(`<b>OpenACP Menu</b>
2224
- Choose an action:`, {
2225
- parse_mode: "HTML",
2226
- reply_markup: buildMenuKeyboard3()
2227
- });
2228
- } catch {
2229
- }
2230
- });
2231
- bot.callbackQuery("s:back:refresh", async (ctx) => {
2232
- try {
2233
- await ctx.answerCallbackQuery();
2234
- } catch {
2235
- }
2236
- try {
2237
- await ctx.editMessageText(`<b>\u2699\uFE0F Settings</b>
2238
- Tap to change:`, {
2239
- parse_mode: "HTML",
2240
- reply_markup: buildSettingsKeyboard(core)
2241
- });
2242
- } catch {
2243
- }
2244
- });
2245
- }
2246
- function buildNestedUpdate(dotPath, value) {
2247
- const parts = dotPath.split(".");
2248
- const result = {};
2249
- let target = result;
2250
- for (let i = 0; i < parts.length - 1; i++) {
2251
- target[parts[i]] = {};
2252
- target = target[parts[i]];
2253
- }
2254
- target[parts[parts.length - 1]] = value;
2255
- return result;
2256
- }
2257
-
2258
- // src/adapters/telegram/commands/doctor.ts
2259
- import { InlineKeyboard as InlineKeyboard8 } from "grammy";
2260
- var log6 = createChildLogger({ module: "telegram-cmd-doctor" });
2261
- var pendingFixesStore = /* @__PURE__ */ new Map();
2262
- function renderReport(report) {
2263
- const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
2264
- const lines = ["\u{1FA7A} <b>OpenACP Doctor</b>\n"];
2265
- for (const category of report.categories) {
2266
- lines.push(`<b>${category.name}</b>`);
2267
- for (const result of category.results) {
2268
- lines.push(` ${icons[result.status]} ${escapeHtml2(result.message)}`);
2269
- }
2270
- lines.push("");
2271
- }
2272
- const { passed, warnings, failed, fixed } = report.summary;
2273
- const fixedStr = fixed > 0 ? `, ${fixed} fixed` : "";
2274
- lines.push(`<b>Result:</b> ${passed} passed, ${warnings} warnings, ${failed} failed${fixedStr}`);
2275
- let keyboard;
2276
- if (report.pendingFixes.length > 0) {
2277
- keyboard = new InlineKeyboard8();
2278
- for (let i = 0; i < report.pendingFixes.length; i++) {
2279
- const label = `\u{1F527} Fix: ${report.pendingFixes[i].message.slice(0, 30)}`;
2280
- keyboard.text(label, `m:doctor:fix:${i}`).row();
2281
- }
2282
- }
2283
- return { text: lines.join("\n"), keyboard };
2284
- }
2285
- function escapeHtml2(text) {
2286
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2287
- }
2288
- async function handleDoctor(ctx) {
2289
- const statusMsg = await ctx.reply("\u{1FA7A} Running diagnostics...", { parse_mode: "HTML" });
2290
- try {
2291
- const engine = new DoctorEngine();
2292
- const report = await engine.runAll();
2293
- const { text, keyboard } = renderReport(report);
2294
- const storeKey = `${ctx.chat.id}:${statusMsg.message_id}`;
2295
- if (report.pendingFixes.length > 0) {
2296
- pendingFixesStore.set(storeKey, report.pendingFixes);
2297
- }
2298
- await ctx.api.editMessageText(ctx.chat.id, statusMsg.message_id, text, {
2299
- parse_mode: "HTML",
2300
- reply_markup: keyboard
2301
- });
2302
- } catch (err) {
2303
- log6.error({ err }, "Doctor command failed");
2304
- await ctx.api.editMessageText(
2305
- ctx.chat.id,
2306
- statusMsg.message_id,
2307
- `\u274C Doctor failed: ${err instanceof Error ? err.message : String(err)}`,
2308
- { parse_mode: "HTML" }
2309
- );
2310
- }
2311
- }
2312
- function setupDoctorCallbacks(bot) {
2313
- bot.callbackQuery(/^m:doctor:fix:/, async (ctx) => {
2314
- const data = ctx.callbackQuery.data;
2315
- const index = parseInt(data.replace("m:doctor:fix:", ""), 10);
2316
- const chatId = ctx.callbackQuery.message?.chat.id;
2317
- const messageId = ctx.callbackQuery.message?.message_id;
2318
- try {
2319
- await ctx.answerCallbackQuery({ text: "Applying fix..." });
2320
- } catch {
2321
- }
2322
- if (chatId === void 0 || messageId === void 0) return;
2323
- const storeKey = `${chatId}:${messageId}`;
2324
- const fixes = pendingFixesStore.get(storeKey);
2325
- if (!fixes || index < 0 || index >= fixes.length) {
2326
- try {
2327
- await ctx.answerCallbackQuery({ text: "Fix no longer available" });
2328
- } catch {
2329
- }
2330
- return;
2331
- }
2332
- const pending = fixes[index];
2333
- try {
2334
- const result = await pending.fix();
2335
- if (result.success) {
2336
- const engine = new DoctorEngine();
2337
- const report = await engine.runAll();
2338
- const { text, keyboard } = renderReport(report);
2339
- if (report.pendingFixes.length > 0) {
2340
- pendingFixesStore.set(storeKey, report.pendingFixes);
2341
- } else {
2342
- pendingFixesStore.delete(storeKey);
2343
- }
2344
- await ctx.editMessageText(text, { parse_mode: "HTML", reply_markup: keyboard });
2345
- } else {
2346
- try {
2347
- await ctx.answerCallbackQuery({ text: `Fix failed: ${result.message}` });
2348
- } catch {
2349
- }
2350
- }
2351
- } catch (err) {
2352
- log6.error({ err, index }, "Doctor fix callback failed");
2353
- }
2354
- });
2355
- bot.callbackQuery("m:doctor", async (ctx) => {
2356
- try {
2357
- await ctx.answerCallbackQuery();
2358
- } catch {
2359
- }
2360
- await handleDoctor(ctx);
2361
- });
2362
- }
2363
-
2364
- // src/adapters/telegram/commands/tunnel.ts
2365
- import { InlineKeyboard as InlineKeyboard9 } from "grammy";
2366
- var log7 = createChildLogger({ module: "telegram-cmd-tunnel" });
2367
- async function handleTunnel(ctx, core) {
2368
- if (!core.tunnelService) {
2369
- await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
2370
- return;
2371
- }
2372
- const match = ctx.match?.trim() ?? "";
2373
- if (match.startsWith("stop ")) {
2374
- const portStr = match.slice(5).trim();
2375
- const port = parseInt(portStr, 10);
2376
- if (isNaN(port)) {
2377
- await ctx.reply("\u274C Invalid port number.", { parse_mode: "HTML" });
2378
- return;
2379
- }
2380
- try {
2381
- await core.tunnelService.stopTunnel(port);
2382
- await ctx.reply(`\u{1F50C} Tunnel stopped: port ${port}`, { parse_mode: "HTML" });
2383
- } catch (err) {
2384
- await ctx.reply(`\u274C ${escapeHtml(err.message)}`, { parse_mode: "HTML" });
2385
- }
2386
- return;
2387
- }
2388
- if (match) {
2389
- const parts = match.split(/\s+/);
2390
- const port = parseInt(parts[0], 10);
2391
- if (isNaN(port)) {
2392
- await ctx.reply("\u274C Invalid port number. Usage: <code>/tunnel 3000 [label]</code>", { parse_mode: "HTML" });
2393
- return;
2394
- }
2395
- const label = parts.slice(1).join(" ") || void 0;
2396
- const threadId = ctx.message?.message_thread_id;
2397
- let sessionId;
2398
- if (threadId) {
2399
- const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
2400
- if (session) sessionId = session.id;
2401
- }
2402
- try {
2403
- await ctx.reply(`\u23F3 Starting tunnel for port ${port}...`, { parse_mode: "HTML" });
2404
- const entry = await core.tunnelService.addTunnel(port, { label, sessionId });
2405
- await ctx.reply(
2406
- `\u{1F517} <b>Tunnel active</b>
2407
-
2408
- Port ${port}${label ? ` (${escapeHtml(label)})` : ""}
2409
- \u2192 <a href="${escapeHtml(entry.publicUrl || "")}">${escapeHtml(entry.publicUrl || "")}</a>`,
2410
- { parse_mode: "HTML" }
2411
- );
2412
- } catch (err) {
2413
- await ctx.reply(`\u274C ${escapeHtml(err.message)}`, { parse_mode: "HTML" });
2414
- }
2415
- return;
2416
- }
2417
- await ctx.reply(
2418
- `<b>Tunnel commands:</b>
2419
-
2420
- <code>/tunnel &lt;port&gt; [label]</code> \u2014 Create tunnel
2421
- <code>/tunnel stop &lt;port&gt;</code> \u2014 Stop tunnel
2422
- <code>/tunnels</code> \u2014 List active tunnels`,
2423
- { parse_mode: "HTML" }
2424
- );
2425
- }
2426
- async function handleTunnels(ctx, core) {
2427
- if (!core.tunnelService) {
2428
- await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
2429
- return;
2430
- }
2431
- const threadId = ctx.message?.message_thread_id;
2432
- let entries = core.tunnelService.listTunnels();
2433
- let sessionScoped = false;
2434
- if (threadId) {
2435
- const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
2436
- if (session) {
2437
- entries = entries.filter((e) => e.sessionId === session.id);
2438
- sessionScoped = true;
2439
- }
2440
- }
2441
- if (entries.length === 0) {
2442
- const hint = sessionScoped ? "No tunnels for this session.\n\nUse <code>/tunnel &lt;port&gt;</code> to create one." : "No active tunnels.\n\nUse <code>/tunnel &lt;port&gt;</code> to create one.";
2443
- await ctx.reply(hint, { parse_mode: "HTML" });
2444
- return;
2445
- }
2446
- const lines = entries.map((e) => {
2447
- const status = e.status === "active" ? "\u2705" : e.status === "starting" ? "\u23F3" : "\u274C";
2448
- const label = e.label ? ` (${escapeHtml(e.label)})` : "";
2449
- const url = e.publicUrl ? `
2450
- \u2192 <a href="${escapeHtml(e.publicUrl)}">${escapeHtml(e.publicUrl)}</a>` : "";
2451
- return `${status} Port <b>${e.port}</b>${label}${url}`;
2452
- });
2453
- const keyboard = new InlineKeyboard9();
2454
- for (const e of entries) {
2455
- keyboard.text(`\u{1F50C} Stop ${e.port}${e.label ? ` (${e.label})` : ""}`, `tn:stop:${e.port}`).row();
2456
- }
2457
- if (entries.length > 1) {
2458
- keyboard.text("\u{1F50C} Stop all", "tn:stop-all").row();
2459
- }
2460
- await ctx.reply(
2461
- `<b>Active tunnels:</b>
2462
-
2463
- ${lines.join("\n\n")}`,
2464
- { parse_mode: "HTML", reply_markup: keyboard }
2465
- );
2466
- }
2467
- function setupTunnelCallbacks(bot, core) {
2468
- bot.callbackQuery(/^tn:/, async (ctx) => {
2469
- const data = ctx.callbackQuery.data;
2470
- if (!core.tunnelService) {
2471
- await ctx.answerCallbackQuery({ text: "Tunnel not enabled" });
2472
- return;
2473
- }
2474
- try {
2475
- if (data === "tn:stop-all") {
2476
- const entries = core.tunnelService.listTunnels();
2477
- for (const e of entries) {
2478
- try {
2479
- await core.tunnelService.stopTunnel(e.port);
2480
- } catch {
2481
- }
2482
- }
2483
- await ctx.answerCallbackQuery({ text: "All tunnels stopped" });
2484
- await ctx.editMessageText("\u{1F50C} All tunnels stopped.", { parse_mode: "HTML" });
2485
- } else if (data.startsWith("tn:stop:")) {
2486
- const port = parseInt(data.replace("tn:stop:", ""), 10);
2487
- await core.tunnelService.stopTunnel(port);
2488
- await ctx.answerCallbackQuery({ text: `Port ${port} stopped` });
2489
- const remaining = core.tunnelService.listTunnels();
2490
- if (remaining.length === 0) {
2491
- await ctx.editMessageText("\u{1F50C} All tunnels stopped.", { parse_mode: "HTML" });
2492
- } else {
2493
- const kb = new InlineKeyboard9();
2494
- for (const e of remaining) {
2495
- kb.text(`\u{1F50C} Stop ${e.port}${e.label ? ` (${e.label})` : ""}`, `tn:stop:${e.port}`).row();
2496
- }
2497
- if (remaining.length > 1) {
2498
- kb.text("\u{1F50C} Stop all", "tn:stop-all").row();
2499
- }
2500
- await ctx.editMessageText(
2501
- `<b>Active tunnels:</b>
2502
-
2503
- ` + remaining.map((e) => {
2504
- const status = e.status === "active" ? "\u2705" : "\u23F3";
2505
- const label = e.label ? ` (${escapeHtml(e.label)})` : "";
2506
- const url = e.publicUrl ? `
2507
- \u2192 <a href="${escapeHtml(e.publicUrl)}">${escapeHtml(e.publicUrl)}</a>` : "";
2508
- return `${status} Port <b>${e.port}</b>${label}${url}`;
2509
- }).join("\n\n"),
2510
- { parse_mode: "HTML", reply_markup: kb }
2511
- );
2512
- }
2513
- }
2514
- } catch (err) {
2515
- await ctx.answerCallbackQuery({ text: err.message });
2516
- }
2517
- });
2518
- }
2519
-
2520
- // src/adapters/telegram/commands/index.ts
2521
- function setupCommands(bot, core, chatId, assistant) {
2522
- bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
2523
- bot.command("newchat", (ctx) => handleNewChat(ctx, core, chatId));
2524
- bot.command("cancel", (ctx) => handleCancel(ctx, core, assistant));
2525
- bot.command("status", (ctx) => handleStatus(ctx, core));
2526
- bot.command("sessions", (ctx) => handleTopics(ctx, core));
2527
- bot.command("agents", (ctx) => handleAgents(ctx, core));
2528
- bot.command("install", (ctx) => handleInstall(ctx, core));
2529
- bot.command("help", (ctx) => handleHelp(ctx));
2530
- bot.command("menu", (ctx) => handleMenu(ctx));
2531
- bot.command("enable_dangerous", (ctx) => handleEnableDangerous(ctx, core));
2532
- bot.command("disable_dangerous", (ctx) => handleDisableDangerous(ctx, core));
2533
- bot.command("restart", (ctx) => handleRestart(ctx, core));
2534
- bot.command("update", (ctx) => handleUpdate(ctx, core));
2535
- bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
2536
- bot.command("clear", (ctx) => handleClear(ctx, assistant));
2537
- bot.command("doctor", (ctx) => handleDoctor(ctx));
2538
- bot.command("usage", (ctx) => handleUsage(ctx, core));
2539
- bot.command("tunnel", (ctx) => handleTunnel(ctx, core));
2540
- bot.command("tunnels", (ctx) => handleTunnels(ctx, core));
2541
- bot.command("archive", (ctx) => handleArchive(ctx, core));
2542
- bot.command("summary", (ctx) => handleSummary(ctx, core));
2543
- bot.command("text_to_speech", (ctx) => handleTTS(ctx, core));
2544
- bot.command("verbosity", (ctx) => handleVerbosity(ctx, core));
2545
- bot.command("resume", (ctx) => handleResume(ctx, core, chatId, assistant));
2546
- }
2547
- function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSession) {
2548
- setupNewSessionCallbacks(bot, core, chatId);
2549
- setupResumeCallbacks(bot, core, chatId);
2550
- setupSessionCallbacks(bot, core, chatId, systemTopicIds);
2551
- setupSettingsCallbacks(bot, core, getAssistantSession ?? (() => void 0));
2552
- setupDoctorCallbacks(bot);
2553
- setupTunnelCallbacks(bot, core);
2554
- bot.callbackQuery(/^ag:/, (ctx) => handleAgentCallback(ctx, core));
2555
- bot.callbackQuery(/^na:/, async (ctx) => {
2556
- const agentKey = ctx.callbackQuery.data.replace("na:", "");
2557
- await ctx.answerCallbackQuery();
2558
- await createSessionDirect(
2559
- ctx,
2560
- core,
2561
- chatId,
2562
- agentKey,
2563
- core.configManager.get().workspace.baseDir
2564
- );
2565
- });
2566
- bot.callbackQuery(/^ar:/, (ctx) => handleArchiveConfirm(ctx, core, chatId));
2567
- bot.callbackQuery(/^sm:/, (ctx) => handleSummaryCallback(ctx, core, chatId));
2568
- bot.callbackQuery(/^m:/, async (ctx) => {
2569
- const data = ctx.callbackQuery.data;
2570
- try {
2571
- await ctx.answerCallbackQuery();
2572
- } catch {
2573
- }
2574
- switch (data) {
2575
- case "m:new":
2576
- await handleNew(ctx, core, chatId);
2577
- break;
2578
- case "m:status":
2579
- await handleStatus(ctx, core);
2580
- break;
2581
- case "m:agents":
2582
- await handleAgents(ctx, core);
2583
- break;
2584
- case "m:help":
2585
- await handleHelp(ctx);
2586
- break;
2587
- case "m:restart":
2588
- await handleRestart(ctx, core);
2589
- break;
2590
- case "m:update":
2591
- await handleUpdate(ctx, core);
2592
- break;
2593
- case "m:integrate":
2594
- await handleIntegrate(ctx, core);
2595
- break;
2596
- case "m:topics":
2597
- await handleTopics(ctx, core);
2598
- break;
2599
- case "m:settings":
2600
- await handleSettings(ctx, core);
2601
- break;
2602
- }
2603
- });
2604
- }
2605
- var STATIC_COMMANDS = [
2606
- { command: "new", description: "Create new session" },
2607
- { command: "newchat", description: "New chat, same agent & workspace" },
2608
- { command: "cancel", description: "Cancel current session" },
2609
- { command: "status", description: "Show status" },
2610
- { command: "sessions", description: "List all sessions" },
2611
- { command: "agents", description: "List available agents" },
2612
- { command: "install", description: "Install a new agent" },
2613
- { command: "help", description: "Help" },
2614
- { command: "menu", description: "Show menu" },
2615
- {
2616
- command: "enable_dangerous",
2617
- description: "Auto-approve all permission requests (session only)"
2618
- },
2619
- {
2620
- command: "disable_dangerous",
2621
- description: "Restore normal permission prompts (session only)"
2622
- },
2623
- { command: "integrate", description: "Manage agent integrations" },
2624
- { command: "handoff", description: "Continue this session in your terminal" },
2625
- { command: "clear", description: "Clear assistant history" },
2626
- { command: "restart", description: "Restart OpenACP" },
2627
- { command: "update", description: "Update to latest version and restart" },
2628
- { command: "doctor", description: "Run system diagnostics" },
2629
- { command: "usage", description: "View token usage and cost report" },
2630
- { command: "tunnel", description: "Create/stop tunnel for a local port" },
2631
- { command: "tunnels", description: "List active tunnels" },
2632
- { command: "archive", description: "Archive session topic (recreate with clean history)" },
2633
- { command: "summary", description: "Get AI summary of current session" },
2634
- { command: "text_to_speech", description: "Toggle Text to Speech (/text_to_speech on, /text_to_speech off)" },
2635
- { command: "verbosity", description: "Set display verbosity (/verbosity low|medium|high)" },
2636
- { command: "resume", description: "Resume with conversation history from Entire checkpoints" }
2637
- ];
2638
-
2639
- // src/adapters/telegram/permissions.ts
2640
- import { InlineKeyboard as InlineKeyboard10 } from "grammy";
2641
- import { nanoid } from "nanoid";
2642
- var log8 = createChildLogger({ module: "telegram-permissions" });
2643
- var PermissionHandler = class {
2644
- constructor(bot, chatId, getSession, sendNotification) {
2645
- this.bot = bot;
2646
- this.chatId = chatId;
2647
- this.getSession = getSession;
2648
- this.sendNotification = sendNotification;
2649
- }
2650
- pending = /* @__PURE__ */ new Map();
2651
- async sendPermissionRequest(session, request) {
2652
- const threadId = Number(session.threadId);
2653
- const callbackKey = nanoid(8);
2654
- this.pending.set(callbackKey, {
2655
- sessionId: session.id,
2656
- requestId: request.id,
2657
- options: request.options.map((o) => ({ id: o.id, isAllow: o.isAllow }))
2658
- });
2659
- const keyboard = new InlineKeyboard10();
2660
- for (const option of request.options) {
2661
- const emoji = option.isAllow ? "\u2705" : "\u274C";
2662
- keyboard.text(`${emoji} ${option.label}`, `p:${callbackKey}:${option.id}`);
2663
- }
2664
- const msg = await this.bot.api.sendMessage(
2665
- this.chatId,
2666
- `\u{1F510} <b>Permission request:</b>
2667
-
2668
- ${escapeHtml(request.description)}`,
2669
- {
2670
- message_thread_id: threadId,
2671
- parse_mode: "HTML",
2672
- reply_markup: keyboard,
2673
- disable_notification: false
2674
- }
2675
- );
2676
- const deepLink = buildDeepLink(this.chatId, threadId, msg.message_id);
2677
- void this.sendNotification({
2678
- sessionId: session.id,
2679
- sessionName: session.name,
2680
- type: "permission",
2681
- summary: request.description,
2682
- deepLink
2683
- });
2684
- }
2685
- setupCallbackHandler() {
2686
- this.bot.on("callback_query:data", async (ctx) => {
2687
- const data = ctx.callbackQuery.data;
2688
- if (!data.startsWith("p:")) return;
2689
- const parts = data.split(":");
2690
- if (parts.length < 3) return;
2691
- const [, callbackKey, optionId] = parts;
2692
- const pending = this.pending.get(callbackKey);
2693
- if (!pending) {
2694
- try {
2695
- await ctx.answerCallbackQuery({ text: "\u274C Expired" });
2696
- } catch {
2697
- }
2698
- return;
2699
- }
2700
- const session = this.getSession(pending.sessionId);
2701
- const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
2702
- log8.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
2703
- if (session?.permissionGate.requestId === pending.requestId) {
2704
- session.permissionGate.resolve(optionId);
2705
- }
2706
- this.pending.delete(callbackKey);
2707
- try {
2708
- await ctx.answerCallbackQuery({ text: "\u2705 Responded" });
2709
- } catch {
2710
- }
2711
- try {
2712
- await ctx.editMessageReplyMarkup({ reply_markup: void 0 });
2713
- } catch {
2714
- }
2715
- });
2716
- }
2717
- };
2718
-
2719
- // src/adapters/telegram/assistant.ts
2720
- var log9 = createChildLogger({ module: "telegram-assistant" });
2721
- async function spawnAssistant(core, adapter, assistantTopicId) {
2722
- const config = core.configManager.get();
2723
- log9.info({ agent: config.defaultAgent }, "Creating assistant session...");
2724
- const session = await core.createSession({
2725
- channelId: "telegram",
2726
- agentName: config.defaultAgent,
2727
- workingDirectory: core.configManager.resolveWorkspace(),
2728
- initialName: "Assistant"
2729
- // Prevent auto-naming from triggering after system prompt
2730
- });
2731
- session.threadId = String(assistantTopicId);
2732
- log9.info({ sessionId: session.id }, "Assistant agent spawned");
2733
- const allRecords = core.sessionManager.listRecords();
2734
- const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
2735
- const statusCounts = /* @__PURE__ */ new Map();
2736
- for (const r of allRecords) {
2737
- statusCounts.set(r.status, (statusCounts.get(r.status) ?? 0) + 1);
2738
- }
2739
- const topicSummary = Array.from(statusCounts.entries()).map(([status, count]) => ({ status, count }));
2740
- const installedAgents = Object.keys(core.agentCatalog.getInstalledEntries());
2741
- const availableItems = core.agentCatalog.getAvailable();
2742
- const availableAgentCount = availableItems.filter((i) => !i.installed).length;
2743
- const ctx = {
2744
- config,
2745
- activeSessionCount: activeCount,
2746
- totalSessionCount: allRecords.length,
2747
- topicSummary,
2748
- installedAgents,
2749
- availableAgentCount
2750
- };
2751
- const systemPrompt = buildAssistantSystemPrompt(ctx);
2752
- const ready = session.enqueuePrompt(systemPrompt).then(() => {
2753
- log9.info({ sessionId: session.id }, "Assistant system prompt completed");
2754
- }).catch((err) => {
2755
- log9.warn({ err }, "Assistant system prompt failed");
2756
- });
2757
- return { session, ready };
2758
- }
2759
- function buildWelcomeMessage(ctx) {
2760
- const { activeCount, errorCount, totalCount, agents, defaultAgent } = ctx;
2761
- const agentList = agents.map((a) => `${a}${a === defaultAgent ? " (default)" : ""}`).join(", ");
2762
- if (totalCount === 0) {
2763
- return `\u{1F44B} <b>OpenACP is ready!</b>
2764
-
2765
- No sessions yet. Tap \u{1F195} New Session to start, or ask me anything!`;
2766
- }
2767
- if (errorCount > 0) {
2768
- return `\u{1F44B} <b>OpenACP is ready!</b>
2769
-
2770
- \u{1F4CA} ${activeCount} active, ${errorCount} errors / ${totalCount} total
2771
- \u26A0\uFE0F ${errorCount} session${errorCount > 1 ? "s have" : " has"} errors \u2014 ask me to check if you'd like.
2772
-
2773
- Agents: ${agentList}`;
2774
- }
2775
- return `\u{1F44B} <b>OpenACP is ready!</b>
2776
-
2777
- \u{1F4CA} ${activeCount} active / ${totalCount} total
2778
- Agents: ${agentList}`;
2779
- }
2780
- function buildAssistantSystemPrompt(ctx) {
2781
- const { config, activeSessionCount, totalSessionCount, topicSummary, installedAgents, availableAgentCount } = ctx;
2782
- const agentNames = installedAgents?.length ? installedAgents.join(", ") : Object.keys(config.agents).join(", ");
2783
- const topicBreakdown = topicSummary.map((s) => `${s.status}: ${s.count}`).join(", ") || "none";
2784
- return `You are the OpenACP Assistant \u2014 a helpful guide for managing AI coding sessions.
2785
-
2786
- ## Current State
2787
- - Active sessions: ${activeSessionCount} / ${totalSessionCount} total
2788
- - Topics by status: ${topicBreakdown}
2789
- - Installed agents: ${agentNames}
2790
- - Available in ACP Registry: ${availableAgentCount ?? "28+"} more agents (use /agents to browse)
2791
- - Default agent: ${config.defaultAgent}
2792
- - Workspace base directory: ${config.workspace.baseDir}
2793
- - STT: ${config.speech?.stt?.provider ? `${config.speech.stt.provider} \u2705` : "Not configured"}
2794
-
2795
- ## Action Playbook
2796
-
2797
- ### Create Session
2798
- - The workspace is the project directory where the agent will work (read, write, execute code). It is NOT the base directory \u2014 it should be a specific project folder like \`~/code/my-project\` or \`${config.workspace.baseDir}/my-app\`.
2799
- - Ask which agent to use (if multiple are installed). Show installed: ${agentNames}
2800
- - Ask which project directory to use as workspace. Suggest \`${config.workspace.baseDir}\` as the base, but explain the user can provide any path.
2801
- - Confirm before creating: show agent name + full workspace path.
2802
- - Create via: \`openacp api new <agent> <workspace>\`
2803
-
2804
- ### Browse & Install Agents
2805
- - Guide users to /agents command to see all available agents (installed + from ACP Registry)
2806
- - The /agents list is paginated with install buttons \u2014 users can tap to install directly
2807
- - For CLI users: \`openacp agents install <name>\`
2808
- - Some agents need login/setup after install \u2014 guide users to \`openacp agents info <name>\` for setup steps
2809
- - To run agent CLI for login: \`openacp agents run <name> -- <args>\`
2810
- - Common setup examples:
2811
- - Gemini: \`openacp agents run gemini -- auth login\`
2812
- - GitHub Copilot: \`openacp agents run copilot -- auth login\`
2813
- - Codex: needs OPENAI_API_KEY environment variable
2814
-
2815
- ### Check Status / List Sessions
2816
- - Run \`openacp api status\` for active sessions overview
2817
- - Run \`openacp api topics\` for full list with statuses
2818
- - Format the output nicely for the user
2819
-
2820
- ### Cancel Session
2821
- - Run \`openacp api status\` to see what's active
2822
- - If 1 active session \u2192 ask user to confirm \u2192 \`openacp api cancel <id>\`
2823
- - If multiple \u2192 list them, ask user which one to cancel
2824
-
2825
- ### Troubleshoot (Session Stuck, Errors)
2826
- - Run \`openacp api health\` + \`openacp api status\` to diagnose
2827
- - Small issue (stuck session) \u2192 suggest cancel + create new
2828
- - Big issue (system-level) \u2192 suggest restart, ask for confirmation first
2829
-
2830
- ### Cleanup Old Sessions
2831
- - Run \`openacp api topics --status finished,error\` to see what can be cleaned
2832
- - Report the count, ask user to confirm
2833
- - Execute: \`openacp api cleanup --status <statuses>\`
2834
-
2835
- ### Configuration
2836
- - View: \`openacp config\` (or \`openacp api config\` \u2014 deprecated)
2837
- - Update: \`openacp config set <key> <value>\`
2838
- - When user asks about "settings" or "config", use \`openacp config set\` directly
2839
- - When receiving a delegated request from the Settings menu, ask user for the new value, then apply with \`openacp config set <path> <value>\`
2840
-
2841
- ### Voice / Speech-to-Text
2842
- - OpenACP can transcribe voice messages to text using STT providers (Groq Whisper, OpenAI Whisper)
2843
- - Current STT provider: ${config.speech?.stt?.provider ?? "Not configured"}
2844
- - To enable: user needs an API key from the STT provider
2845
- - Groq (recommended, free tier ~8h/day): Get key at console.groq.com \u2192 API Keys
2846
- - Set via: \`openacp config set speech.stt.provider groq\` then \`openacp config set speech.stt.providers.groq.apiKey <key>\`
2847
- - When STT is configured, voice messages are automatically transcribed before sending to agents that don't support audio
2848
- - Agents with audio capability receive the audio directly (no transcription needed)
2849
- - User can also configure via /settings \u2192 STT Provider
2850
-
2851
- ### Restart / Update
2852
- - Always ask for confirmation \u2014 these are disruptive actions
2853
- - Guide user: "Tap \u{1F504} Restart button or type /restart"
2854
-
2855
- ### Toggle Dangerous Mode
2856
- - Run \`openacp api dangerous <id> on|off\`
2857
- - Explain: dangerous mode auto-approves all permission requests \u2014 the agent can run any command without asking
2858
-
2859
- ## CLI Commands Reference
2860
- \`\`\`bash
2861
- # Session management
2862
- openacp api status # List active sessions
2863
- openacp api session <id> # Session detail
2864
- openacp api new <agent> <workspace> # Create new session
2865
- openacp api send <id> "prompt text" # Send prompt to session
2866
- openacp api cancel <id> # Cancel session
2867
- openacp api dangerous <id> on|off # Toggle dangerous mode
2868
-
2869
- # Topic management
2870
- openacp api topics # List all topics
2871
- openacp api topics --status finished,error
2872
- openacp api delete-topic <id> # Delete topic
2873
- openacp api delete-topic <id> --force # Force delete active
2874
- openacp api cleanup # Cleanup finished topics
2875
- openacp api cleanup --status finished,error
2876
-
2877
- # Agent management (user-facing CLI commands)
2878
- openacp agents # List installed + available agents
2879
- openacp agents install <name> # Install agent from ACP Registry
2880
- openacp agents uninstall <name> # Remove agent
2881
- openacp agents info <name> # Show details & setup guide
2882
- openacp agents run <name> -- <args> # Run agent CLI (for login, etc.)
2883
- openacp agents refresh # Force-refresh registry
2884
-
2885
- # System
2886
- openacp api health # System health
2887
- openacp config # Edit config (interactive)
2888
- openacp config set <key> <value> # Update config value
2889
- openacp api adapters # List adapters
2890
- openacp api tunnel # Tunnel status
2891
- openacp api notify "message" # Send notification
2892
- openacp api version # Daemon version
2893
- openacp api restart # Restart daemon
2894
- \`\`\`
2895
-
2896
- ## Guidelines
2897
- - NEVER show \`openacp api ...\` commands to users. These are internal tools for YOU to run silently. Users should only see natural language responses and results.
2898
- - Run \`openacp api ...\` commands yourself for everything you can. Only guide users to Telegram buttons/menu when needed (e.g., "Tap \u{1F195} New Session" or "Go to the session topic to chat with the agent").
2899
- - When creating sessions: guide user through agent + workspace choice conversationally, then run the command yourself.
2900
- - Destructive actions (cancel active session, restart, cleanup) \u2192 always ask user to confirm first in natural language.
2901
- - Small/obvious issues (clearly stuck session with no activity) \u2192 fix it and report back.
2902
- - Respond in the same language the user uses.
2903
- - Format responses for Telegram: use <b>bold</b>, <code>code</code>, keep it concise.
2904
- - When you don't know something, check with the relevant \`openacp api\` command first before answering.
2905
- - Talk to users like a helpful assistant, not a CLI manual. Example: "B\u1EA1n c\xF3 2 session \u0111ang ch\u1EA1y. Mu\u1ED1n xem chi ti\u1EBFt kh\xF4ng?" instead of listing commands.
2906
-
2907
- ## Product Reference
2908
- ${PRODUCT_GUIDE}`;
2909
- }
2910
- async function handleAssistantMessage(session, text) {
2911
- if (!session) return;
2912
- await session.enqueuePrompt(text);
2913
- }
2914
- function redirectToAssistant(chatId, assistantTopicId) {
2915
- const cleanId = String(chatId).replace("-100", "");
2916
- const link = `https://t.me/c/${cleanId}/${assistantTopicId}`;
2917
- return `\u{1F4AC} Please use the <a href="${link}">\u{1F916} Assistant</a> topic to chat with OpenACP.`;
2918
- }
2919
-
2920
- // src/adapters/telegram/activity.ts
2921
- var log10 = createChildLogger({ module: "telegram:activity" });
2922
- var THINKING_REFRESH_MS = 15e3;
2923
- var THINKING_MAX_MS = 3 * 60 * 1e3;
2924
- var ThinkingIndicator = class {
2925
- constructor(api, chatId, threadId, sendQueue) {
2926
- this.api = api;
2927
- this.chatId = chatId;
2928
- this.threadId = threadId;
2929
- this.sendQueue = sendQueue;
2930
- }
2931
- msgId;
2932
- sending = false;
2933
- dismissed = false;
2934
- refreshTimer;
2935
- showTime = 0;
2936
- async show() {
2937
- if (this.msgId || this.sending || this.dismissed) return;
2938
- this.sending = true;
2939
- this.showTime = Date.now();
2940
- try {
2941
- const result = await this.sendQueue.enqueue(
2942
- () => this.api.sendMessage(this.chatId, "\u{1F4AD} <i>Thinking...</i>", {
2943
- message_thread_id: this.threadId,
2944
- parse_mode: "HTML",
2945
- disable_notification: true
2946
- })
2947
- );
2948
- if (result && !this.dismissed) {
2949
- this.msgId = result.message_id;
2950
- this.startRefreshTimer();
2951
- }
2952
- } catch (err) {
2953
- log10.warn({ err }, "ThinkingIndicator.show() failed");
2954
- } finally {
2955
- this.sending = false;
2956
- }
2957
- }
2958
- /** Clear state — stops refresh timer, no Telegram API call */
2959
- dismiss() {
2960
- this.dismissed = true;
2961
- this.msgId = void 0;
2962
- this.stopRefreshTimer();
2963
- }
2964
- /** Reset for a new prompt cycle */
2965
- reset() {
2966
- this.dismissed = false;
2967
- }
2968
- startRefreshTimer() {
2969
- this.stopRefreshTimer();
2970
- this.refreshTimer = setInterval(() => {
2971
- if (this.dismissed || !this.msgId || Date.now() - this.showTime >= THINKING_MAX_MS) {
2972
- this.stopRefreshTimer();
2973
- return;
2974
- }
2975
- const elapsed = Math.round((Date.now() - this.showTime) / 1e3);
2976
- this.sendQueue.enqueue(() => {
2977
- if (this.dismissed) return Promise.resolve(void 0);
2978
- return this.api.sendMessage(this.chatId, `\u{1F4AD} <i>Still thinking... (${elapsed}s)</i>`, {
2979
- message_thread_id: this.threadId,
2980
- parse_mode: "HTML",
2981
- disable_notification: true
2982
- });
2983
- }).then((result) => {
2984
- if (result && !this.dismissed) {
2985
- this.msgId = result.message_id;
2986
- }
2987
- }).catch(() => {
2988
- });
2989
- }, THINKING_REFRESH_MS);
2990
- }
2991
- stopRefreshTimer() {
2992
- if (this.refreshTimer) {
2993
- clearInterval(this.refreshTimer);
2994
- this.refreshTimer = void 0;
2995
- }
2996
- }
2997
- };
2998
- var UsageMessage = class {
2999
- constructor(api, chatId, threadId, sendQueue) {
3000
- this.api = api;
3001
- this.chatId = chatId;
3002
- this.threadId = threadId;
3003
- this.sendQueue = sendQueue;
3004
- }
3005
- msgId;
3006
- async send(usage) {
3007
- const text = formatUsage(usage);
3008
- try {
3009
- if (this.msgId) {
3010
- await this.sendQueue.enqueue(
3011
- () => this.api.editMessageText(this.chatId, this.msgId, text, {
3012
- parse_mode: "HTML"
3013
- })
3014
- );
3015
- } else {
3016
- const result = await this.sendQueue.enqueue(
3017
- () => this.api.sendMessage(this.chatId, text, {
3018
- message_thread_id: this.threadId,
3019
- parse_mode: "HTML",
3020
- disable_notification: true
3021
- })
3022
- );
3023
- if (result) this.msgId = result.message_id;
3024
- }
3025
- } catch (err) {
3026
- log10.warn({ err }, "UsageMessage.send() failed");
3027
- }
3028
- }
3029
- getMsgId() {
3030
- return this.msgId;
3031
- }
3032
- async delete() {
3033
- if (!this.msgId) return;
3034
- const id = this.msgId;
3035
- this.msgId = void 0;
3036
- try {
3037
- await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
3038
- } catch (err) {
3039
- log10.warn({ err }, "UsageMessage.delete() failed");
3040
- }
3041
- }
3042
- };
3043
- function formatPlanCard(entries) {
3044
- const statusIcon = {
3045
- completed: "\u2705",
3046
- in_progress: "\u{1F504}",
3047
- pending: "\u2B1C",
3048
- failed: "\u274C"
3049
- };
3050
- const total = entries.length;
3051
- const done = entries.filter((e) => e.status === "completed").length;
3052
- const ratio = total > 0 ? done / total : 0;
3053
- const filled = Math.round(ratio * 10);
3054
- const bar = "\u2593".repeat(filled) + "\u2591".repeat(10 - filled);
3055
- const pct = Math.round(ratio * 100);
3056
- const header = `\u{1F4CB} <b>Plan</b>
3057
- ${bar} ${pct}% \xB7 ${done}/${total}`;
3058
- const lines = entries.map((e, i) => {
3059
- const icon = statusIcon[e.status] ?? "\u2B1C";
3060
- return `${icon} ${i + 1}. ${e.content}`;
3061
- });
3062
- return [header, ...lines].join("\n");
3063
- }
3064
- var PlanCard = class {
3065
- constructor(api, chatId, threadId, sendQueue) {
3066
- this.api = api;
3067
- this.chatId = chatId;
3068
- this.threadId = threadId;
3069
- this.sendQueue = sendQueue;
3070
- }
3071
- msgId;
3072
- flushPromise = Promise.resolve();
3073
- latestEntries;
3074
- lastSentText;
3075
- flushTimer;
3076
- update(entries) {
3077
- this.latestEntries = entries;
3078
- if (this.flushTimer) clearTimeout(this.flushTimer);
3079
- this.flushTimer = setTimeout(() => {
3080
- this.flushTimer = void 0;
3081
- this.flushPromise = this.flushPromise.then(() => this._flush()).catch(() => {
3082
- });
3083
- }, 3500);
3084
- }
3085
- async finalize() {
3086
- if (!this.latestEntries) return;
3087
- if (this.flushTimer) {
3088
- clearTimeout(this.flushTimer);
3089
- this.flushTimer = void 0;
3090
- }
3091
- await this.flushPromise;
3092
- this.flushPromise = this.flushPromise.then(() => this._flush()).catch(() => {
3093
- });
3094
- await this.flushPromise;
3095
- }
3096
- destroy() {
3097
- if (this.flushTimer) {
3098
- clearTimeout(this.flushTimer);
3099
- this.flushTimer = void 0;
3100
- }
3101
- }
3102
- async _flush() {
3103
- if (!this.latestEntries) return;
3104
- const text = formatPlanCard(this.latestEntries);
3105
- if (this.msgId && text === this.lastSentText) return;
3106
- this.lastSentText = text;
3107
- try {
3108
- if (this.msgId) {
3109
- await this.sendQueue.enqueue(
3110
- () => this.api.editMessageText(this.chatId, this.msgId, text, {
3111
- parse_mode: "HTML"
3112
- })
3113
- );
3114
- } else {
3115
- const result = await this.sendQueue.enqueue(
3116
- () => this.api.sendMessage(this.chatId, text, {
3117
- message_thread_id: this.threadId,
3118
- parse_mode: "HTML",
3119
- disable_notification: true
3120
- })
3121
- );
3122
- if (result) this.msgId = result.message_id;
3123
- }
3124
- } catch (err) {
3125
- log10.warn({ err }, "PlanCard flush failed");
3126
- }
3127
- }
3128
- };
3129
- var ActivityTracker = class {
3130
- constructor(api, chatId, threadId, sendQueue) {
3131
- this.api = api;
3132
- this.chatId = chatId;
3133
- this.threadId = threadId;
3134
- this.sendQueue = sendQueue;
3135
- this.thinking = new ThinkingIndicator(api, chatId, threadId, sendQueue);
3136
- this.planCard = new PlanCard(api, chatId, threadId, sendQueue);
3137
- this.usage = new UsageMessage(api, chatId, threadId, sendQueue);
3138
- }
3139
- isFirstEvent = true;
3140
- hasPlanCard = false;
3141
- thinking;
3142
- planCard;
3143
- usage;
3144
- async onNewPrompt() {
3145
- this.isFirstEvent = true;
3146
- this.hasPlanCard = false;
3147
- this.thinking.dismiss();
3148
- this.thinking.reset();
3149
- }
3150
- async onThought() {
3151
- await this._firstEventGuard();
3152
- await this.thinking.show();
3153
- }
3154
- async onPlan(entries) {
3155
- await this._firstEventGuard();
3156
- this.thinking.dismiss();
3157
- this.hasPlanCard = true;
3158
- this.planCard.update(entries);
3159
- }
3160
- async onToolCall() {
3161
- await this._firstEventGuard();
3162
- this.thinking.dismiss();
3163
- this.thinking.reset();
3164
- }
3165
- async onTextStart() {
3166
- await this._firstEventGuard();
3167
- this.thinking.dismiss();
3168
- }
3169
- async sendUsage(data) {
3170
- await this.usage.send(data);
3171
- }
3172
- getUsageMsgId() {
3173
- return this.usage.getMsgId();
3174
- }
3175
- async onComplete() {
3176
- if (this.hasPlanCard) {
3177
- await this.planCard.finalize();
3178
- } else {
3179
- try {
3180
- await this.sendQueue.enqueue(
3181
- () => this.api.sendMessage(this.chatId, "\u2705 <b>Done</b>", {
3182
- message_thread_id: this.threadId,
3183
- parse_mode: "HTML",
3184
- disable_notification: true
3185
- })
3186
- );
3187
- } catch (err) {
3188
- log10.warn({ err }, "ActivityTracker.onComplete() Done send failed");
3189
- }
3190
- }
3191
- }
3192
- destroy() {
3193
- this.thinking.dismiss();
3194
- this.planCard.destroy();
3195
- }
3196
- async _firstEventGuard() {
3197
- if (!this.isFirstEvent) return;
3198
- this.isFirstEvent = false;
3199
- await this.usage.delete();
3200
- }
3201
- };
3202
-
3203
- // src/adapters/telegram/send-queue.ts
3204
- var TelegramSendQueue = class {
3205
- items = [];
3206
- processing = false;
3207
- lastExec = 0;
3208
- minInterval;
3209
- constructor(minInterval = 3e3) {
3210
- this.minInterval = minInterval;
3211
- }
3212
- enqueue(fn, opts) {
3213
- const type = opts?.type ?? "other";
3214
- const key = opts?.key;
3215
- return new Promise((resolve, reject) => {
3216
- if (type === "text" && key) {
3217
- const idx = this.items.findIndex(
3218
- (item) => item.type === "text" && item.key === key
3219
- );
3220
- if (idx !== -1) {
3221
- this.items[idx].resolve(void 0);
3222
- this.items[idx] = { fn, type, key, resolve, reject };
3223
- this.scheduleProcess();
3224
- return;
3225
- }
3226
- }
3227
- this.items.push({ fn, type, key, resolve, reject });
3228
- this.scheduleProcess();
3229
- });
3230
- }
3231
- onRateLimited() {
3232
- const remaining = [];
3233
- for (const item of this.items) {
3234
- if (item.type === "text") {
3235
- item.resolve(void 0);
3236
- } else {
3237
- remaining.push(item);
3238
- }
3239
- }
3240
- this.items = remaining;
3241
- }
3242
- scheduleProcess() {
3243
- if (this.processing) return;
3244
- if (this.items.length === 0) return;
3245
- const elapsed = Date.now() - this.lastExec;
3246
- const delay = Math.max(0, this.minInterval - elapsed);
3247
- this.processing = true;
3248
- setTimeout(() => void this.processNext(), delay);
3249
- }
3250
- async processNext() {
3251
- const item = this.items.shift();
3252
- if (!item) {
3253
- this.processing = false;
3254
- return;
3255
- }
3256
- try {
3257
- const result = await item.fn();
3258
- item.resolve(result);
3259
- } catch (err) {
3260
- item.reject(err);
3261
- } finally {
3262
- this.lastExec = Date.now();
3263
- this.processing = false;
3264
- this.scheduleProcess();
3265
- }
3266
- }
3267
- };
3268
-
3269
- // src/adapters/telegram/action-detect.ts
3270
- import { nanoid as nanoid2 } from "nanoid";
3271
- import { InlineKeyboard as InlineKeyboard11 } from "grammy";
3272
- var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
3273
- var CMD_CANCEL_RE = /\/cancel\b/;
3274
- var KW_NEW_RE = /(?:create|new)\s+session/i;
3275
- var KW_CANCEL_RE = /(?:cancel|stop)\s+session/i;
3276
- function detectAction(text) {
3277
- if (!text) return null;
3278
- const cancelCmd = CMD_CANCEL_RE.exec(text);
3279
- if (cancelCmd) return { action: "cancel_session" };
3280
- const newCmd = CMD_NEW_RE.exec(text);
3281
- if (newCmd) {
3282
- return {
3283
- action: "new_session",
3284
- agent: newCmd[1] || void 0,
3285
- workspace: newCmd[2] || void 0
3286
- };
3287
- }
3288
- if (KW_CANCEL_RE.test(text)) return { action: "cancel_session" };
3289
- if (KW_NEW_RE.test(text))
3290
- return { action: "new_session", agent: void 0, workspace: void 0 };
3291
- return null;
3292
- }
3293
- var ACTION_TTL_MS = 5 * 60 * 1e3;
3294
- var actionMap = /* @__PURE__ */ new Map();
3295
- function storeAction(action) {
3296
- const id = nanoid2(10);
3297
- actionMap.set(id, { action, createdAt: Date.now() });
3298
- for (const [key, entry] of actionMap) {
3299
- if (Date.now() - entry.createdAt > ACTION_TTL_MS) {
3300
- actionMap.delete(key);
3301
- }
3302
- }
3303
- return id;
3304
- }
3305
- function getAction(id) {
3306
- const entry = actionMap.get(id);
3307
- if (!entry) return void 0;
3308
- if (Date.now() - entry.createdAt > ACTION_TTL_MS) {
3309
- actionMap.delete(id);
3310
- return void 0;
3311
- }
3312
- return entry.action;
3313
- }
3314
- function removeAction(id) {
3315
- actionMap.delete(id);
3316
- }
3317
- function buildActionKeyboard(actionId, action) {
3318
- const keyboard = new InlineKeyboard11();
3319
- if (action.action === "new_session") {
3320
- keyboard.text("\u2705 Create session", `a:${actionId}`);
3321
- keyboard.text("\u274C Cancel", `a:dismiss:${actionId}`);
3322
- } else {
3323
- keyboard.text("\u26D4 Cancel session", `a:${actionId}`);
3324
- keyboard.text("\u274C No", `a:dismiss:${actionId}`);
3325
- }
3326
- return keyboard;
3327
- }
3328
- function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
3329
- bot.callbackQuery(/^a:dismiss:/, async (ctx) => {
3330
- const actionId = ctx.callbackQuery.data.replace("a:dismiss:", "");
3331
- removeAction(actionId);
3332
- try {
3333
- await ctx.editMessageReplyMarkup({
3334
- reply_markup: { inline_keyboard: [] }
3335
- });
3336
- } catch {
3337
- }
3338
- await ctx.answerCallbackQuery({ text: "Dismissed" });
3339
- });
3340
- bot.callbackQuery(/^a:(?!dismiss)/, async (ctx) => {
3341
- const actionId = ctx.callbackQuery.data.replace("a:", "");
3342
- const action = getAction(actionId);
3343
- if (!action) {
3344
- await ctx.answerCallbackQuery({ text: "Action expired" });
3345
- return;
3346
- }
3347
- removeAction(actionId);
3348
- try {
3349
- if (action.action === "new_session") {
3350
- if (action.agent && action.workspace) {
3351
- await ctx.answerCallbackQuery({ text: "\u23F3 Creating session..." });
3352
- const { threadId, firstMsgId } = await executeNewSession(
3353
- bot,
3354
- core,
3355
- chatId,
3356
- action.agent,
3357
- action.workspace
3358
- );
3359
- const cleanId = String(chatId).replace("-100", "");
3360
- const topicLink = firstMsgId ? `https://t.me/c/${cleanId}/${threadId}/${firstMsgId}` : `https://t.me/c/${cleanId}/${threadId}`;
3361
- const originalText = ctx.callbackQuery.message?.text ?? "";
3362
- try {
3363
- await ctx.editMessageText(
3364
- originalText + `
3365
-
3366
- \u2705 Session created \u2192 <a href="${topicLink}">Go to topic</a>`,
3367
- { parse_mode: "HTML" }
3368
- );
3369
- } catch {
3370
- await ctx.editMessageReplyMarkup({
3371
- reply_markup: { inline_keyboard: [] }
3372
- });
3373
- }
3374
- } else {
3375
- await ctx.answerCallbackQuery();
3376
- try {
3377
- await ctx.editMessageReplyMarkup({
3378
- reply_markup: { inline_keyboard: [] }
3379
- });
3380
- } catch {
3381
- }
3382
- await startInteractiveNewSession(ctx, core, chatId, action.agent);
3383
- }
3384
- } else if (action.action === "cancel_session") {
3385
- const assistantId = getAssistantSessionId();
3386
- const cancelled = await executeCancelSession(core, assistantId);
3387
- if (cancelled) {
3388
- await ctx.answerCallbackQuery({ text: "\u26D4 Session cancelled" });
3389
- const originalText = ctx.callbackQuery.message?.text ?? "";
3390
- try {
3391
- await ctx.editMessageText(
3392
- originalText + `
3393
-
3394
- \u26D4 Session "${cancelled.name ?? cancelled.id}" cancelled`,
3395
- { parse_mode: "HTML" }
3396
- );
3397
- } catch {
3398
- await ctx.editMessageReplyMarkup({
3399
- reply_markup: { inline_keyboard: [] }
3400
- });
3401
- }
3402
- } else {
3403
- await ctx.answerCallbackQuery({
3404
- text: "No active session"
3405
- });
3406
- try {
3407
- await ctx.editMessageReplyMarkup({
3408
- reply_markup: { inline_keyboard: [] }
3409
- });
3410
- } catch {
3411
- }
3412
- }
3413
- }
3414
- } catch {
3415
- await ctx.answerCallbackQuery({ text: "\u274C Error, try again later" });
3416
- try {
3417
- await ctx.editMessageReplyMarkup({
3418
- reply_markup: { inline_keyboard: [] }
3419
- });
3420
- } catch {
3421
- }
3422
- }
3423
- });
3424
- }
3425
-
3426
- // src/adapters/telegram/tool-call-tracker.ts
3427
- var log11 = createChildLogger({ module: "tool-call-tracker" });
3428
- var ToolCallTracker = class {
3429
- constructor(bot, chatId, sendQueue) {
3430
- this.bot = bot;
3431
- this.chatId = chatId;
3432
- this.sendQueue = sendQueue;
3433
- }
3434
- sessions = /* @__PURE__ */ new Map();
3435
- async trackNewCall(sessionId, threadId, meta, verbosity = "medium") {
3436
- if (!this.sessions.has(sessionId)) {
3437
- this.sessions.set(sessionId, /* @__PURE__ */ new Map());
3438
- }
3439
- let resolveReady;
3440
- const ready = new Promise((r) => {
3441
- resolveReady = r;
3442
- });
3443
- this.sessions.get(sessionId).set(meta.id, {
3444
- msgId: 0,
3445
- name: meta.name,
3446
- kind: meta.kind,
3447
- rawInput: meta.rawInput,
3448
- viewerLinks: meta.viewerLinks,
3449
- viewerFilePath: meta.viewerFilePath,
3450
- ready
3451
- });
3452
- try {
3453
- const msg = await this.sendQueue.enqueue(
3454
- () => this.bot.api.sendMessage(this.chatId, formatToolCall(meta, verbosity), {
3455
- message_thread_id: threadId,
3456
- parse_mode: "HTML",
3457
- disable_notification: true
3458
- })
3459
- );
3460
- const toolEntry = this.sessions.get(sessionId).get(meta.id);
3461
- toolEntry.msgId = msg.message_id;
3462
- } finally {
3463
- resolveReady();
3464
- }
3465
- }
3466
- async updateCall(sessionId, meta, verbosity = "medium") {
3467
- const toolState = this.sessions.get(sessionId)?.get(meta.id);
3468
- if (!toolState) return;
3469
- if (meta.viewerLinks) {
3470
- toolState.viewerLinks = meta.viewerLinks;
3471
- log11.debug(
3472
- { toolId: meta.id, viewerLinks: meta.viewerLinks },
3473
- "Accumulated viewerLinks"
3474
- );
3475
- }
3476
- if (meta.viewerFilePath) toolState.viewerFilePath = meta.viewerFilePath;
3477
- if (meta.name) toolState.name = meta.name;
3478
- if (meta.kind) toolState.kind = meta.kind;
3479
- const isTerminal = meta.status === "completed" || meta.status === "failed";
3480
- if (!isTerminal) return;
3481
- await toolState.ready;
3482
- log11.debug(
3483
- {
3484
- toolId: meta.id,
3485
- status: meta.status,
3486
- hasViewerLinks: !!toolState.viewerLinks,
3487
- viewerLinks: toolState.viewerLinks,
3488
- name: toolState.name,
3489
- msgId: toolState.msgId
3490
- },
3491
- "Tool completed, preparing edit"
3492
- );
3493
- const merged = {
3494
- ...meta,
3495
- name: toolState.name,
3496
- kind: toolState.kind,
3497
- rawInput: toolState.rawInput,
3498
- viewerLinks: toolState.viewerLinks,
3499
- viewerFilePath: toolState.viewerFilePath
3500
- };
3501
- const formattedText = formatToolUpdate(merged, verbosity);
3502
- try {
3503
- await this.sendQueue.enqueue(
3504
- () => this.bot.api.editMessageText(
3505
- this.chatId,
3506
- toolState.msgId,
3507
- formattedText,
3508
- { parse_mode: "HTML" }
3509
- )
3510
- );
3511
- } catch (err) {
3512
- log11.warn(
3513
- {
3514
- err,
3515
- msgId: toolState.msgId,
3516
- textLen: formattedText.length,
3517
- hasViewerLinks: !!merged.viewerLinks
3518
- },
3519
- "Tool update edit failed"
3520
- );
3521
- }
3522
- }
3523
- cleanup(sessionId) {
3524
- this.sessions.delete(sessionId);
3525
- }
3526
- };
3527
-
3528
- // src/adapters/telegram/streaming.ts
3529
- var FLUSH_INTERVAL = 5e3;
3530
- var MessageDraft = class {
3531
- constructor(bot, chatId, threadId, sendQueue, sessionId) {
3532
- this.bot = bot;
3533
- this.chatId = chatId;
3534
- this.threadId = threadId;
3535
- this.sendQueue = sendQueue;
3536
- this.sessionId = sessionId;
3537
- }
3538
- buffer = "";
3539
- messageId;
3540
- firstFlushPending = false;
3541
- flushTimer;
3542
- flushPromise = Promise.resolve();
3543
- lastSentBuffer = "";
3544
- displayTruncated = false;
3545
- append(text) {
3546
- if (!text) return;
3547
- this.buffer += text;
3548
- this.scheduleFlush();
3549
- }
3550
- scheduleFlush() {
3551
- if (this.flushTimer) return;
3552
- this.flushTimer = setTimeout(() => {
3553
- this.flushTimer = void 0;
3554
- this.flushPromise = this.flushPromise.then(() => this.flush()).catch(() => {
3555
- });
3556
- }, FLUSH_INTERVAL);
3557
- }
3558
- async flush() {
3559
- if (!this.buffer) return;
3560
- if (this.firstFlushPending) return;
3561
- const snapshot = this.buffer;
3562
- let html = markdownToTelegramHtml(snapshot);
3563
- if (!html) return;
3564
- let truncated = false;
3565
- if (html.length > 4096) {
3566
- const ratio = 4e3 / html.length;
3567
- const targetLen = Math.floor(snapshot.length * ratio);
3568
- let cutAt = snapshot.lastIndexOf("\n", targetLen);
3569
- if (cutAt < targetLen * 0.5) cutAt = targetLen;
3570
- html = markdownToTelegramHtml(snapshot.slice(0, cutAt) + "\n\u2026");
3571
- truncated = true;
3572
- if (html.length > 4096) {
3573
- html = html.slice(0, 4090) + "\n\u2026";
3574
- }
3575
- }
3576
- if (!this.messageId) {
3577
- this.firstFlushPending = true;
3578
- try {
3579
- const result = await this.sendQueue.enqueue(
3580
- () => this.bot.api.sendMessage(this.chatId, html, {
3581
- message_thread_id: this.threadId,
3582
- parse_mode: "HTML",
3583
- disable_notification: true
3584
- }),
3585
- { type: "other" }
3586
- );
3587
- if (result) {
3588
- this.messageId = result.message_id;
3589
- if (!truncated) {
3590
- this.lastSentBuffer = snapshot;
3591
- this.displayTruncated = false;
3592
- } else {
3593
- this.displayTruncated = true;
3594
- }
3595
- }
3596
- } catch {
3597
- } finally {
3598
- this.firstFlushPending = false;
3599
- }
3600
- } else {
3601
- try {
3602
- const result = await this.sendQueue.enqueue(
3603
- () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
3604
- parse_mode: "HTML"
3605
- }),
3606
- { type: "text", key: this.sessionId }
3607
- );
3608
- if (result !== void 0) {
3609
- if (!truncated) {
3610
- this.lastSentBuffer = snapshot;
3611
- this.displayTruncated = false;
3612
- } else {
3613
- this.displayTruncated = true;
3614
- }
3615
- }
3616
- } catch {
3617
- }
3618
- }
3619
- }
3620
- async finalize() {
3621
- if (this.flushTimer) {
3622
- clearTimeout(this.flushTimer);
3623
- this.flushTimer = void 0;
3624
- }
3625
- await this.flushPromise;
3626
- if (!this.buffer) return this.messageId;
3627
- if (this.messageId && this.buffer === this.lastSentBuffer && !this.displayTruncated) {
3628
- return this.messageId;
3629
- }
3630
- const fullHtml = markdownToTelegramHtml(this.buffer);
3631
- if (fullHtml.length <= 4096) {
3632
- try {
3633
- if (this.messageId) {
3634
- await this.sendQueue.enqueue(
3635
- () => this.bot.api.editMessageText(this.chatId, this.messageId, fullHtml, {
3636
- parse_mode: "HTML"
3637
- }),
3638
- { type: "other" }
3639
- );
3640
- } else {
3641
- const msg = await this.sendQueue.enqueue(
3642
- () => this.bot.api.sendMessage(this.chatId, fullHtml, {
3643
- message_thread_id: this.threadId,
3644
- parse_mode: "HTML",
3645
- disable_notification: true
3646
- }),
3647
- { type: "other" }
3648
- );
3649
- if (msg) this.messageId = msg.message_id;
3650
- }
3651
- return this.messageId;
3652
- } catch {
3653
- }
3654
- }
3655
- const mdChunks = splitMessage2(this.buffer);
3656
- const chunkPromises = [];
3657
- for (let i = 0; i < mdChunks.length; i++) {
3658
- const html = markdownToTelegramHtml(mdChunks[i]);
3659
- const isEdit = i === 0 && !!this.messageId;
3660
- const chunkMd = mdChunks[i];
3661
- const fn = isEdit ? () => this.bot.api.editMessageText(this.chatId, this.messageId, html, { parse_mode: "HTML" }) : () => this.bot.api.sendMessage(this.chatId, html, {
3662
- message_thread_id: this.threadId,
3663
- parse_mode: "HTML",
3664
- disable_notification: true
3665
- });
3666
- const promise = this.sendQueue.enqueue(fn, { type: "other" }).then((result) => {
3667
- if (!isEdit && result && typeof result === "object" && "message_id" in result) {
3668
- this.messageId = result.message_id;
3669
- }
3670
- }).catch(() => {
3671
- const fallbackFn = isEdit ? () => this.bot.api.editMessageText(this.chatId, this.messageId, chunkMd.slice(0, 4096)) : () => this.bot.api.sendMessage(this.chatId, chunkMd.slice(0, 4096), {
3672
- message_thread_id: this.threadId,
3673
- disable_notification: true
3674
- });
3675
- return this.sendQueue.enqueue(fallbackFn, { type: "other" }).then((result) => {
3676
- if (!isEdit && result && typeof result === "object" && "message_id" in result) {
3677
- this.messageId = result.message_id;
3678
- }
3679
- }).catch(() => {
3680
- });
3681
- });
3682
- chunkPromises.push(promise);
3683
- }
3684
- await Promise.all(chunkPromises);
3685
- return this.messageId;
3686
- }
3687
- getMessageId() {
3688
- return this.messageId;
3689
- }
3690
- async stripPattern(pattern) {
3691
- if (!this.messageId || !this.buffer) return;
3692
- const stripped = this.buffer.replace(pattern, "").trim();
3693
- if (stripped === this.buffer.trim()) return;
3694
- this.buffer = stripped;
3695
- this.lastSentBuffer = stripped;
3696
- const html = markdownToTelegramHtml(stripped);
3697
- if (!html) return;
3698
- try {
3699
- await this.sendQueue.enqueue(
3700
- () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
3701
- parse_mode: "HTML"
3702
- }),
3703
- { type: "other" }
3704
- );
3705
- } catch {
3706
- }
3707
- }
3708
- };
3709
-
3710
- // src/adapters/telegram/draft-manager.ts
3711
- var DraftManager = class {
3712
- constructor(bot, chatId, sendQueue) {
3713
- this.bot = bot;
3714
- this.chatId = chatId;
3715
- this.sendQueue = sendQueue;
3716
- }
3717
- drafts = /* @__PURE__ */ new Map();
3718
- textBuffers = /* @__PURE__ */ new Map();
3719
- getOrCreate(sessionId, threadId) {
3720
- let draft = this.drafts.get(sessionId);
3721
- if (!draft) {
3722
- draft = new MessageDraft(
3723
- this.bot,
3724
- this.chatId,
3725
- threadId,
3726
- this.sendQueue,
3727
- sessionId
3728
- );
3729
- this.drafts.set(sessionId, draft);
3730
- }
3731
- return draft;
3732
- }
3733
- hasDraft(sessionId) {
3734
- return this.drafts.has(sessionId);
3735
- }
3736
- getDraft(sessionId) {
3737
- return this.drafts.get(sessionId);
3738
- }
3739
- appendText(sessionId, text) {
3740
- this.textBuffers.set(
3741
- sessionId,
3742
- (this.textBuffers.get(sessionId) ?? "") + text
3743
- );
3744
- }
3745
- /**
3746
- * Finalize the current draft and return the message ID.
3747
- * Optionally detects actions in assistant responses.
3748
- */
3749
- async finalize(sessionId, assistantSessionId) {
3750
- const draft = this.drafts.get(sessionId);
3751
- if (!draft) return;
3752
- this.drafts.delete(sessionId);
3753
- const finalMsgId = await draft.finalize();
3754
- if (assistantSessionId && sessionId === assistantSessionId) {
3755
- const fullText = this.textBuffers.get(sessionId);
3756
- this.textBuffers.delete(sessionId);
3757
- if (fullText && finalMsgId) {
3758
- const detected = detectAction(fullText);
3759
- if (detected) {
3760
- const actionId = storeAction(detected);
3761
- const keyboard = buildActionKeyboard(actionId, detected);
3762
- try {
3763
- await this.bot.api.editMessageReplyMarkup(
3764
- this.chatId,
3765
- finalMsgId,
3766
- { reply_markup: keyboard }
3767
- );
3768
- } catch {
3769
- }
3770
- }
3771
- }
3772
- } else {
3773
- this.textBuffers.delete(sessionId);
3774
- }
3775
- }
3776
- cleanup(sessionId) {
3777
- this.drafts.delete(sessionId);
3778
- this.textBuffers.delete(sessionId);
3779
- }
3780
- };
3781
-
3782
- // src/adapters/telegram/skill-command-manager.ts
3783
- var log12 = createChildLogger({ module: "skill-commands" });
3784
- var SkillCommandManager = class {
3785
- // sessionId → pinned msgId
3786
- constructor(bot, chatId, sendQueue, sessionManager) {
3787
- this.bot = bot;
3788
- this.chatId = chatId;
3789
- this.sendQueue = sendQueue;
3790
- this.sessionManager = sessionManager;
3791
- }
3792
- messages = /* @__PURE__ */ new Map();
3793
- async send(sessionId, threadId, commands) {
3794
- if (!this.messages.has(sessionId)) {
3795
- const record = this.sessionManager.getSessionRecord(sessionId);
3796
- const platform = record?.platform;
3797
- if (platform?.skillMsgId) {
3798
- this.messages.set(sessionId, platform.skillMsgId);
3799
- }
3800
- }
3801
- if (commands.length === 0) {
3802
- await this.cleanup(sessionId);
3803
- return;
3804
- }
3805
- const messages = buildSkillMessages(commands);
3806
- const existingMsgId = this.messages.get(sessionId);
3807
- if (existingMsgId) {
3808
- try {
3809
- await this.bot.api.editMessageText(
3810
- this.chatId,
3811
- existingMsgId,
3812
- messages[0],
3813
- { parse_mode: "HTML" }
3814
- );
3815
- return;
3816
- } catch (err) {
3817
- const msg = err instanceof Error ? err.message : "";
3818
- if (msg.includes("message is not modified")) return;
3819
- try {
3820
- await this.bot.api.deleteMessage(this.chatId, existingMsgId);
3821
- } catch {
3822
- }
3823
- this.messages.delete(sessionId);
3824
- }
3825
- }
3826
- try {
3827
- let firstMsgId;
3828
- for (const text of messages) {
3829
- const msg = await this.sendQueue.enqueue(
3830
- () => this.bot.api.sendMessage(this.chatId, text, {
3831
- message_thread_id: threadId,
3832
- parse_mode: "HTML",
3833
- disable_notification: true
3834
- })
3835
- );
3836
- if (!firstMsgId) firstMsgId = msg.message_id;
3837
- }
3838
- this.messages.set(sessionId, firstMsgId);
3839
- const record = this.sessionManager.getSessionRecord(sessionId);
3840
- if (record) {
3841
- await this.sessionManager.patchRecord(sessionId, {
3842
- platform: { ...record.platform, skillMsgId: firstMsgId }
3843
- });
3844
- }
3845
- await this.bot.api.pinChatMessage(this.chatId, firstMsgId, {
3846
- disable_notification: true
3847
- });
3848
- } catch (err) {
3849
- log12.error({ err, sessionId }, "Failed to send skill commands");
3850
- }
3851
- }
3852
- async cleanup(sessionId) {
3853
- const msgId = this.messages.get(sessionId);
3854
- if (!msgId) return;
3855
- try {
3856
- await this.bot.api.editMessageText(
3857
- this.chatId,
3858
- msgId,
3859
- "\u{1F6E0} <i>Session ended</i>",
3860
- { parse_mode: "HTML" }
3861
- );
3862
- await this.bot.api.unpinChatMessage(this.chatId, msgId);
3863
- } catch {
3864
- }
3865
- this.messages.delete(sessionId);
3866
- const record = this.sessionManager.getSessionRecord(sessionId);
3867
- if (record) {
3868
- const platform = record.platform;
3869
- if (platform && typeof platform === "object" && "topicId" in platform) {
3870
- const { skillMsgId: _removed, ...rest } = platform;
3871
- await this.sessionManager.patchRecord(sessionId, { platform: rest });
3872
- }
3873
- }
3874
- }
3875
- };
3876
-
3877
- // src/adapters/telegram/adapter.ts
3878
- var log13 = createChildLogger({ module: "telegram" });
3879
- function patchedFetch(input, init) {
3880
- if (init?.signal && !(init.signal instanceof AbortSignal)) {
3881
- const nativeController = new AbortController();
3882
- const polyfillSignal = init.signal;
3883
- if (polyfillSignal.aborted) {
3884
- nativeController.abort();
3885
- } else {
3886
- polyfillSignal.addEventListener("abort", () => nativeController.abort());
3887
- }
3888
- init = { ...init, signal: nativeController.signal };
3889
- }
3890
- return fetch(input, init);
3891
- }
3892
- var TelegramAdapter = class extends ChannelAdapter {
3893
- bot;
3894
- telegramConfig;
3895
- permissionHandler;
3896
- assistantSession = null;
3897
- assistantInitializing = false;
3898
- notificationTopicId;
3899
- assistantTopicId;
3900
- sendQueue = new TelegramSendQueue(3e3);
3901
- // Extracted managers
3902
- toolTracker;
3903
- draftManager;
3904
- skillManager;
3905
- fileService;
3906
- sessionTrackers = /* @__PURE__ */ new Map();
3907
- get verbosity() {
3908
- const live = this.core.configManager.get().channels?.telegram;
3909
- const v = live?.displayVerbosity ?? this.telegramConfig.displayVerbosity;
3910
- if (v === "low" || v === "high") return v;
3911
- return "medium";
3912
- }
3913
- getOrCreateTracker(sessionId, threadId) {
3914
- let tracker = this.sessionTrackers.get(sessionId);
3915
- if (!tracker) {
3916
- tracker = new ActivityTracker(
3917
- this.bot.api,
3918
- this.telegramConfig.chatId,
3919
- threadId,
3920
- this.sendQueue
3921
- );
3922
- this.sessionTrackers.set(sessionId, tracker);
3923
- }
3924
- return tracker;
3925
- }
3926
- constructor(core, config) {
3927
- super(core, config);
3928
- this.telegramConfig = config;
3929
- }
3930
- async start() {
3931
- this.bot = new Bot(this.telegramConfig.botToken, {
3932
- client: {
3933
- baseFetchConfig: { duplex: "half" },
3934
- fetch: patchedFetch
3935
- }
3936
- });
3937
- this.fileService = this.core.fileService;
3938
- this.toolTracker = new ToolCallTracker(
3939
- this.bot,
3940
- this.telegramConfig.chatId,
3941
- this.sendQueue
3942
- );
3943
- this.draftManager = new DraftManager(
3944
- this.bot,
3945
- this.telegramConfig.chatId,
3946
- this.sendQueue
3947
- );
3948
- this.skillManager = new SkillCommandManager(
3949
- this.bot,
3950
- this.telegramConfig.chatId,
3951
- this.sendQueue,
3952
- this.core.sessionManager
3953
- );
3954
- this.bot.catch((err) => {
3955
- const rootCause = err.error instanceof Error ? err.error : err;
3956
- log13.error({ err: rootCause }, "Telegram bot error");
3957
- });
3958
- this.bot.api.config.use(async (prev, method, payload, signal) => {
3959
- const maxRetries = 3;
3960
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
3961
- const result = await prev(method, payload, signal);
3962
- if (result.ok || result.error_code !== 429 || attempt === maxRetries) {
3963
- return result;
3964
- }
3965
- const retryAfter = (result.parameters?.retry_after ?? 5) + 1;
3966
- const rateLimitedMethods = [
3967
- "sendMessage",
3968
- "editMessageText",
3969
- "editMessageReplyMarkup"
3970
- ];
3971
- if (rateLimitedMethods.includes(method)) {
3972
- this.sendQueue.onRateLimited();
3973
- }
3974
- log13.warn(
3975
- { method, retryAfter, attempt: attempt + 1 },
3976
- "Rate limited by Telegram, retrying"
3977
- );
3978
- await new Promise((r) => setTimeout(r, retryAfter * 1e3));
3979
- }
3980
- return prev(method, payload, signal);
3981
- });
3982
- this.bot.api.config.use((prev, method, payload, signal) => {
3983
- if (method === "getUpdates") {
3984
- const p = payload;
3985
- p.allowed_updates = p.allowed_updates ?? [
3986
- "message",
3987
- "callback_query"
3988
- ];
3989
- }
3990
- return prev(method, payload, signal);
3991
- });
3992
- await this.bot.api.setMyCommands(STATIC_COMMANDS, {
3993
- scope: { type: "chat", chat_id: this.telegramConfig.chatId }
3994
- });
3995
- this.bot.use((ctx, next) => {
3996
- const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
3997
- if (chatId !== this.telegramConfig.chatId) return;
3998
- return next();
3999
- });
4000
- const topics = await ensureTopics(
4001
- this.bot,
4002
- this.telegramConfig.chatId,
4003
- this.telegramConfig,
4004
- async (updates) => {
4005
- await this.core.configManager.save({
4006
- channels: { telegram: updates }
4007
- });
4008
- }
4009
- );
4010
- this.notificationTopicId = topics.notificationTopicId;
4011
- this.assistantTopicId = topics.assistantTopicId;
4012
- this.permissionHandler = new PermissionHandler(
4013
- this.bot,
4014
- this.telegramConfig.chatId,
4015
- (sessionId) => this.core.sessionManager.getSession(sessionId),
4016
- (notification) => this.sendNotification(notification)
4017
- );
4018
- setupDangerousModeCallbacks(this.bot, this.core);
4019
- setupTTSCallbacks(this.bot, this.core);
4020
- setupVerbosityCallbacks(this.bot, this.core);
4021
- setupActionCallbacks(
4022
- this.bot,
4023
- this.core,
4024
- this.telegramConfig.chatId,
4025
- () => this.assistantSession?.id
4026
- );
4027
- setupIntegrateCallbacks(this.bot, this.core);
4028
- setupAllCallbacks(
4029
- this.bot,
4030
- this.core,
4031
- this.telegramConfig.chatId,
4032
- {
4033
- notificationTopicId: this.notificationTopicId,
4034
- assistantTopicId: this.assistantTopicId
4035
- },
4036
- () => {
4037
- if (!this.assistantSession) return void 0;
4038
- return {
4039
- topicId: this.assistantTopicId,
4040
- enqueuePrompt: (p) => this.assistantSession.enqueuePrompt(p)
4041
- };
4042
- }
4043
- );
4044
- setupCommands(
4045
- this.bot,
4046
- this.core,
4047
- this.telegramConfig.chatId,
4048
- {
4049
- topicId: this.assistantTopicId,
4050
- getSession: () => this.assistantSession,
4051
- respawn: async () => {
4052
- if (this.assistantSession) {
4053
- await this.assistantSession.destroy();
4054
- this.assistantSession = null;
4055
- }
4056
- const { session, ready } = await spawnAssistant(
4057
- this.core,
4058
- this,
4059
- this.assistantTopicId
4060
- );
4061
- this.assistantSession = session;
4062
- this.assistantInitializing = true;
4063
- ready.then(() => {
4064
- this.assistantInitializing = false;
4065
- });
4066
- }
4067
- }
4068
- );
4069
- this.permissionHandler.setupCallbackHandler();
4070
- this.bot.command("handoff", async (ctx) => {
4071
- const threadId = ctx.message?.message_thread_id;
4072
- if (!threadId) return;
4073
- if (threadId === this.notificationTopicId || threadId === this.assistantTopicId) {
4074
- await ctx.reply("This command only works in session topics.", {
4075
- message_thread_id: threadId
4076
- });
4077
- return;
4078
- }
4079
- const session = this.core.sessionManager.getSessionByThread(
4080
- "telegram",
4081
- String(threadId)
4082
- );
4083
- const record = session ? void 0 : this.core.sessionManager.getRecordByThread(
4084
- "telegram",
4085
- String(threadId)
4086
- );
4087
- const agentName = session?.agentName ?? record?.agentName;
4088
- const agentSessionId = session?.agentSessionId ?? record?.agentSessionId;
4089
- if (!agentName || !agentSessionId) {
4090
- await ctx.reply("No session found for this topic.", {
4091
- message_thread_id: threadId
4092
- });
4093
- return;
4094
- }
4095
- const { getAgentCapabilities } = await import("./agent-registry-WT4NXPYG.js");
4096
- const caps = getAgentCapabilities(agentName);
4097
- if (!caps.supportsResume || !caps.resumeCommand) {
4098
- await ctx.reply("This agent does not support session transfer.", {
4099
- message_thread_id: threadId
4100
- });
4101
- return;
4102
- }
4103
- const command = caps.resumeCommand(agentSessionId);
4104
- await ctx.reply(
4105
- `Run this in your terminal to continue the session:
4106
-
4107
- <code>${command}</code>`,
4108
- {
4109
- message_thread_id: threadId,
4110
- parse_mode: "HTML"
4111
- }
4112
- );
4113
- });
4114
- this.setupRoutes();
4115
- this.bot.start({
4116
- allowed_updates: ["message", "callback_query"],
4117
- onStart: () => log13.info(
4118
- { chatId: this.telegramConfig.chatId },
4119
- "Telegram bot started"
4120
- )
4121
- });
4122
- try {
4123
- const config = this.core.configManager.get();
4124
- const agents = this.core.agentManager.getAvailableAgents();
4125
- const allRecords = this.core.sessionManager.listRecords();
4126
- const welcomeText = buildWelcomeMessage({
4127
- activeCount: allRecords.filter(
4128
- (r) => r.status === "active" || r.status === "initializing"
4129
- ).length,
4130
- errorCount: allRecords.filter((r) => r.status === "error").length,
4131
- totalCount: allRecords.length,
4132
- agents: agents.map((a) => a.name),
4133
- defaultAgent: config.defaultAgent
4134
- });
4135
- await this.bot.api.sendMessage(this.telegramConfig.chatId, welcomeText, {
4136
- message_thread_id: this.assistantTopicId,
4137
- parse_mode: "HTML",
4138
- reply_markup: buildMenuKeyboard()
4139
- });
4140
- } catch (err) {
4141
- log13.warn({ err }, "Failed to send welcome message");
4142
- }
4143
- try {
4144
- log13.info("Spawning assistant session...");
4145
- const { session, ready } = await spawnAssistant(
4146
- this.core,
4147
- this,
4148
- this.assistantTopicId
4149
- );
4150
- this.assistantSession = session;
4151
- this.assistantInitializing = true;
4152
- log13.info(
4153
- { sessionId: session.id },
4154
- "Assistant session ready, system prompt running in background"
4155
- );
4156
- ready.then(() => {
4157
- this.assistantInitializing = false;
4158
- log13.info(
4159
- { sessionId: session.id },
4160
- "Assistant ready for user messages"
4161
- );
4162
- });
4163
- } catch (err) {
4164
- log13.error({ err }, "Failed to spawn assistant");
4165
- this.bot.api.sendMessage(
4166
- this.telegramConfig.chatId,
4167
- `\u26A0\uFE0F <b>Failed to start assistant session.</b>
4168
-
4169
- <code>${err instanceof Error ? err.message : String(err)}</code>`,
4170
- { message_thread_id: this.assistantTopicId, parse_mode: "HTML" }
4171
- ).catch(() => {
4172
- });
4173
- }
4174
- }
4175
- async stop() {
4176
- if (this.assistantSession) {
4177
- await this.assistantSession.destroy();
4178
- }
4179
- await this.bot.stop();
4180
- log13.info("Telegram bot stopped");
4181
- }
4182
- setupRoutes() {
4183
- this.bot.on("message:text", async (ctx) => {
4184
- const threadId = ctx.message.message_thread_id;
4185
- const text = ctx.message.text;
4186
- if (await handlePendingWorkspaceInput(
4187
- ctx,
4188
- this.core,
4189
- this.telegramConfig.chatId,
4190
- this.assistantTopicId
4191
- )) {
4192
- return;
4193
- }
4194
- if (await handlePendingResumeInput(
4195
- ctx,
4196
- this.core,
4197
- this.telegramConfig.chatId,
4198
- this.assistantTopicId
4199
- )) {
4200
- return;
4201
- }
4202
- if (!threadId) {
4203
- const html = redirectToAssistant(
4204
- this.telegramConfig.chatId,
4205
- this.assistantTopicId
4206
- );
4207
- await ctx.reply(html, { parse_mode: "HTML" });
4208
- return;
4209
- }
4210
- if (threadId === this.notificationTopicId) return;
4211
- const forwardText = text.startsWith("/") ? text.slice(1) : text;
4212
- if (threadId === this.assistantTopicId) {
4213
- if (!this.assistantSession) {
4214
- await ctx.reply(
4215
- "\u26A0\uFE0F Assistant is not available yet. Please try again shortly.",
4216
- { parse_mode: "HTML" }
4217
- );
4218
- return;
4219
- }
4220
- await this.draftManager.finalize(
4221
- this.assistantSession.id,
4222
- this.assistantSession.id
4223
- );
4224
- ctx.replyWithChatAction("typing").catch(() => {
4225
- });
4226
- handleAssistantMessage(this.assistantSession, forwardText).catch(
4227
- (err) => log13.error({ err }, "Assistant error")
4228
- );
4229
- return;
4230
- }
4231
- const sessionId = this.core.sessionManager.getSessionByThread(
4232
- "telegram",
4233
- String(threadId)
4234
- )?.id;
4235
- if (sessionId)
4236
- await this.draftManager.finalize(sessionId, this.assistantSession?.id);
4237
- if (sessionId) {
4238
- const tracker = this.sessionTrackers.get(sessionId);
4239
- if (tracker) await tracker.onNewPrompt();
4240
- }
4241
- ctx.replyWithChatAction("typing").catch(() => {
4242
- });
4243
- this.core.handleMessage({
4244
- channelId: "telegram",
4245
- threadId: String(threadId),
4246
- userId: String(ctx.from.id),
4247
- text: forwardText
4248
- }).catch((err) => log13.error({ err }, "handleMessage error"));
4249
- });
4250
- this.bot.on("message:photo", async (ctx) => {
4251
- const threadId = ctx.message.message_thread_id;
4252
- if (!threadId || threadId === this.notificationTopicId) return;
4253
- const photos = ctx.message.photo;
4254
- const largest = photos[photos.length - 1];
4255
- const ext = ".jpg";
4256
- await this.handleIncomingMedia(
4257
- threadId,
4258
- ctx.from.id,
4259
- largest.file_id,
4260
- `photo${ext}`,
4261
- "image/jpeg",
4262
- ctx.message.caption || void 0
4263
- );
4264
- });
4265
- this.bot.on("message:document", async (ctx) => {
4266
- const threadId = ctx.message.message_thread_id;
4267
- if (!threadId || threadId === this.notificationTopicId) return;
4268
- const doc = ctx.message.document;
4269
- await this.handleIncomingMedia(
4270
- threadId,
4271
- ctx.from.id,
4272
- doc.file_id,
4273
- doc.file_name || "document",
4274
- doc.mime_type || "application/octet-stream",
4275
- ctx.message.caption || void 0
4276
- );
4277
- });
4278
- this.bot.on("message:voice", async (ctx) => {
4279
- const threadId = ctx.message.message_thread_id;
4280
- if (!threadId || threadId === this.notificationTopicId) return;
4281
- const voice = ctx.message.voice;
4282
- await this.handleIncomingMedia(
4283
- threadId,
4284
- ctx.from.id,
4285
- voice.file_id,
4286
- "voice.wav",
4287
- "audio/wav",
4288
- void 0,
4289
- true
4290
- );
4291
- });
4292
- this.bot.on("message:audio", async (ctx) => {
4293
- const threadId = ctx.message.message_thread_id;
4294
- if (!threadId || threadId === this.notificationTopicId) return;
4295
- const audio = ctx.message.audio;
4296
- await this.handleIncomingMedia(
4297
- threadId,
4298
- ctx.from.id,
4299
- audio.file_id,
4300
- audio.file_name || "audio.mp3",
4301
- audio.mime_type || "audio/mpeg",
4302
- ctx.message.caption || void 0
4303
- );
4304
- });
4305
- this.bot.on("message:video_note", async (ctx) => {
4306
- const threadId = ctx.message.message_thread_id;
4307
- if (!threadId || threadId === this.notificationTopicId) return;
4308
- const videoNote = ctx.message.video_note;
4309
- await this.handleIncomingMedia(
4310
- threadId,
4311
- ctx.from.id,
4312
- videoNote.file_id,
4313
- "video_note.mp4",
4314
- "video/mp4"
4315
- );
4316
- });
4317
- }
4318
- // --- MessageHandlers for dispatchMessage ---
4319
- messageHandlers = {
4320
- onThought: async (ctx, _content) => {
4321
- const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
4322
- await tracker.onThought();
4323
- },
4324
- onText: async (ctx, content) => {
4325
- if (!this.draftManager.hasDraft(ctx.sessionId)) {
4326
- const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
4327
- tracker.onTextStart().catch(() => {
4328
- });
4329
- }
4330
- const draft = this.draftManager.getOrCreate(ctx.sessionId, ctx.threadId);
4331
- draft.append(content.text);
4332
- this.draftManager.appendText(ctx.sessionId, content.text);
4333
- },
4334
- onToolCall: async (ctx, content) => {
4335
- const meta = content.metadata ?? {};
4336
- const toolName = meta.name ?? content.text ?? "Tool";
4337
- const toolKind = String(meta.kind ?? "other");
4338
- const noiseAction = evaluateNoise(toolName, toolKind, meta.rawInput);
4339
- if (noiseAction === "hide" && this.verbosity !== "high") return;
4340
- if (noiseAction === "collapse" && this.verbosity === "low") return;
4341
- const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
4342
- await tracker.onToolCall();
4343
- await this.draftManager.finalize(
4344
- ctx.sessionId,
4345
- this.assistantSession?.id
4346
- );
4347
- await this.toolTracker.trackNewCall(
4348
- ctx.sessionId,
4349
- ctx.threadId,
4350
- {
4351
- id: meta.id ?? "",
4352
- name: meta.name ?? content.text ?? "Tool",
4353
- kind: meta.kind,
4354
- status: meta.status,
4355
- content: meta.content,
4356
- rawInput: meta.rawInput,
4357
- viewerLinks: meta.viewerLinks,
4358
- viewerFilePath: meta.viewerFilePath
4359
- },
4360
- this.verbosity
4361
- );
4362
- },
4363
- onToolUpdate: async (ctx, content) => {
4364
- const meta = content.metadata ?? {};
4365
- await this.toolTracker.updateCall(
4366
- ctx.sessionId,
4367
- {
4368
- id: meta.id ?? "",
4369
- name: meta.name ?? content.text ?? "",
4370
- kind: meta.kind,
4371
- status: meta.status ?? "completed",
4372
- content: meta.content,
4373
- rawInput: meta.rawInput,
4374
- viewerLinks: meta.viewerLinks,
4375
- viewerFilePath: meta.viewerFilePath
4376
- },
4377
- this.verbosity
4378
- );
4379
- },
4380
- onPlan: async (ctx, content) => {
4381
- const meta = content.metadata ?? {};
4382
- const entries = meta.entries ?? [];
4383
- const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
4384
- await tracker.onPlan(
4385
- entries.map((e) => ({
4386
- content: e.content,
4387
- status: e.status,
4388
- priority: e.priority ?? "medium"
4389
- }))
4390
- );
4391
- },
4392
- onUsage: async (ctx, content) => {
4393
- const meta = content.metadata;
4394
- await this.draftManager.finalize(
4395
- ctx.sessionId,
4396
- this.assistantSession?.id
4397
- );
4398
- const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
4399
- await tracker.sendUsage(meta ?? {});
4400
- if (this.notificationTopicId && ctx.sessionId !== this.assistantSession?.id) {
4401
- const sess = this.core.sessionManager.getSession(ctx.sessionId);
4402
- const sessionName = sess?.name || "Session";
4403
- const chatIdStr = String(this.telegramConfig.chatId);
4404
- const numericId = chatIdStr.startsWith("-100") ? chatIdStr.slice(4) : chatIdStr.replace("-", "");
4405
- const usageMsgId = tracker.getUsageMsgId();
4406
- const deepLink = usageMsgId ? `https://t.me/c/${numericId}/${ctx.threadId}/${usageMsgId}` : `https://t.me/c/${numericId}/${ctx.threadId}`;
4407
- const text = `\u2705 <b>${escapeHtml(sessionName)}</b>
4408
- Task completed.
4409
-
4410
- <a href="${deepLink}">\u2192 Go to topic</a>`;
4411
- this.sendQueue.enqueue(
4412
- () => this.bot.api.sendMessage(this.telegramConfig.chatId, text, {
4413
- message_thread_id: this.notificationTopicId,
4414
- parse_mode: "HTML",
4415
- disable_notification: false
4416
- })
4417
- ).catch(() => {
4418
- });
4419
- }
4420
- },
4421
- onAttachment: async (ctx, content) => {
4422
- if (!content.attachment) return;
4423
- const { attachment } = content;
4424
- if (attachment.size > 50 * 1024 * 1024) {
4425
- log13.warn(
4426
- {
4427
- sessionId: ctx.sessionId,
4428
- fileName: attachment.fileName,
4429
- size: attachment.size
4430
- },
4431
- "File too large for Telegram (>50MB)"
4432
- );
4433
- await this.sendQueue.enqueue(
4434
- () => this.bot.api.sendMessage(
4435
- this.telegramConfig.chatId,
4436
- `\u26A0\uFE0F File too large to send (${Math.round(attachment.size / 1024 / 1024)}MB): ${escapeHtml(attachment.fileName)}`,
4437
- { message_thread_id: ctx.threadId, parse_mode: "HTML" }
4438
- )
4439
- );
4440
- return;
4441
- }
4442
- try {
4443
- const inputFile = new InputFile(attachment.filePath);
4444
- if (attachment.type === "image") {
4445
- await this.sendQueue.enqueue(
4446
- () => this.bot.api.sendPhoto(this.telegramConfig.chatId, inputFile, {
4447
- message_thread_id: ctx.threadId
4448
- })
4449
- );
4450
- } else if (attachment.type === "audio") {
4451
- await this.sendQueue.enqueue(
4452
- () => this.bot.api.sendVoice(this.telegramConfig.chatId, inputFile, {
4453
- message_thread_id: ctx.threadId
4454
- })
4455
- );
4456
- const draft = this.draftManager.getDraft(ctx.sessionId);
4457
- if (draft) {
4458
- draft.stripPattern(/\[TTS\][\s\S]*?\[\/TTS\]/g).catch(() => {
4459
- });
4460
- }
4461
- } else {
4462
- await this.sendQueue.enqueue(
4463
- () => this.bot.api.sendDocument(this.telegramConfig.chatId, inputFile, {
4464
- message_thread_id: ctx.threadId
4465
- })
4466
- );
4467
- }
4468
- } catch (err) {
4469
- log13.error(
4470
- { err, sessionId: ctx.sessionId, fileName: attachment.fileName },
4471
- "Failed to send attachment"
4472
- );
4473
- }
4474
- },
4475
- onSessionEnd: async (ctx, _content) => {
4476
- await this.draftManager.finalize(
4477
- ctx.sessionId,
4478
- this.assistantSession?.id
4479
- );
4480
- this.draftManager.cleanup(ctx.sessionId);
4481
- this.toolTracker.cleanup(ctx.sessionId);
4482
- await this.skillManager.cleanup(ctx.sessionId);
4483
- const tracker = this.sessionTrackers.get(ctx.sessionId);
4484
- if (tracker) {
4485
- await tracker.onComplete();
4486
- tracker.destroy();
4487
- this.sessionTrackers.delete(ctx.sessionId);
4488
- } else {
4489
- await this.sendQueue.enqueue(
4490
- () => this.bot.api.sendMessage(
4491
- this.telegramConfig.chatId,
4492
- `\u2705 <b>Done</b>`,
4493
- {
4494
- message_thread_id: ctx.threadId,
4495
- parse_mode: "HTML",
4496
- disable_notification: true
4497
- }
4498
- )
4499
- );
4500
- }
4501
- },
4502
- onError: async (ctx, content) => {
4503
- await this.draftManager.finalize(
4504
- ctx.sessionId,
4505
- this.assistantSession?.id
4506
- );
4507
- const tracker = this.sessionTrackers.get(ctx.sessionId);
4508
- if (tracker) {
4509
- tracker.destroy();
4510
- this.sessionTrackers.delete(ctx.sessionId);
4511
- }
4512
- await this.sendQueue.enqueue(
4513
- () => this.bot.api.sendMessage(
4514
- this.telegramConfig.chatId,
4515
- `\u274C <b>Error:</b> ${escapeHtml(content.text)}`,
4516
- {
4517
- message_thread_id: ctx.threadId,
4518
- parse_mode: "HTML",
4519
- disable_notification: true
4520
- }
4521
- )
4522
- );
4523
- },
4524
- onSystemMessage: async (ctx, content) => {
4525
- await this.sendQueue.enqueue(
4526
- () => this.bot.api.sendMessage(
4527
- this.telegramConfig.chatId,
4528
- escapeHtml(content.text),
4529
- {
4530
- message_thread_id: ctx.threadId,
4531
- parse_mode: "HTML",
4532
- disable_notification: true
4533
- }
4534
- )
4535
- );
4536
- }
4537
- };
4538
- // --- ChannelAdapter implementations ---
4539
- async sendMessage(sessionId, content) {
4540
- if (this.assistantInitializing && sessionId === this.assistantSession?.id)
4541
- return;
4542
- const session = this.core.sessionManager.getSession(sessionId);
4543
- if (!session) return;
4544
- if (session.archiving) return;
4545
- const threadId = Number(session.threadId);
4546
- if (!threadId || isNaN(threadId)) {
4547
- log13.warn(
4548
- { sessionId, threadId: session.threadId },
4549
- "Session has no valid threadId, skipping message"
4550
- );
4551
- return;
4552
- }
4553
- const ctx = { sessionId, threadId };
4554
- await dispatchMessage(this.messageHandlers, ctx, content, this.verbosity);
4555
- }
4556
- async sendPermissionRequest(sessionId, request) {
4557
- log13.info({ sessionId, requestId: request.id }, "Permission request sent");
4558
- const session = this.core.sessionManager.getSession(sessionId);
4559
- if (!session) return;
4560
- await this.sendQueue.enqueue(
4561
- () => this.permissionHandler.sendPermissionRequest(session, request)
4562
- );
4563
- }
4564
- async sendNotification(notification) {
4565
- if (notification.sessionId === this.assistantSession?.id) return;
4566
- log13.info(
4567
- { sessionId: notification.sessionId, type: notification.type },
4568
- "Notification sent"
4569
- );
4570
- if (!this.notificationTopicId) return;
4571
- const emoji = {
4572
- completed: "\u2705",
4573
- error: "\u274C",
4574
- permission: "\u{1F510}",
4575
- input_required: "\u{1F4AC}"
4576
- };
4577
- let text = `${emoji[notification.type] || "\u2139\uFE0F"} <b>${escapeHtml(notification.sessionName || "New session")}</b>
4578
- `;
4579
- text += escapeHtml(notification.summary);
4580
- const deepLink = notification.deepLink ?? (() => {
4581
- const session = this.core.sessionManager.getSession(
4582
- notification.sessionId
4583
- );
4584
- const threadId = session?.threadId;
4585
- if (!threadId) return void 0;
4586
- const chatIdStr = String(this.telegramConfig.chatId);
4587
- const numericId = chatIdStr.startsWith("-100") ? chatIdStr.slice(4) : chatIdStr.replace("-", "");
4588
- return `https://t.me/c/${numericId}/${threadId}`;
4589
- })();
4590
- if (deepLink) {
4591
- text += `
4592
-
4593
- <a href="${deepLink}">\u2192 Go to topic</a>`;
4594
- }
4595
- const replyMarkup = notification.type === "completed" ? { inline_keyboard: [[{ text: "\u{1F4CB} Summary", callback_data: `sm:summary:${notification.sessionId}` }]] } : void 0;
4596
- await this.sendQueue.enqueue(
4597
- () => this.bot.api.sendMessage(this.telegramConfig.chatId, text, {
4598
- message_thread_id: this.notificationTopicId,
4599
- parse_mode: "HTML",
4600
- disable_notification: false,
4601
- reply_markup: replyMarkup
4602
- })
4603
- );
4604
- }
4605
- async createSessionThread(sessionId, name) {
4606
- log13.info({ sessionId, name }, "Session topic created");
4607
- return String(
4608
- await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
4609
- );
4610
- }
4611
- async renameSessionThread(sessionId, newName) {
4612
- const session = this.core.sessionManager.getSession(sessionId);
4613
- if (!session) return;
4614
- await renameSessionTopic(
4615
- this.bot,
4616
- this.telegramConfig.chatId,
4617
- Number(session.threadId),
4618
- newName
4619
- );
4620
- await this.core.sessionManager.patchRecord(sessionId, { name: newName });
4621
- }
4622
- async deleteSessionThread(sessionId) {
4623
- const record = this.core.sessionManager.getSessionRecord(sessionId);
4624
- const platform = record?.platform;
4625
- const topicId = platform?.topicId;
4626
- if (!topicId) return;
4627
- try {
4628
- await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
4629
- } catch (err) {
4630
- log13.warn(
4631
- { err, sessionId, topicId },
4632
- "Failed to delete forum topic (may already be deleted)"
4633
- );
4634
- }
4635
- }
4636
- async sendSkillCommands(sessionId, commands) {
4637
- if (sessionId === this.assistantSession?.id) return;
4638
- const session = this.core.sessionManager.getSession(sessionId);
4639
- if (!session) return;
4640
- const threadId = Number(session.threadId);
4641
- if (!threadId) return;
4642
- await this.skillManager.send(sessionId, threadId, commands);
4643
- }
4644
- resolveSessionId(threadId) {
4645
- return this.core.sessionManager.getSessionByThread(
4646
- "telegram",
4647
- String(threadId)
4648
- )?.id;
4649
- }
4650
- async downloadTelegramFile(fileId) {
4651
- try {
4652
- const file = await this.bot.api.getFile(fileId);
4653
- if (!file.file_path) return null;
4654
- const url = `https://api.telegram.org/file/bot${this.telegramConfig.botToken}/${file.file_path}`;
4655
- const response = await fetch(url);
4656
- if (!response.ok) return null;
4657
- const buffer = Buffer.from(await response.arrayBuffer());
4658
- return { buffer, filePath: file.file_path };
4659
- } catch (err) {
4660
- log13.error({ err }, "Failed to download file from Telegram");
4661
- return null;
4662
- }
4663
- }
4664
- async handleIncomingMedia(threadId, userId, fileId, fileName, mimeType, caption, convertOggToWav) {
4665
- const downloaded = await this.downloadTelegramFile(fileId);
4666
- if (!downloaded) return;
4667
- let buffer = downloaded.buffer;
4668
- let originalFilePath;
4669
- const sessionId = this.resolveSessionId(threadId) || "unknown";
4670
- if (convertOggToWav) {
4671
- const oggAtt = await this.fileService.saveFile(
4672
- sessionId,
4673
- "voice.ogg",
4674
- downloaded.buffer,
4675
- "audio/ogg"
4676
- );
4677
- originalFilePath = oggAtt.filePath;
4678
- try {
4679
- buffer = await this.fileService.convertOggToWav(buffer);
4680
- } catch (err) {
4681
- log13.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
4682
- fileName = "voice.ogg";
4683
- mimeType = "audio/ogg";
4684
- originalFilePath = void 0;
4685
- }
4686
- }
4687
- const att = await this.fileService.saveFile(
4688
- sessionId,
4689
- fileName,
4690
- buffer,
4691
- mimeType
4692
- );
4693
- if (originalFilePath) {
4694
- att.originalFilePath = originalFilePath;
4695
- }
4696
- const rawText = caption || `[${att.type === "image" ? "Photo" : att.type === "audio" ? "Audio" : "File"}: ${att.fileName}]`;
4697
- const text = rawText.startsWith("/") ? rawText.slice(1) : rawText;
4698
- if (threadId === this.assistantTopicId) {
4699
- if (this.assistantSession) {
4700
- await this.assistantSession.enqueuePrompt(text, [att]);
4701
- }
4702
- return;
4703
- }
4704
- const sid = this.resolveSessionId(threadId);
4705
- if (sid) await this.draftManager.finalize(sid, this.assistantSession?.id);
4706
- this.core.handleMessage({
4707
- channelId: "telegram",
4708
- threadId: String(threadId),
4709
- userId: String(userId),
4710
- text,
4711
- attachments: [att]
4712
- }).catch((err) => log13.error({ err }, "handleMessage error"));
4713
- }
4714
- async cleanupSkillCommands(sessionId) {
4715
- await this.skillManager.cleanup(sessionId);
4716
- }
4717
- async archiveSessionTopic(sessionId) {
4718
- const core = this.core;
4719
- const session = core.sessionManager.getSession(sessionId);
4720
- if (!session) return;
4721
- const chatId = this.telegramConfig.chatId;
4722
- const oldTopicId = Number(session.threadId);
4723
- session.archiving = true;
4724
- await this.draftManager.finalize(session.id, this.assistantSession?.id);
4725
- this.draftManager.cleanup(session.id);
4726
- this.toolTracker.cleanup(session.id);
4727
- await this.skillManager.cleanup(session.id);
4728
- const tracker = this.sessionTrackers.get(session.id);
4729
- if (tracker) {
4730
- tracker.destroy();
4731
- this.sessionTrackers.delete(session.id);
4732
- }
4733
- await deleteSessionTopic(this.bot, chatId, oldTopicId);
4734
- }
4735
- };
4736
-
4737
- export {
4738
- TelegramAdapter
4739
- };
4740
- //# sourceMappingURL=chunk-H5P2C6H4.js.map