beecork 1.7.1 → 2.0.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 (253) hide show
  1. package/README.md +52 -111
  2. package/dist/index.js +1394 -581
  3. package/package.json +26 -54
  4. package/dist/capabilities/index.d.ts +0 -3
  5. package/dist/capabilities/index.js +0 -2
  6. package/dist/capabilities/manager.d.ts +0 -13
  7. package/dist/capabilities/manager.js +0 -115
  8. package/dist/capabilities/packs.d.ts +0 -2
  9. package/dist/capabilities/packs.js +0 -54
  10. package/dist/capabilities/types.d.ts +0 -21
  11. package/dist/capabilities/types.js +0 -1
  12. package/dist/channels/admin.d.ts +0 -10
  13. package/dist/channels/admin.js +0 -20
  14. package/dist/channels/command-handler.d.ts +0 -19
  15. package/dist/channels/command-handler.js +0 -235
  16. package/dist/channels/discord.d.ts +0 -17
  17. package/dist/channels/discord.js +0 -266
  18. package/dist/channels/index.d.ts +0 -7
  19. package/dist/channels/index.js +0 -6
  20. package/dist/channels/loader.d.ts +0 -7
  21. package/dist/channels/loader.js +0 -87
  22. package/dist/channels/pipeline.d.ts +0 -50
  23. package/dist/channels/pipeline.js +0 -92
  24. package/dist/channels/registry.d.ts +0 -36
  25. package/dist/channels/registry.js +0 -80
  26. package/dist/channels/send-helpers.d.ts +0 -19
  27. package/dist/channels/send-helpers.js +0 -21
  28. package/dist/channels/telegram.d.ts +0 -47
  29. package/dist/channels/telegram.js +0 -649
  30. package/dist/channels/types.d.ts +0 -41
  31. package/dist/channels/types.js +0 -1
  32. package/dist/channels/voice-state.d.ts +0 -29
  33. package/dist/channels/voice-state.js +0 -45
  34. package/dist/channels/webhook.d.ts +0 -16
  35. package/dist/channels/webhook.js +0 -222
  36. package/dist/channels/whatsapp.d.ts +0 -28
  37. package/dist/channels/whatsapp.js +0 -323
  38. package/dist/cli/capabilities.d.ts +0 -3
  39. package/dist/cli/capabilities.js +0 -72
  40. package/dist/cli/channel.d.ts +0 -3
  41. package/dist/cli/channel.js +0 -202
  42. package/dist/cli/commands.d.ts +0 -15
  43. package/dist/cli/commands.js +0 -279
  44. package/dist/cli/doctor.d.ts +0 -1
  45. package/dist/cli/doctor.js +0 -208
  46. package/dist/cli/handoff.d.ts +0 -8
  47. package/dist/cli/handoff.js +0 -33
  48. package/dist/cli/helpers.d.ts +0 -2
  49. package/dist/cli/helpers.js +0 -21
  50. package/dist/cli/mcp.d.ts +0 -3
  51. package/dist/cli/mcp.js +0 -47
  52. package/dist/cli/media.d.ts +0 -2
  53. package/dist/cli/media.js +0 -125
  54. package/dist/cli/setup.d.ts +0 -1
  55. package/dist/cli/setup.js +0 -239
  56. package/dist/cli/store.d.ts +0 -3
  57. package/dist/cli/store.js +0 -108
  58. package/dist/config.d.ts +0 -11
  59. package/dist/config.js +0 -115
  60. package/dist/cron/scheduler.d.ts +0 -22
  61. package/dist/cron/scheduler.js +0 -220
  62. package/dist/cron/store.d.ts +0 -12
  63. package/dist/cron/store.js +0 -83
  64. package/dist/daemon.d.ts +0 -1
  65. package/dist/daemon.js +0 -345
  66. package/dist/dashboard/html.d.ts +0 -1
  67. package/dist/dashboard/html.js +0 -849
  68. package/dist/dashboard/routes.d.ts +0 -17
  69. package/dist/dashboard/routes.js +0 -623
  70. package/dist/dashboard/server.d.ts +0 -1
  71. package/dist/dashboard/server.js +0 -104
  72. package/dist/db/connection.d.ts +0 -29
  73. package/dist/db/connection.js +0 -37
  74. package/dist/db/index.d.ts +0 -14
  75. package/dist/db/index.js +0 -127
  76. package/dist/db/migrations.d.ts +0 -2
  77. package/dist/db/migrations.js +0 -384
  78. package/dist/delegation/manager.d.ts +0 -18
  79. package/dist/delegation/manager.js +0 -89
  80. package/dist/index.d.ts +0 -2
  81. package/dist/knowledge/index.d.ts +0 -2
  82. package/dist/knowledge/index.js +0 -1
  83. package/dist/knowledge/manager.d.ts +0 -17
  84. package/dist/knowledge/manager.js +0 -160
  85. package/dist/knowledge/types.d.ts +0 -7
  86. package/dist/knowledge/types.js +0 -1
  87. package/dist/machines/forwarder.d.ts +0 -7
  88. package/dist/machines/forwarder.js +0 -35
  89. package/dist/machines/index.d.ts +0 -1
  90. package/dist/machines/index.js +0 -1
  91. package/dist/machines/registry.d.ts +0 -15
  92. package/dist/machines/registry.js +0 -46
  93. package/dist/mcp/handlers.d.ts +0 -37
  94. package/dist/mcp/handlers.js +0 -520
  95. package/dist/mcp/server.d.ts +0 -2
  96. package/dist/mcp/server.js +0 -86
  97. package/dist/mcp/tool-definitions.d.ts +0 -1225
  98. package/dist/mcp/tool-definitions.js +0 -412
  99. package/dist/mcp/validate.d.ts +0 -23
  100. package/dist/mcp/validate.js +0 -65
  101. package/dist/media/factory.d.ts +0 -6
  102. package/dist/media/factory.js +0 -36
  103. package/dist/media/generators/dall-e.d.ts +0 -10
  104. package/dist/media/generators/dall-e.js +0 -41
  105. package/dist/media/generators/elevenlabs-music.d.ts +0 -9
  106. package/dist/media/generators/elevenlabs-music.js +0 -34
  107. package/dist/media/generators/elevenlabs-sfx.d.ts +0 -9
  108. package/dist/media/generators/elevenlabs-sfx.js +0 -33
  109. package/dist/media/generators/kling.d.ts +0 -9
  110. package/dist/media/generators/kling.js +0 -47
  111. package/dist/media/generators/lyria.d.ts +0 -10
  112. package/dist/media/generators/lyria.js +0 -41
  113. package/dist/media/generators/nano-banana.d.ts +0 -10
  114. package/dist/media/generators/nano-banana.js +0 -45
  115. package/dist/media/generators/poll-util.d.ts +0 -12
  116. package/dist/media/generators/poll-util.js +0 -22
  117. package/dist/media/generators/recraft.d.ts +0 -9
  118. package/dist/media/generators/recraft.js +0 -52
  119. package/dist/media/generators/runway.d.ts +0 -9
  120. package/dist/media/generators/runway.js +0 -49
  121. package/dist/media/generators/stable-diffusion.d.ts +0 -9
  122. package/dist/media/generators/stable-diffusion.js +0 -35
  123. package/dist/media/generators/veo.d.ts +0 -9
  124. package/dist/media/generators/veo.js +0 -32
  125. package/dist/media/index.d.ts +0 -6
  126. package/dist/media/index.js +0 -8
  127. package/dist/media/loader.d.ts +0 -8
  128. package/dist/media/loader.js +0 -46
  129. package/dist/media/store.d.ts +0 -17
  130. package/dist/media/store.js +0 -69
  131. package/dist/media/types.d.ts +0 -43
  132. package/dist/media/types.js +0 -1
  133. package/dist/memory/extractor.d.ts +0 -5
  134. package/dist/memory/extractor.js +0 -157
  135. package/dist/notifications/index.d.ts +0 -4
  136. package/dist/notifications/index.js +0 -21
  137. package/dist/notifications/ntfy.d.ts +0 -9
  138. package/dist/notifications/ntfy.js +0 -25
  139. package/dist/notifications/pushover.d.ts +0 -9
  140. package/dist/notifications/pushover.js +0 -26
  141. package/dist/notifications/types.d.ts +0 -5
  142. package/dist/notifications/types.js +0 -1
  143. package/dist/notifications/webhook-provider.d.ts +0 -9
  144. package/dist/notifications/webhook-provider.js +0 -24
  145. package/dist/observability/analytics.d.ts +0 -30
  146. package/dist/observability/analytics.js +0 -118
  147. package/dist/pipe/anthropic-client.d.ts +0 -9
  148. package/dist/pipe/anthropic-client.js +0 -50
  149. package/dist/pipe/brain.d.ts +0 -16
  150. package/dist/pipe/brain.js +0 -89
  151. package/dist/pipe/memory-store.d.ts +0 -8
  152. package/dist/pipe/memory-store.js +0 -39
  153. package/dist/pipe/project-scanner.d.ts +0 -6
  154. package/dist/pipe/project-scanner.js +0 -26
  155. package/dist/pipe/types.d.ts +0 -34
  156. package/dist/pipe/types.js +0 -1
  157. package/dist/projects/index.d.ts +0 -4
  158. package/dist/projects/index.js +0 -2
  159. package/dist/projects/manager.d.ts +0 -13
  160. package/dist/projects/manager.js +0 -166
  161. package/dist/projects/router.d.ts +0 -17
  162. package/dist/projects/router.js +0 -246
  163. package/dist/projects/types.d.ts +0 -19
  164. package/dist/projects/types.js +0 -1
  165. package/dist/service/install.d.ts +0 -4
  166. package/dist/service/install.js +0 -146
  167. package/dist/service/templates.d.ts +0 -4
  168. package/dist/service/templates.js +0 -52
  169. package/dist/service/windows.d.ts +0 -8
  170. package/dist/service/windows.js +0 -59
  171. package/dist/session/budget-guard.d.ts +0 -20
  172. package/dist/session/budget-guard.js +0 -31
  173. package/dist/session/circuit-breaker.d.ts +0 -19
  174. package/dist/session/circuit-breaker.js +0 -91
  175. package/dist/session/context-compactor.d.ts +0 -32
  176. package/dist/session/context-compactor.js +0 -45
  177. package/dist/session/context-monitor.d.ts +0 -15
  178. package/dist/session/context-monitor.js +0 -44
  179. package/dist/session/handoff.d.ts +0 -21
  180. package/dist/session/handoff.js +0 -50
  181. package/dist/session/manager.d.ts +0 -60
  182. package/dist/session/manager.js +0 -416
  183. package/dist/session/memory-store.d.ts +0 -29
  184. package/dist/session/memory-store.js +0 -45
  185. package/dist/session/message-queue.d.ts +0 -28
  186. package/dist/session/message-queue.js +0 -52
  187. package/dist/session/pending-dispatcher.d.ts +0 -31
  188. package/dist/session/pending-dispatcher.js +0 -120
  189. package/dist/session/pending-store.d.ts +0 -60
  190. package/dist/session/pending-store.js +0 -118
  191. package/dist/session/stale-session.d.ts +0 -31
  192. package/dist/session/stale-session.js +0 -45
  193. package/dist/session/subprocess.d.ts +0 -32
  194. package/dist/session/subprocess.js +0 -230
  195. package/dist/session/tab-store.d.ts +0 -28
  196. package/dist/session/tab-store.js +0 -78
  197. package/dist/session/tool-classifier.d.ts +0 -4
  198. package/dist/session/tool-classifier.js +0 -56
  199. package/dist/tasks/scheduler.d.ts +0 -45
  200. package/dist/tasks/scheduler.js +0 -329
  201. package/dist/tasks/store.d.ts +0 -11
  202. package/dist/tasks/store.js +0 -103
  203. package/dist/timeline/index.d.ts +0 -3
  204. package/dist/timeline/index.js +0 -2
  205. package/dist/timeline/logger.d.ts +0 -8
  206. package/dist/timeline/logger.js +0 -11
  207. package/dist/timeline/query.d.ts +0 -11
  208. package/dist/timeline/query.js +0 -67
  209. package/dist/timeline/types.d.ts +0 -12
  210. package/dist/timeline/types.js +0 -1
  211. package/dist/types.d.ts +0 -225
  212. package/dist/types.js +0 -3
  213. package/dist/users/index.d.ts +0 -2
  214. package/dist/users/index.js +0 -1
  215. package/dist/users/service.d.ts +0 -17
  216. package/dist/users/service.js +0 -46
  217. package/dist/util/auto-heal.d.ts +0 -43
  218. package/dist/util/auto-heal.js +0 -107
  219. package/dist/util/install-info.d.ts +0 -37
  220. package/dist/util/install-info.js +0 -80
  221. package/dist/util/logger.d.ts +0 -18
  222. package/dist/util/logger.js +0 -118
  223. package/dist/util/paths.d.ts +0 -13
  224. package/dist/util/paths.js +0 -55
  225. package/dist/util/platform.d.ts +0 -2
  226. package/dist/util/platform.js +0 -11
  227. package/dist/util/progress.d.ts +0 -34
  228. package/dist/util/progress.js +0 -133
  229. package/dist/util/rate-limiter.d.ts +0 -14
  230. package/dist/util/rate-limiter.js +0 -50
  231. package/dist/util/retry.d.ts +0 -2
  232. package/dist/util/retry.js +0 -18
  233. package/dist/util/text.d.ts +0 -32
  234. package/dist/util/text.js +0 -101
  235. package/dist/version.d.ts +0 -1
  236. package/dist/version.js +0 -4
  237. package/dist/voice/index.d.ts +0 -17
  238. package/dist/voice/index.js +0 -42
  239. package/dist/voice/stt.d.ts +0 -16
  240. package/dist/voice/stt.js +0 -57
  241. package/dist/voice/tts.d.ts +0 -23
  242. package/dist/voice/tts.js +0 -84
  243. package/dist/watchers/evaluator.d.ts +0 -5
  244. package/dist/watchers/evaluator.js +0 -45
  245. package/dist/watchers/index.d.ts +0 -4
  246. package/dist/watchers/index.js +0 -3
  247. package/dist/watchers/scheduler.d.ts +0 -20
  248. package/dist/watchers/scheduler.js +0 -183
  249. package/dist/watchers/store.d.ts +0 -10
  250. package/dist/watchers/store.js +0 -56
  251. package/dist/watchers/types.d.ts +0 -15
  252. package/dist/watchers/types.js +0 -1
  253. package/scripts/postinstall.mjs +0 -50
@@ -1,649 +0,0 @@
1
- import TelegramBot from 'node-telegram-bot-api';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { chunkText, parseTabMessage, formatTabbedResponse } from '../util/text.js';
5
- import { logger } from '../util/logger.js';
6
- import { retryWithBackoff } from '../util/retry.js';
7
- import { sendChunkedResponse } from './send-helpers.js';
8
- import { getLogsDir } from '../util/paths.js';
9
- import { saveMedia, isOversized } from '../media/store.js';
10
- import { inboundLimiter, groupLimiter } from '../util/rate-limiter.js';
11
- import { processInboundMessage } from './pipeline.js';
12
- import { isChannelAdmin } from './admin.js';
13
- import { VoiceState } from './voice-state.js';
14
- const DEFAULT_GROUP_CONFIG = {
15
- activationMode: 'mention',
16
- maxResponsesPerMinute: 3,
17
- tabPerGroup: true,
18
- };
19
- /**
20
- * Strip Telegram bot tokens out of strings before logging. Telegram embeds
21
- * the token in the URL path (e.g. https://api.telegram.org/bot1234:abc.../method),
22
- * so on fetch errors the message/cause can leak the token to disk.
23
- */
24
- function sanitizeBotToken(text) {
25
- return text.replace(/bot\d+:[A-Za-z0-9_-]+/g, 'bot<REDACTED>');
26
- }
27
- export class TelegramChannel {
28
- id = 'telegram';
29
- name = 'Telegram';
30
- maxMessageLength = 4096;
31
- bot;
32
- ctx;
33
- activeChatIds = new Set();
34
- // Set form of config.telegram.allowedUserIds for O(1) per-message membership
35
- // checks. Built lazily in start() so config edits via reload (if added later)
36
- // can call rebuildAllowedSet().
37
- allowedUserIdSet = new Set();
38
- voice = new VoiceState('telegram');
39
- botUserId = null;
40
- botUsername = null;
41
- mutedGroups = new Set();
42
- welcomeSent = new Set();
43
- // Polling-error tracking: warn the user once when the bot is silently broken
44
- // (network drop, telegram 5xx, token revoked, etc.). Without this, the daemon
45
- // looks fine but Telegram has stopped delivering inbound messages.
46
- pollingErrorTimes = [];
47
- pollingDegradedNotified = false;
48
- constructor(ctx) {
49
- this.ctx = ctx;
50
- this.bot = new TelegramBot(ctx.config.telegram.token, {
51
- polling: {
52
- params: {
53
- timeout: 30,
54
- allowed_updates: ['message', 'callback_query'],
55
- },
56
- autoStart: false,
57
- },
58
- });
59
- this.bot.sendMessage = this.bot.sendMessage.bind(this.bot);
60
- }
61
- async start() {
62
- // Clear pending updates from old sessions, then start polling.
63
- // node-telegram-bot-api's TS types don't include deleteWebHook, so we go
64
- // through a typed shim rather than `as any`.
65
- try {
66
- await this.bot.deleteWebHook({ drop_pending_updates: true });
67
- }
68
- catch (err) {
69
- logger.error('Failed to clear pending updates, starting anyway:', err);
70
- }
71
- // Initialize voice providers (STT + TTS)
72
- this.voice.init(this.ctx.config);
73
- // Subscribe to library-level error events BEFORE startPolling so transient
74
- // failures don't disappear into the void. node-telegram-bot-api emits
75
- // 'polling_error' on network/auth/5xx issues — without a listener these
76
- // were previously silently dropped, leaving the channel dead with no signal.
77
- this.bot.on('polling_error', (err) => {
78
- logger.error('Telegram polling error:', sanitizeBotToken(err?.message || String(err)));
79
- this.recordPollingError();
80
- });
81
- this.bot.on('error', (err) => {
82
- logger.error('Telegram client error:', sanitizeBotToken(err?.message || String(err)));
83
- });
84
- this.bot.startPolling();
85
- this.allowedUserIdSet = new Set(this.ctx.config.telegram.allowedUserIds);
86
- if (this.allowedUserIdSet.size === 0) {
87
- logger.warn('Telegram allowedUserIds is empty — bot will reject all inbound messages until you add at least one user ID.');
88
- }
89
- // Cache bot identity for group mention detection
90
- try {
91
- const me = await this.bot.getMe();
92
- this.botUserId = me.id;
93
- this.botUsername = me.username ?? null;
94
- }
95
- catch (err) {
96
- logger.warn('Failed to fetch bot identity (group mentions may not work):', err);
97
- }
98
- this.setupHandlers();
99
- logger.info('Telegram bot started (polling mode, cleared pending updates)');
100
- }
101
- /**
102
- * Track polling errors over a 60s rolling window. If we see 5+ errors in 60s
103
- * we surface "polling degraded" exactly once via the notify callback so the
104
- * user knows Telegram is broken instead of silently failing.
105
- */
106
- recordPollingError() {
107
- const now = Date.now();
108
- this.pollingErrorTimes.push(now);
109
- this.pollingErrorTimes = this.pollingErrorTimes.filter((t) => now - t < 60_000);
110
- if (this.pollingErrorTimes.length >= 5 && !this.pollingDegradedNotified) {
111
- this.pollingDegradedNotified = true;
112
- this.ctx
113
- .notifyCallback?.('⚠️ Telegram polling degraded (5+ errors in 60s). Check daemon.log.')
114
- .catch((err) => logger.warn('Failed to send polling-degraded notice:', err));
115
- // Reset notification flag after 5 minutes so a sustained outage that
116
- // recovers and reoccurs can re-notify.
117
- setTimeout(() => {
118
- this.pollingDegradedNotified = false;
119
- }, 5 * 60_000).unref();
120
- }
121
- }
122
- stop() {
123
- this.bot.stopPolling();
124
- logger.info('Telegram bot stopped');
125
- }
126
- async sendMessage(peerId, text, _options) {
127
- const chatId = Number(peerId);
128
- const chunks = chunkText(text);
129
- for (const chunk of chunks) {
130
- await this.sendWithRetry(chatId, chunk);
131
- }
132
- }
133
- async sendNotification(message, _urgent) {
134
- for (const chatId of this.activeChatIds) {
135
- try {
136
- await this.bot.sendMessage(chatId, message);
137
- }
138
- catch (err) {
139
- logger.error(`Failed to send notification to chat ${chatId}:`, err);
140
- }
141
- }
142
- for (const userId of this.ctx.config.telegram.allowedUserIds) {
143
- if (this.activeChatIds.has(userId))
144
- continue;
145
- try {
146
- await this.bot.sendMessage(userId, message);
147
- this.activeChatIds.add(userId);
148
- }
149
- catch (err) {
150
- // Differentiate: 400 "chat not found" means the user has not started a
151
- // conversation with the bot yet — silently skip. Anything else (rate
152
- // limit, bot blocked, network) is a real delivery failure worth logging.
153
- const errAny = err;
154
- const status = errAny?.response?.statusCode;
155
- const isChatNotFound = status === 400 || /chat not found/i.test(errAny?.message || '');
156
- if (!isChatNotFound) {
157
- logger.warn(`Telegram notify to ${userId} failed (status=${status ?? '?'}):`, sanitizeBotToken(errAny?.message || String(err)));
158
- }
159
- }
160
- }
161
- }
162
- /**
163
- * Send a media file to every recipient the bot would notify. Used by the
164
- * pending-message dispatcher to deliver media queued by MCP tools.
165
- * Routes by attachment type to the right Telegram primitive (sendVoice for
166
- * voice messages, sendPhoto for images, sendVideo for video, sendDocument
167
- * for everything else).
168
- */
169
- async broadcastMedia(media) {
170
- const recipients = new Set(this.activeChatIds);
171
- for (const userId of this.ctx.config.telegram.allowedUserIds)
172
- recipients.add(userId);
173
- if (recipients.size === 0)
174
- return;
175
- const caption = media.caption ? { caption: media.caption.slice(0, 1024) } : undefined;
176
- for (const chatId of recipients) {
177
- try {
178
- switch (media.type) {
179
- case 'voice':
180
- await this.bot.sendVoice(chatId, media.filePath, caption);
181
- break;
182
- case 'audio':
183
- await this.bot.sendAudio(chatId, media.filePath, caption);
184
- break;
185
- case 'image':
186
- await this.bot.sendPhoto(chatId, media.filePath, caption);
187
- break;
188
- case 'video':
189
- await this.bot.sendVideo(chatId, media.filePath, caption);
190
- break;
191
- case 'document':
192
- default:
193
- await this.bot.sendDocument(chatId, media.filePath, caption);
194
- break;
195
- }
196
- }
197
- catch (err) {
198
- logger.warn(`Telegram broadcastMedia to ${chatId} failed:`, sanitizeBotToken(err instanceof Error ? err.message : String(err)));
199
- }
200
- }
201
- }
202
- async setTyping(peerId, active) {
203
- if (active) {
204
- await this.bot.sendChatAction(Number(peerId), 'typing').catch((err) => {
205
- logger.error(`Typing indicator failed for chat ${peerId}:`, err);
206
- });
207
- }
208
- }
209
- // ─── Private ───
210
- setupHandlers() {
211
- this.bot.on('message', async (msg) => {
212
- if (!this.isAllowed(msg.from?.id))
213
- return;
214
- const chatId = msg.chat.id;
215
- // Rate limit check
216
- if (!inboundLimiter.check(this.id)) {
217
- await this.bot.sendMessage(chatId, "I'm receiving too many messages right now. Please wait a moment.");
218
- return;
219
- }
220
- const isGroup = msg.chat.type === 'group' || msg.chat.type === 'supergroup';
221
- // First-run welcome message (once per user)
222
- if (msg.chat.type === 'private' && !this.welcomeSent.has(chatId)) {
223
- const db = (await import('../db/index.js')).getDb();
224
- const hasMessages = db.prepare('SELECT COUNT(*) as c FROM messages').get();
225
- if (hasMessages.c === 0) {
226
- this.welcomeSent.add(chatId);
227
- const welcomeText = msg.text?.trim() || '';
228
- await this.bot.sendMessage(chatId, [
229
- '\uD83D\uDC4B Welcome to Beecork!\n',
230
- "Send any message and I'll pass it to Claude Code.",
231
- '',
232
- 'Quick tips:',
233
- '\u2022 /tab name message \u2014 organize work into tabs',
234
- "\u2022 /tabs \u2014 see what's running",
235
- '\u2022 /stop name \u2014 stop a tab',
236
- '',
237
- "Let's get started! Send me something.",
238
- ].join('\n'));
239
- // Don't return - let the actual message be processed too (unless it was just /start)
240
- if (welcomeText === '/start')
241
- return;
242
- }
243
- else {
244
- this.welcomeSent.add(chatId);
245
- }
246
- }
247
- // Only add to activeChatIds for private chats
248
- if (!isGroup) {
249
- this.activeChatIds.add(chatId);
250
- }
251
- // Extract text (from text, caption, etc.)
252
- let text = msg.text?.trim() || msg.caption?.trim() || '';
253
- // ─── Group activation logic ───
254
- if (isGroup) {
255
- // Check muted status
256
- if (this.mutedGroups.has(chatId))
257
- return;
258
- const groupConfig = this.ctx.config.groups || DEFAULT_GROUP_CONFIG;
259
- const isMentioned = this.botUsername ? text.includes(`@${this.botUsername}`) : false;
260
- const isReplyToBot = msg.reply_to_message?.from?.id === this.botUserId;
261
- let shouldActivate = false;
262
- switch (groupConfig.activationMode) {
263
- case 'mention':
264
- shouldActivate = !!isMentioned;
265
- break;
266
- case 'reply':
267
- shouldActivate = !!isReplyToBot;
268
- break;
269
- case 'keyword':
270
- shouldActivate =
271
- groupConfig.keywords?.some((kw) => text.toLowerCase().includes(kw.toLowerCase())) ??
272
- false;
273
- break;
274
- case 'always':
275
- shouldActivate = true;
276
- break;
277
- }
278
- if (!shouldActivate)
279
- return;
280
- // Group rate limiting
281
- const groupKey = `group:${chatId}`;
282
- if (!groupLimiter.check(groupKey)) {
283
- // Silently ignore — don't spam the group with rate limit messages
284
- return;
285
- }
286
- // Clean mention from text
287
- if (isMentioned && this.botUsername) {
288
- text = text.replace(new RegExp(`@${this.botUsername}`, 'gi'), '').trim();
289
- }
290
- }
291
- // Track voice pipeline timing
292
- const voicePipelineStart = msg.voice ? Date.now() : null;
293
- // Download media if present (in parallel)
294
- const downloadTasks = [];
295
- if (msg.photo) {
296
- const photo = msg.photo[msg.photo.length - 1];
297
- downloadTasks.push(this.downloadTelegramFile(photo.file_id, 'jpg')
298
- .then((fp) => fp
299
- ? {
300
- type: 'image',
301
- mimeType: 'image/jpeg',
302
- filePath: fp,
303
- fileName: `photo-${photo.file_id}.jpg`,
304
- }
305
- : null)
306
- .catch(() => null));
307
- }
308
- if (msg.voice) {
309
- downloadTasks.push(this.downloadTelegramFile(msg.voice.file_id, 'ogg')
310
- .then((fp) => fp ? { type: 'voice', mimeType: 'audio/ogg', filePath: fp } : null)
311
- .catch(() => null));
312
- }
313
- if (msg.audio) {
314
- downloadTasks.push(this.downloadTelegramFile(msg.audio.file_id, 'mp3')
315
- .then((fp) => fp
316
- ? {
317
- type: 'audio',
318
- mimeType: msg.audio.mime_type || 'audio/mpeg',
319
- filePath: fp,
320
- fileName: msg.audio.title,
321
- }
322
- : null)
323
- .catch(() => null));
324
- }
325
- if (msg.document) {
326
- const ext = msg.document.file_name?.split('.').pop() || 'bin';
327
- downloadTasks.push(this.downloadTelegramFile(msg.document.file_id, ext)
328
- .then((fp) => fp
329
- ? {
330
- type: 'document',
331
- mimeType: msg.document.mime_type || 'application/octet-stream',
332
- filePath: fp,
333
- fileName: msg.document.file_name,
334
- }
335
- : null)
336
- .catch(() => null));
337
- }
338
- if (msg.video) {
339
- downloadTasks.push(this.downloadTelegramFile(msg.video.file_id, 'mp4')
340
- .then((fp) => fp
341
- ? {
342
- type: 'video',
343
- mimeType: msg.video.mime_type || 'video/mp4',
344
- filePath: fp,
345
- }
346
- : null)
347
- .catch(() => null));
348
- }
349
- const downloadResults = await Promise.allSettled(downloadTasks);
350
- const media = downloadResults
351
- .filter((r) => r.status === 'fulfilled' && r.value !== null)
352
- .map((r) => r.value);
353
- // Transcribe voice messages if STT is configured
354
- await this.voice.transcribe(media);
355
- // Skip if no text AND no media
356
- if (!text && media.length === 0)
357
- return;
358
- try {
359
- // Commands bypass debouncing (only if pure text, no media)
360
- if (text.startsWith('/') && media.length === 0) {
361
- await this.handleCommand(chatId, text, msg.from?.id, msg.message_id, isGroup);
362
- return;
363
- }
364
- // Send typing indicator immediately
365
- this.bot.sendChatAction(chatId, 'typing').catch((err) => {
366
- logger.error(`Typing indicator failed for chat ${chatId}:`, err);
367
- });
368
- logger.info(`[telegram] Message received from ${msg.from?.id}, sending typing`);
369
- await this.handleMessage(chatId, text, msg.message_id, media, isGroup);
370
- if (voicePipelineStart) {
371
- logger.info(`[telegram] Voice-to-response total: ${Date.now() - voicePipelineStart}ms`);
372
- }
373
- }
374
- catch (err) {
375
- logger.error('Telegram: error handling message:', err);
376
- // Wrap the fallback send so a Telegram outage doesn't escalate to an
377
- // unhandledRejection on the message-event handler.
378
- this.bot
379
- .sendMessage(chatId, 'Something went wrong processing your message. Check daemon logs for details.')
380
- .catch((sendErr) => logger.error('Telegram: failed to send fallback error message:', sendErr));
381
- }
382
- });
383
- }
384
- async handleCommand(chatId, text, userId, messageId, isGroup = false) {
385
- // Telegram-only group commands
386
- if (text === '/mute' && isGroup) {
387
- this.mutedGroups.add(chatId);
388
- await this.bot.sendMessage(chatId, 'Beecork muted in this group. Use /unmute to re-enable.');
389
- return;
390
- }
391
- if (text === '/unmute' && isGroup) {
392
- this.mutedGroups.delete(chatId);
393
- await this.bot.sendMessage(chatId, 'Beecork unmuted in this group.');
394
- return;
395
- }
396
- // /history and /knowledge now handled by the shared command handler.
397
- // Shared command handler (covers /tabs, /stop, /tab, /projects, /project, /newproject, /close, /fresh, /cost, /activity, /handoff, /history, /knowledge)
398
- const { handleSharedCommand } = await import('./command-handler.js');
399
- const result = await handleSharedCommand({
400
- userId: String(userId || 'default'),
401
- text,
402
- isAdmin: this.isAdmin(userId),
403
- channelId: 'telegram',
404
- }, this.ctx.tabManager);
405
- if (result.handled) {
406
- if (result.response)
407
- await this.bot.sendMessage(chatId, result.response);
408
- return;
409
- }
410
- // /tab with valid name — falls through from shared handler, treat as message
411
- if (text.startsWith('/tab ')) {
412
- await this.handleMessage(chatId, text, messageId);
413
- return;
414
- }
415
- // Unknown command — treat as regular message
416
- await this.handleMessage(chatId, text, messageId);
417
- }
418
- async handleMessage(chatId, text, messageId, media = [], isGroup = false) {
419
- const { tabName } = parseTabMessage(text);
420
- if (!tabName && !text && media.length === 0)
421
- return;
422
- // Telegram-specific: group tab routing
423
- let overrideTabName;
424
- if (isGroup) {
425
- const groupConfig = this.ctx.config.groups || DEFAULT_GROUP_CONFIG;
426
- if (groupConfig.tabPerGroup && !text.startsWith('/tab ')) {
427
- overrideTabName = `group-tg-${Math.abs(chatId)}`;
428
- }
429
- }
430
- // React with ⏳
431
- await this.setReaction(chatId, messageId, '⏳');
432
- // Typing indicator — keep refreshing every 4s
433
- const sendTyping = () => this.bot.sendChatAction(chatId, 'typing').catch((err) => {
434
- logger.error(`Typing indicator failed:`, err);
435
- });
436
- await sendTyping();
437
- const typingInterval = setInterval(sendTyping, 4000);
438
- // "Still working" timeout
439
- const stillWorkingTimeout = setTimeout(() => {
440
- this.bot.sendMessage(chatId, `Still working on your request...`).catch(() => { });
441
- }, 120000);
442
- try {
443
- // Telegram-specific: streaming message edits
444
- let streamMsgId = null;
445
- let streamBuffer = '';
446
- let lastEditTime = 0;
447
- // We need the effective tab name for the stream prefix, but it's determined
448
- // inside the pipeline. Use a mutable ref that the pipeline result will fill.
449
- let effectiveTabForStream = overrideTabName || tabName;
450
- const onTextChunk = async (chunk) => {
451
- streamBuffer += chunk;
452
- const now = Date.now();
453
- if (streamBuffer.length < 100 || now - lastEditTime < 1000)
454
- return;
455
- lastEditTime = now;
456
- try {
457
- const truncated = streamBuffer.slice(0, 4000) + (streamBuffer.length > 4000 ? '...' : '');
458
- const preview = formatTabbedResponse(truncated, effectiveTabForStream);
459
- if (!streamMsgId) {
460
- const sent = await this.bot.sendMessage(chatId, preview);
461
- streamMsgId = sent.message_id;
462
- }
463
- else {
464
- await this.bot.editMessageText(preview, { chat_id: chatId, message_id: streamMsgId });
465
- }
466
- }
467
- catch {
468
- /* edit failures are non-critical */
469
- }
470
- };
471
- // Shared pipeline handles: routing, media prompt, progress, sendMessage, TTS
472
- const pipelineResult = await processInboundMessage({
473
- text,
474
- media,
475
- channelId: 'telegram',
476
- tabManager: this.ctx.tabManager,
477
- voiceReplyMode: this.ctx.config.voice?.replyMode,
478
- ttsProvider: this.voice.tts,
479
- userId: String(chatId),
480
- sendProgress: (msg) => {
481
- this.bot.sendMessage(chatId, msg).catch(() => { });
482
- },
483
- overrideTabName,
484
- onTextChunk,
485
- });
486
- // Empty result means no prompt and no media
487
- if (!pipelineResult.responseText) {
488
- clearInterval(typingInterval);
489
- clearTimeout(stillWorkingTimeout);
490
- return;
491
- }
492
- // Update the effective tab for stream prefix (now known)
493
- effectiveTabForStream = pipelineResult.tabName;
494
- const responseText = pipelineResult.responseText;
495
- const responseError = pipelineResult.isError;
496
- const responseTab = pipelineResult.tabName;
497
- // Telegram-specific: if streaming was active and no error, edit the final message
498
- if (streamMsgId && !responseError) {
499
- clearInterval(typingInterval);
500
- clearTimeout(stillWorkingTimeout);
501
- await this.setReaction(chatId, messageId, '✅');
502
- // Send voice if available (even with streaming)
503
- if (pipelineResult.audioPath) {
504
- await this.bot.sendVoice(chatId, pipelineResult.audioPath);
505
- if (pipelineResult.voiceOnly)
506
- return;
507
- }
508
- try {
509
- const finalText = formatTabbedResponse(responseText, responseTab);
510
- if (finalText.length <= 4096) {
511
- await this.bot.editMessageText(finalText, { chat_id: chatId, message_id: streamMsgId });
512
- }
513
- else {
514
- await this.sendResponse(chatId, responseText, responseTab);
515
- }
516
- }
517
- catch {
518
- await this.sendResponse(chatId, responseText, responseTab);
519
- }
520
- return;
521
- }
522
- // Send voice reply if TTS generated audio (non-streaming path)
523
- if (pipelineResult.audioPath) {
524
- clearInterval(typingInterval);
525
- clearTimeout(stillWorkingTimeout);
526
- await this.setReaction(chatId, messageId, responseError ? '❌' : '✅');
527
- await this.bot.sendVoice(chatId, pipelineResult.audioPath);
528
- if (pipelineResult.voiceOnly)
529
- return;
530
- if (!responseError) {
531
- await this.sendResponse(chatId, responseText, responseTab);
532
- return;
533
- }
534
- }
535
- clearInterval(typingInterval);
536
- clearTimeout(stillWorkingTimeout);
537
- if (responseError) {
538
- await this.setReaction(chatId, messageId, '❌');
539
- await this.sendResponse(chatId, responseText, responseTab);
540
- return;
541
- }
542
- await this.setReaction(chatId, messageId, '✅');
543
- await this.sendResponse(chatId, responseText, responseTab);
544
- }
545
- catch (err) {
546
- clearInterval(typingInterval);
547
- clearTimeout(stillWorkingTimeout);
548
- await this.setReaction(chatId, messageId, '❌');
549
- throw err;
550
- }
551
- }
552
- async sendResponse(chatId, text, tabName) {
553
- const fullText = formatTabbedResponse(text, tabName);
554
- const chunks = chunkText(fullText);
555
- // Telegram-specific quirk: if the response would be >10 chunks, send a
556
- // preview + the rest as a file. This runs BEFORE sendChunkedResponse so the
557
- // helper is only invoked for normal-sized responses.
558
- if (chunks.length > 10) {
559
- for (let i = 0; i < 3; i++) {
560
- await this.sendWithRetry(chatId, chunks[i]);
561
- }
562
- const tmpPath = path.join(getLogsDir(), `response-${Date.now()}.txt`);
563
- fs.writeFileSync(tmpPath, fullText);
564
- await this.bot.sendDocument(chatId, tmpPath, {
565
- caption: `Full response (${chunks.length} chunks)`,
566
- });
567
- fs.unlinkSync(tmpPath);
568
- return;
569
- }
570
- // Use the shared chunked-send helper so prefix/chunk/retry logic stays
571
- // identical across Telegram/Discord/WhatsApp. Quirky per-chunk error
572
- // logging (delivery-failures.log) stays in sendWithRetry.
573
- await sendChunkedResponse({
574
- text,
575
- tabName,
576
- maxLength: this.maxMessageLength,
577
- retryLabel: 'telegram-send',
578
- sendChunk: (chunk) => this.sendWithRetryRaw(chatId, chunk),
579
- });
580
- }
581
- async sendWithRetry(chatId, text) {
582
- // Wrapped call used by the >10-chunk fallback path. retryWithBackoff +
583
- // delivery-failures.log on permanent failure.
584
- try {
585
- await this.sendWithRetryRaw(chatId, text);
586
- }
587
- catch (err) {
588
- const failLog = path.join(getLogsDir(), 'delivery-failures.log');
589
- const sanitizedErr = sanitizeBotToken(err instanceof Error ? err.message : String(err));
590
- const entry = `[${new Date().toISOString()}] chatId=${chatId} error=${sanitizedErr} text=${text.slice(0, 200)}\n`;
591
- fs.appendFileSync(failLog, entry);
592
- logger.error(`Delivery failed after retries for chat ${chatId}`);
593
- }
594
- }
595
- async sendWithRetryRaw(chatId, text) {
596
- await retryWithBackoff(
597
- // Send as plain text — Telegram's legacy "Markdown" parser silently mangles
598
- // underscores/asterisks in Claude's responses (code identifiers, names),
599
- // and Beecork has no escaping pass for it.
600
- () => this.bot.sendMessage(chatId, text), [1000, 5000, 15000], 'telegram-send');
601
- }
602
- async downloadTelegramFile(fileId, extension) {
603
- const fileInfo = await this.bot.getFile(fileId);
604
- if (!fileInfo.file_path)
605
- return null;
606
- // Check file size (Telegram provides file_size in bytes)
607
- if (fileInfo.file_size && isOversized(fileInfo.file_size)) {
608
- logger.warn(`Skipping oversized file: ${fileInfo.file_size} bytes`);
609
- return null;
610
- }
611
- const url = `https://api.telegram.org/file/bot${this.ctx.config.telegram.token}/${fileInfo.file_path}`;
612
- const response = await fetch(url, { signal: AbortSignal.timeout(30000) });
613
- if (!response.ok)
614
- return null;
615
- const buffer = Buffer.from(await response.arrayBuffer());
616
- return saveMedia(buffer, extension, fileInfo.file_path.split('/').pop());
617
- }
618
- isAllowed(userId) {
619
- if (!userId)
620
- return false;
621
- // Set-based O(1) membership check; explicit empty-set check keeps the
622
- // fail-closed contract documented in code.
623
- if (this.allowedUserIdSet.size === 0)
624
- return false;
625
- return this.allowedUserIdSet.has(userId);
626
- }
627
- isAdmin(userId) {
628
- const cfg = this.ctx.config.telegram;
629
- return isChannelAdmin(cfg.allowedUserIds, userId, cfg.adminUserId);
630
- }
631
- async setReaction(chatId, messageId, emoji) {
632
- try {
633
- const url = `https://api.telegram.org/bot${this.ctx.config.telegram.token}/setMessageReaction`;
634
- await fetch(url, {
635
- method: 'POST',
636
- headers: { 'Content-Type': 'application/json' },
637
- body: JSON.stringify({
638
- chat_id: chatId,
639
- message_id: messageId,
640
- reaction: [{ type: 'emoji', emoji }],
641
- }),
642
- signal: AbortSignal.timeout(5000),
643
- });
644
- }
645
- catch {
646
- // Reactions not supported or failed — non-critical
647
- }
648
- }
649
- }