cowork-os 0.3.21

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 (526) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1638 -0
  3. package/bin/cowork.js +42 -0
  4. package/build/entitlements.mac.plist +16 -0
  5. package/build/icon.icns +0 -0
  6. package/build/icon.png +0 -0
  7. package/dist/electron/electron/activity/ActivityRepository.js +190 -0
  8. package/dist/electron/electron/agent/browser/browser-service.js +639 -0
  9. package/dist/electron/electron/agent/context-manager.js +225 -0
  10. package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
  11. package/dist/electron/electron/agent/daemon.js +975 -0
  12. package/dist/electron/electron/agent/executor.js +3561 -0
  13. package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
  14. package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
  15. package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
  16. package/dist/electron/electron/agent/llm/index.js +34 -0
  17. package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
  18. package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
  19. package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
  20. package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
  21. package/dist/electron/electron/agent/llm/pricing.js +160 -0
  22. package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
  23. package/dist/electron/electron/agent/llm/types.js +178 -0
  24. package/dist/electron/electron/agent/queue-manager.js +378 -0
  25. package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
  26. package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
  27. package/dist/electron/electron/agent/sandbox/runner.js +410 -0
  28. package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
  29. package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
  30. package/dist/electron/electron/agent/search/brave-provider.js +119 -0
  31. package/dist/electron/electron/agent/search/google-provider.js +100 -0
  32. package/dist/electron/electron/agent/search/index.js +28 -0
  33. package/dist/electron/electron/agent/search/provider-factory.js +395 -0
  34. package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
  35. package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
  36. package/dist/electron/electron/agent/search/types.js +40 -0
  37. package/dist/electron/electron/agent/security/index.js +12 -0
  38. package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
  39. package/dist/electron/electron/agent/security/output-filter.js +217 -0
  40. package/dist/electron/electron/agent/skill-eligibility.js +281 -0
  41. package/dist/electron/electron/agent/skill-registry.js +396 -0
  42. package/dist/electron/electron/agent/skills/document.js +878 -0
  43. package/dist/electron/electron/agent/skills/image-generator.js +225 -0
  44. package/dist/electron/electron/agent/skills/organizer.js +141 -0
  45. package/dist/electron/electron/agent/skills/presentation.js +367 -0
  46. package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
  47. package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
  48. package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
  49. package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
  50. package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
  51. package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
  52. package/dist/electron/electron/agent/tools/file-tools.js +719 -0
  53. package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
  54. package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
  55. package/dist/electron/electron/agent/tools/image-tools.js +111 -0
  56. package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
  57. package/dist/electron/electron/agent/tools/node-tools.js +476 -0
  58. package/dist/electron/electron/agent/tools/registry.js +2719 -0
  59. package/dist/electron/electron/agent/tools/search-tools.js +91 -0
  60. package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
  61. package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
  62. package/dist/electron/electron/agent/tools/system-tools.js +578 -0
  63. package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
  64. package/dist/electron/electron/agent/tools/x-tools.js +264 -0
  65. package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
  66. package/dist/electron/electron/agents/HeartbeatService.js +356 -0
  67. package/dist/electron/electron/agents/MentionRepository.js +197 -0
  68. package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
  69. package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
  70. package/dist/electron/electron/canvas/canvas-manager.js +714 -0
  71. package/dist/electron/electron/canvas/canvas-preload.js +53 -0
  72. package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
  73. package/dist/electron/electron/canvas/canvas-store.js +174 -0
  74. package/dist/electron/electron/canvas/index.js +13 -0
  75. package/dist/electron/electron/control-plane/client.js +364 -0
  76. package/dist/electron/electron/control-plane/handlers.js +572 -0
  77. package/dist/electron/electron/control-plane/index.js +41 -0
  78. package/dist/electron/electron/control-plane/node-manager.js +264 -0
  79. package/dist/electron/electron/control-plane/protocol.js +194 -0
  80. package/dist/electron/electron/control-plane/remote-client.js +437 -0
  81. package/dist/electron/electron/control-plane/server.js +640 -0
  82. package/dist/electron/electron/control-plane/settings.js +369 -0
  83. package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
  84. package/dist/electron/electron/cron/index.js +30 -0
  85. package/dist/electron/electron/cron/schedule.js +190 -0
  86. package/dist/electron/electron/cron/service.js +614 -0
  87. package/dist/electron/electron/cron/store.js +155 -0
  88. package/dist/electron/electron/cron/types.js +82 -0
  89. package/dist/electron/electron/cron/webhook.js +258 -0
  90. package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
  91. package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
  92. package/dist/electron/electron/database/repositories.js +1781 -0
  93. package/dist/electron/electron/database/schema.js +978 -0
  94. package/dist/electron/electron/extensions/index.js +33 -0
  95. package/dist/electron/electron/extensions/loader.js +313 -0
  96. package/dist/electron/electron/extensions/registry.js +485 -0
  97. package/dist/electron/electron/extensions/types.js +11 -0
  98. package/dist/electron/electron/gateway/channel-registry.js +1102 -0
  99. package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
  100. package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
  101. package/dist/electron/electron/gateway/channels/discord.js +975 -0
  102. package/dist/electron/electron/gateway/channels/email-client.js +593 -0
  103. package/dist/electron/electron/gateway/channels/email.js +443 -0
  104. package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
  105. package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
  106. package/dist/electron/electron/gateway/channels/imessage.js +465 -0
  107. package/dist/electron/electron/gateway/channels/index.js +36 -0
  108. package/dist/electron/electron/gateway/channels/line-client.js +470 -0
  109. package/dist/electron/electron/gateway/channels/line.js +479 -0
  110. package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
  111. package/dist/electron/electron/gateway/channels/matrix.js +592 -0
  112. package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
  113. package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
  114. package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
  115. package/dist/electron/electron/gateway/channels/signal.js +582 -0
  116. package/dist/electron/electron/gateway/channels/slack.js +415 -0
  117. package/dist/electron/electron/gateway/channels/teams.js +596 -0
  118. package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
  119. package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
  120. package/dist/electron/electron/gateway/channels/twitch.js +396 -0
  121. package/dist/electron/electron/gateway/channels/types.js +8 -0
  122. package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
  123. package/dist/electron/electron/gateway/context-policy.js +268 -0
  124. package/dist/electron/electron/gateway/index.js +1063 -0
  125. package/dist/electron/electron/gateway/infrastructure.js +496 -0
  126. package/dist/electron/electron/gateway/router.js +2700 -0
  127. package/dist/electron/electron/gateway/security.js +375 -0
  128. package/dist/electron/electron/gateway/session.js +115 -0
  129. package/dist/electron/electron/gateway/tunnel.js +503 -0
  130. package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
  131. package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
  132. package/dist/electron/electron/hooks/index.js +46 -0
  133. package/dist/electron/electron/hooks/mappings.js +381 -0
  134. package/dist/electron/electron/hooks/server.js +480 -0
  135. package/dist/electron/electron/hooks/settings.js +447 -0
  136. package/dist/electron/electron/hooks/types.js +41 -0
  137. package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
  138. package/dist/electron/electron/ipc/handlers.js +3138 -0
  139. package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
  140. package/dist/electron/electron/main.js +448 -0
  141. package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
  142. package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
  143. package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
  144. package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
  145. package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
  146. package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
  147. package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
  148. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
  149. package/dist/electron/electron/mcp/settings.js +446 -0
  150. package/dist/electron/electron/mcp/types.js +59 -0
  151. package/dist/electron/electron/memory/MemoryService.js +435 -0
  152. package/dist/electron/electron/notifications/index.js +17 -0
  153. package/dist/electron/electron/notifications/service.js +118 -0
  154. package/dist/electron/electron/notifications/store.js +144 -0
  155. package/dist/electron/electron/preload.js +842 -0
  156. package/dist/electron/electron/reports/StandupReportService.js +272 -0
  157. package/dist/electron/electron/security/concurrency.js +293 -0
  158. package/dist/electron/electron/security/index.js +15 -0
  159. package/dist/electron/electron/security/policy-manager.js +435 -0
  160. package/dist/electron/electron/settings/appearance-manager.js +193 -0
  161. package/dist/electron/electron/settings/personality-manager.js +724 -0
  162. package/dist/electron/electron/settings/x-manager.js +58 -0
  163. package/dist/electron/electron/tailscale/exposure.js +188 -0
  164. package/dist/electron/electron/tailscale/index.js +28 -0
  165. package/dist/electron/electron/tailscale/settings.js +205 -0
  166. package/dist/electron/electron/tailscale/tailscale.js +355 -0
  167. package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
  168. package/dist/electron/electron/tray/TrayManager.js +895 -0
  169. package/dist/electron/electron/tray/index.js +9 -0
  170. package/dist/electron/electron/updater/index.js +6 -0
  171. package/dist/electron/electron/updater/update-manager.js +418 -0
  172. package/dist/electron/electron/utils/env-migration.js +209 -0
  173. package/dist/electron/electron/utils/process.js +102 -0
  174. package/dist/electron/electron/utils/rate-limiter.js +104 -0
  175. package/dist/electron/electron/utils/validation.js +419 -0
  176. package/dist/electron/electron/utils/x-cli.js +177 -0
  177. package/dist/electron/electron/voice/VoiceService.js +507 -0
  178. package/dist/electron/electron/voice/index.js +14 -0
  179. package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
  180. package/dist/electron/shared/channelMessages.js +170 -0
  181. package/dist/electron/shared/types.js +1185 -0
  182. package/package.json +159 -0
  183. package/resources/skills/1password.json +10 -0
  184. package/resources/skills/add-documentation.json +31 -0
  185. package/resources/skills/analyze-csv.json +17 -0
  186. package/resources/skills/apple-notes.json +10 -0
  187. package/resources/skills/apple-reminders.json +10 -0
  188. package/resources/skills/auto-commenter.json +10 -0
  189. package/resources/skills/bear-notes.json +10 -0
  190. package/resources/skills/bird.json +35 -0
  191. package/resources/skills/blogwatcher.json +10 -0
  192. package/resources/skills/blucli.json +10 -0
  193. package/resources/skills/bluebubbles.json +10 -0
  194. package/resources/skills/camsnap.json +10 -0
  195. package/resources/skills/clean-imports.json +18 -0
  196. package/resources/skills/code-review.json +18 -0
  197. package/resources/skills/coding-agent.json +10 -0
  198. package/resources/skills/compare-files.json +23 -0
  199. package/resources/skills/convert-code.json +34 -0
  200. package/resources/skills/create-changelog.json +24 -0
  201. package/resources/skills/debug-error.json +17 -0
  202. package/resources/skills/dependency-check.json +10 -0
  203. package/resources/skills/discord.json +10 -0
  204. package/resources/skills/eightctl.json +10 -0
  205. package/resources/skills/explain-code.json +29 -0
  206. package/resources/skills/extract-todos.json +18 -0
  207. package/resources/skills/food-order.json +10 -0
  208. package/resources/skills/gemini.json +10 -0
  209. package/resources/skills/generate-readme.json +10 -0
  210. package/resources/skills/gifgrep.json +10 -0
  211. package/resources/skills/git-commit.json +10 -0
  212. package/resources/skills/github.json +10 -0
  213. package/resources/skills/gog.json +10 -0
  214. package/resources/skills/goplaces.json +10 -0
  215. package/resources/skills/himalaya.json +10 -0
  216. package/resources/skills/imsg.json +10 -0
  217. package/resources/skills/karpathy-guidelines.json +12 -0
  218. package/resources/skills/last30days.json +26 -0
  219. package/resources/skills/local-places.json +10 -0
  220. package/resources/skills/mcporter.json +10 -0
  221. package/resources/skills/model-usage.json +10 -0
  222. package/resources/skills/nano-banana-pro.json +10 -0
  223. package/resources/skills/nano-pdf.json +10 -0
  224. package/resources/skills/notion.json +10 -0
  225. package/resources/skills/obsidian.json +10 -0
  226. package/resources/skills/openai-image-gen.json +10 -0
  227. package/resources/skills/openai-whisper-api.json +10 -0
  228. package/resources/skills/openai-whisper.json +10 -0
  229. package/resources/skills/openhue.json +10 -0
  230. package/resources/skills/oracle.json +10 -0
  231. package/resources/skills/ordercli.json +10 -0
  232. package/resources/skills/peekaboo.json +10 -0
  233. package/resources/skills/project-structure.json +10 -0
  234. package/resources/skills/proofread.json +17 -0
  235. package/resources/skills/refactor-code.json +31 -0
  236. package/resources/skills/rename-symbol.json +23 -0
  237. package/resources/skills/sag.json +10 -0
  238. package/resources/skills/security-audit.json +18 -0
  239. package/resources/skills/session-logs.json +10 -0
  240. package/resources/skills/sherpa-onnx-tts.json +10 -0
  241. package/resources/skills/skill-creator.json +15 -0
  242. package/resources/skills/skill-hub.json +29 -0
  243. package/resources/skills/slack.json +10 -0
  244. package/resources/skills/songsee.json +10 -0
  245. package/resources/skills/sonoscli.json +10 -0
  246. package/resources/skills/spotify-player.json +10 -0
  247. package/resources/skills/startup-cfo.json +55 -0
  248. package/resources/skills/summarize-folder.json +18 -0
  249. package/resources/skills/summarize.json +10 -0
  250. package/resources/skills/things-mac.json +10 -0
  251. package/resources/skills/tmux.json +10 -0
  252. package/resources/skills/translate.json +36 -0
  253. package/resources/skills/trello.json +10 -0
  254. package/resources/skills/video-frames.json +10 -0
  255. package/resources/skills/voice-call.json +10 -0
  256. package/resources/skills/wacli.json +10 -0
  257. package/resources/skills/weather.json +10 -0
  258. package/resources/skills/write-tests.json +31 -0
  259. package/src/electron/activity/ActivityRepository.ts +238 -0
  260. package/src/electron/agent/browser/browser-service.ts +721 -0
  261. package/src/electron/agent/context-manager.ts +257 -0
  262. package/src/electron/agent/custom-skill-loader.ts +634 -0
  263. package/src/electron/agent/daemon.ts +1097 -0
  264. package/src/electron/agent/executor.ts +4017 -0
  265. package/src/electron/agent/llm/anthropic-provider.ts +175 -0
  266. package/src/electron/agent/llm/bedrock-provider.ts +236 -0
  267. package/src/electron/agent/llm/gemini-provider.ts +422 -0
  268. package/src/electron/agent/llm/index.ts +9 -0
  269. package/src/electron/agent/llm/ollama-provider.ts +347 -0
  270. package/src/electron/agent/llm/openai-oauth.ts +127 -0
  271. package/src/electron/agent/llm/openai-provider.ts +686 -0
  272. package/src/electron/agent/llm/openrouter-provider.ts +273 -0
  273. package/src/electron/agent/llm/pricing.ts +180 -0
  274. package/src/electron/agent/llm/provider-factory.ts +971 -0
  275. package/src/electron/agent/llm/types.ts +291 -0
  276. package/src/electron/agent/queue-manager.ts +408 -0
  277. package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
  278. package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
  279. package/src/electron/agent/sandbox/runner.ts +453 -0
  280. package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
  281. package/src/electron/agent/sandbox/security-utils.ts +251 -0
  282. package/src/electron/agent/search/brave-provider.ts +141 -0
  283. package/src/electron/agent/search/google-provider.ts +131 -0
  284. package/src/electron/agent/search/index.ts +6 -0
  285. package/src/electron/agent/search/provider-factory.ts +450 -0
  286. package/src/electron/agent/search/serpapi-provider.ts +138 -0
  287. package/src/electron/agent/search/tavily-provider.ts +108 -0
  288. package/src/electron/agent/search/types.ts +118 -0
  289. package/src/electron/agent/security/index.ts +20 -0
  290. package/src/electron/agent/security/input-sanitizer.ts +380 -0
  291. package/src/electron/agent/security/output-filter.ts +259 -0
  292. package/src/electron/agent/skill-eligibility.ts +334 -0
  293. package/src/electron/agent/skill-registry.ts +457 -0
  294. package/src/electron/agent/skills/document.ts +1070 -0
  295. package/src/electron/agent/skills/image-generator.ts +272 -0
  296. package/src/electron/agent/skills/organizer.ts +131 -0
  297. package/src/electron/agent/skills/presentation.ts +418 -0
  298. package/src/electron/agent/skills/spreadsheet.ts +166 -0
  299. package/src/electron/agent/tools/browser-tools.ts +546 -0
  300. package/src/electron/agent/tools/builtin-settings.ts +422 -0
  301. package/src/electron/agent/tools/canvas-tools.ts +572 -0
  302. package/src/electron/agent/tools/cron-tools.ts +723 -0
  303. package/src/electron/agent/tools/edit-tools.ts +196 -0
  304. package/src/electron/agent/tools/file-tools.ts +811 -0
  305. package/src/electron/agent/tools/glob-tools.ts +303 -0
  306. package/src/electron/agent/tools/grep-tools.ts +432 -0
  307. package/src/electron/agent/tools/image-tools.ts +126 -0
  308. package/src/electron/agent/tools/mention-tools.ts +371 -0
  309. package/src/electron/agent/tools/node-tools.ts +550 -0
  310. package/src/electron/agent/tools/registry.ts +3052 -0
  311. package/src/electron/agent/tools/search-tools.ts +111 -0
  312. package/src/electron/agent/tools/shell-tools.ts +651 -0
  313. package/src/electron/agent/tools/skill-tools.ts +340 -0
  314. package/src/electron/agent/tools/system-tools.ts +665 -0
  315. package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
  316. package/src/electron/agent/tools/x-tools.ts +267 -0
  317. package/src/electron/agents/AgentRoleRepository.ts +557 -0
  318. package/src/electron/agents/HeartbeatService.ts +469 -0
  319. package/src/electron/agents/MentionRepository.ts +242 -0
  320. package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
  321. package/src/electron/agents/WorkingStateRepository.ts +278 -0
  322. package/src/electron/canvas/canvas-manager.ts +818 -0
  323. package/src/electron/canvas/canvas-preload.ts +102 -0
  324. package/src/electron/canvas/canvas-protocol.ts +174 -0
  325. package/src/electron/canvas/canvas-store.ts +200 -0
  326. package/src/electron/canvas/index.ts +8 -0
  327. package/src/electron/control-plane/client.ts +527 -0
  328. package/src/electron/control-plane/handlers.ts +723 -0
  329. package/src/electron/control-plane/index.ts +51 -0
  330. package/src/electron/control-plane/node-manager.ts +322 -0
  331. package/src/electron/control-plane/protocol.ts +269 -0
  332. package/src/electron/control-plane/remote-client.ts +517 -0
  333. package/src/electron/control-plane/server.ts +853 -0
  334. package/src/electron/control-plane/settings.ts +401 -0
  335. package/src/electron/control-plane/ssh-tunnel.ts +624 -0
  336. package/src/electron/cron/index.ts +9 -0
  337. package/src/electron/cron/schedule.ts +217 -0
  338. package/src/electron/cron/service.ts +743 -0
  339. package/src/electron/cron/store.ts +165 -0
  340. package/src/electron/cron/types.ts +291 -0
  341. package/src/electron/cron/webhook.ts +303 -0
  342. package/src/electron/database/SecureSettingsRepository.ts +514 -0
  343. package/src/electron/database/TaskLabelRepository.ts +148 -0
  344. package/src/electron/database/repositories.ts +2397 -0
  345. package/src/electron/database/schema.ts +1017 -0
  346. package/src/electron/extensions/index.ts +18 -0
  347. package/src/electron/extensions/loader.ts +336 -0
  348. package/src/electron/extensions/registry.ts +546 -0
  349. package/src/electron/extensions/types.ts +372 -0
  350. package/src/electron/gateway/channel-registry.ts +1267 -0
  351. package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
  352. package/src/electron/gateway/channels/bluebubbles.ts +509 -0
  353. package/src/electron/gateway/channels/discord.ts +1150 -0
  354. package/src/electron/gateway/channels/email-client.ts +708 -0
  355. package/src/electron/gateway/channels/email.ts +516 -0
  356. package/src/electron/gateway/channels/google-chat.ts +760 -0
  357. package/src/electron/gateway/channels/imessage-client.ts +473 -0
  358. package/src/electron/gateway/channels/imessage.ts +520 -0
  359. package/src/electron/gateway/channels/index.ts +21 -0
  360. package/src/electron/gateway/channels/line-client.ts +598 -0
  361. package/src/electron/gateway/channels/line.ts +559 -0
  362. package/src/electron/gateway/channels/matrix-client.ts +632 -0
  363. package/src/electron/gateway/channels/matrix.ts +655 -0
  364. package/src/electron/gateway/channels/mattermost-client.ts +526 -0
  365. package/src/electron/gateway/channels/mattermost.ts +550 -0
  366. package/src/electron/gateway/channels/signal-client.ts +722 -0
  367. package/src/electron/gateway/channels/signal.ts +666 -0
  368. package/src/electron/gateway/channels/slack.ts +458 -0
  369. package/src/electron/gateway/channels/teams.ts +681 -0
  370. package/src/electron/gateway/channels/telegram.ts +1727 -0
  371. package/src/electron/gateway/channels/twitch-client.ts +665 -0
  372. package/src/electron/gateway/channels/twitch.ts +468 -0
  373. package/src/electron/gateway/channels/types.ts +1002 -0
  374. package/src/electron/gateway/channels/whatsapp.ts +1101 -0
  375. package/src/electron/gateway/context-policy.ts +382 -0
  376. package/src/electron/gateway/index.ts +1274 -0
  377. package/src/electron/gateway/infrastructure.ts +645 -0
  378. package/src/electron/gateway/router.ts +3206 -0
  379. package/src/electron/gateway/security.ts +422 -0
  380. package/src/electron/gateway/session.ts +144 -0
  381. package/src/electron/gateway/tunnel.ts +626 -0
  382. package/src/electron/guardrails/guardrail-manager.ts +380 -0
  383. package/src/electron/hooks/gmail-watcher.ts +355 -0
  384. package/src/electron/hooks/index.ts +30 -0
  385. package/src/electron/hooks/mappings.ts +404 -0
  386. package/src/electron/hooks/server.ts +574 -0
  387. package/src/electron/hooks/settings.ts +466 -0
  388. package/src/electron/hooks/types.ts +245 -0
  389. package/src/electron/ipc/canvas-handlers.ts +223 -0
  390. package/src/electron/ipc/handlers.ts +3661 -0
  391. package/src/electron/ipc/mission-control-handlers.ts +182 -0
  392. package/src/electron/main.ts +496 -0
  393. package/src/electron/mcp/client/MCPClientManager.ts +406 -0
  394. package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
  395. package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
  396. package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
  397. package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
  398. package/src/electron/mcp/host/MCPHostServer.ts +388 -0
  399. package/src/electron/mcp/host/ToolAdapter.ts +140 -0
  400. package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
  401. package/src/electron/mcp/settings.ts +468 -0
  402. package/src/electron/mcp/types.ts +371 -0
  403. package/src/electron/memory/MemoryService.ts +523 -0
  404. package/src/electron/notifications/index.ts +16 -0
  405. package/src/electron/notifications/service.ts +161 -0
  406. package/src/electron/notifications/store.ts +163 -0
  407. package/src/electron/preload.ts +2845 -0
  408. package/src/electron/reports/StandupReportService.ts +356 -0
  409. package/src/electron/security/concurrency.ts +333 -0
  410. package/src/electron/security/index.ts +17 -0
  411. package/src/electron/security/policy-manager.ts +539 -0
  412. package/src/electron/settings/appearance-manager.ts +182 -0
  413. package/src/electron/settings/personality-manager.ts +800 -0
  414. package/src/electron/settings/x-manager.ts +62 -0
  415. package/src/electron/tailscale/exposure.ts +262 -0
  416. package/src/electron/tailscale/index.ts +34 -0
  417. package/src/electron/tailscale/settings.ts +218 -0
  418. package/src/electron/tailscale/tailscale.ts +379 -0
  419. package/src/electron/tray/QuickInputWindow.ts +609 -0
  420. package/src/electron/tray/TrayManager.ts +1005 -0
  421. package/src/electron/tray/index.ts +6 -0
  422. package/src/electron/updater/index.ts +1 -0
  423. package/src/electron/updater/update-manager.ts +447 -0
  424. package/src/electron/utils/env-migration.ts +203 -0
  425. package/src/electron/utils/process.ts +124 -0
  426. package/src/electron/utils/rate-limiter.ts +130 -0
  427. package/src/electron/utils/validation.ts +493 -0
  428. package/src/electron/utils/x-cli.ts +198 -0
  429. package/src/electron/voice/VoiceService.ts +583 -0
  430. package/src/electron/voice/index.ts +9 -0
  431. package/src/electron/voice/voice-settings-manager.ts +403 -0
  432. package/src/renderer/App.tsx +775 -0
  433. package/src/renderer/components/ActivityFeed.tsx +407 -0
  434. package/src/renderer/components/ActivityFeedItem.tsx +285 -0
  435. package/src/renderer/components/AgentRoleCard.tsx +343 -0
  436. package/src/renderer/components/AgentRoleEditor.tsx +805 -0
  437. package/src/renderer/components/AgentSquadSettings.tsx +295 -0
  438. package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
  439. package/src/renderer/components/AppearanceSettings.tsx +122 -0
  440. package/src/renderer/components/ApprovalDialog.tsx +100 -0
  441. package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
  442. package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
  443. package/src/renderer/components/CanvasPreview.tsx +1189 -0
  444. package/src/renderer/components/CommandOutput.tsx +202 -0
  445. package/src/renderer/components/ContextPolicySettings.tsx +523 -0
  446. package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
  447. package/src/renderer/components/DisclaimerModal.tsx +124 -0
  448. package/src/renderer/components/DiscordSettings.tsx +436 -0
  449. package/src/renderer/components/EmailSettings.tsx +606 -0
  450. package/src/renderer/components/ExtensionsSettings.tsx +542 -0
  451. package/src/renderer/components/FileViewer.tsx +224 -0
  452. package/src/renderer/components/GoogleChatSettings.tsx +535 -0
  453. package/src/renderer/components/GuardrailSettings.tsx +487 -0
  454. package/src/renderer/components/HooksSettings.tsx +581 -0
  455. package/src/renderer/components/ImessageSettings.tsx +484 -0
  456. package/src/renderer/components/LineSettings.tsx +483 -0
  457. package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
  458. package/src/renderer/components/MCPSettings.tsx +943 -0
  459. package/src/renderer/components/MainContent.tsx +2433 -0
  460. package/src/renderer/components/MatrixSettings.tsx +510 -0
  461. package/src/renderer/components/MattermostSettings.tsx +473 -0
  462. package/src/renderer/components/MemorySettings.tsx +247 -0
  463. package/src/renderer/components/MentionBadge.tsx +87 -0
  464. package/src/renderer/components/MentionInput.tsx +409 -0
  465. package/src/renderer/components/MentionList.tsx +476 -0
  466. package/src/renderer/components/MissionControlPanel.tsx +1995 -0
  467. package/src/renderer/components/NodesSettings.tsx +316 -0
  468. package/src/renderer/components/NotificationPanel.tsx +481 -0
  469. package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
  470. package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
  471. package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
  472. package/src/renderer/components/Onboarding/index.ts +3 -0
  473. package/src/renderer/components/OnboardingModal.tsx +698 -0
  474. package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
  475. package/src/renderer/components/PersonalitySettings.tsx +597 -0
  476. package/src/renderer/components/QueueSettings.tsx +119 -0
  477. package/src/renderer/components/QuickTaskFAB.tsx +71 -0
  478. package/src/renderer/components/RightPanel.tsx +413 -0
  479. package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
  480. package/src/renderer/components/SearchSettings.tsx +328 -0
  481. package/src/renderer/components/Settings.tsx +1504 -0
  482. package/src/renderer/components/Sidebar.tsx +344 -0
  483. package/src/renderer/components/SignalSettings.tsx +673 -0
  484. package/src/renderer/components/SkillHubBrowser.tsx +458 -0
  485. package/src/renderer/components/SkillParameterModal.tsx +185 -0
  486. package/src/renderer/components/SkillsSettings.tsx +451 -0
  487. package/src/renderer/components/SlackSettings.tsx +442 -0
  488. package/src/renderer/components/StandupReportViewer.tsx +614 -0
  489. package/src/renderer/components/TaskBoard.tsx +498 -0
  490. package/src/renderer/components/TaskBoardCard.tsx +357 -0
  491. package/src/renderer/components/TaskBoardColumn.tsx +211 -0
  492. package/src/renderer/components/TaskLabelManager.tsx +472 -0
  493. package/src/renderer/components/TaskQueuePanel.tsx +144 -0
  494. package/src/renderer/components/TaskQuickActions.tsx +492 -0
  495. package/src/renderer/components/TaskTimeline.tsx +216 -0
  496. package/src/renderer/components/TaskView.tsx +162 -0
  497. package/src/renderer/components/TeamsSettings.tsx +518 -0
  498. package/src/renderer/components/TelegramSettings.tsx +421 -0
  499. package/src/renderer/components/Toast.tsx +76 -0
  500. package/src/renderer/components/TraySettings.tsx +189 -0
  501. package/src/renderer/components/TwitchSettings.tsx +511 -0
  502. package/src/renderer/components/UpdateSettings.tsx +295 -0
  503. package/src/renderer/components/VoiceIndicator.tsx +270 -0
  504. package/src/renderer/components/VoiceSettings.tsx +867 -0
  505. package/src/renderer/components/WhatsAppSettings.tsx +721 -0
  506. package/src/renderer/components/WorkingStateEditor.tsx +309 -0
  507. package/src/renderer/components/WorkingStateHistory.tsx +481 -0
  508. package/src/renderer/components/WorkspaceSelector.tsx +150 -0
  509. package/src/renderer/components/XSettings.tsx +311 -0
  510. package/src/renderer/global.d.ts +9 -0
  511. package/src/renderer/hooks/useAgentContext.ts +153 -0
  512. package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
  513. package/src/renderer/hooks/useVoiceInput.ts +268 -0
  514. package/src/renderer/index.html +12 -0
  515. package/src/renderer/main.tsx +10 -0
  516. package/src/renderer/public/cowork-os-logo.png +0 -0
  517. package/src/renderer/quick-input.html +164 -0
  518. package/src/renderer/styles/index.css +14504 -0
  519. package/src/renderer/utils/agentMessages.ts +749 -0
  520. package/src/renderer/utils/voice-directives.ts +169 -0
  521. package/src/shared/channelMessages.ts +213 -0
  522. package/src/shared/types.ts +3608 -0
  523. package/tsconfig.electron.json +26 -0
  524. package/tsconfig.json +26 -0
  525. package/tsconfig.node.json +10 -0
  526. package/vite.config.ts +23 -0
@@ -0,0 +1,1727 @@
1
+ /**
2
+ * Telegram Channel Adapter
3
+ *
4
+ * Implements the ChannelAdapter interface using grammY for Telegram Bot API.
5
+ * Supports both polling and webhook modes.
6
+ *
7
+ * Features:
8
+ * - API throttling to prevent rate limits
9
+ * - Message deduplication to prevent double processing
10
+ * - Text fragment assembly for split long messages
11
+ * - ACK reactions while processing
12
+ * - Draft streaming for real-time response preview
13
+ * - Sequential message processing to prevent race conditions
14
+ * - Connection conflict detection (409 errors)
15
+ * - Exponential backoff with jitter for error recovery
16
+ * - Health check endpoint for webhook mode
17
+ */
18
+
19
+ import { Bot, Context, webhookCallback, InputFile, GrammyError, HttpError, InlineKeyboard } from 'grammy';
20
+ import { sequentialize } from '@grammyjs/runner';
21
+ import { apiThrottler } from '@grammyjs/transformer-throttler';
22
+ import * as fs from 'fs';
23
+ import * as path from 'path';
24
+ import * as http from 'http';
25
+ import {
26
+ ChannelAdapter,
27
+ ChannelStatus,
28
+ IncomingMessage,
29
+ OutgoingMessage,
30
+ MessageHandler,
31
+ ErrorHandler,
32
+ StatusHandler,
33
+ ChannelInfo,
34
+ TelegramConfig,
35
+ MessageAttachment,
36
+ CallbackQuery,
37
+ CallbackQueryHandler,
38
+ InlineKeyboardButton,
39
+ Poll,
40
+ ReplyKeyboard,
41
+ } from './types';
42
+
43
+ /**
44
+ * Exponential backoff configuration
45
+ */
46
+ export interface BackoffConfig {
47
+ /** Initial delay in ms (default: 2000) */
48
+ initialDelay?: number;
49
+ /** Maximum delay in ms (default: 30000) */
50
+ maxDelay?: number;
51
+ /** Backoff multiplier (default: 1.8) */
52
+ multiplier?: number;
53
+ /** Jitter percentage 0-1 (default: 0.25) */
54
+ jitter?: number;
55
+ /** Maximum retry attempts before giving up (default: 10) */
56
+ maxAttempts?: number;
57
+ }
58
+
59
+ /**
60
+ * Webhook server configuration
61
+ */
62
+ export interface WebhookServerConfig {
63
+ /** Port to listen on */
64
+ port: number;
65
+ /** Host to bind to (default: '0.0.0.0') */
66
+ host?: string;
67
+ /** Secret token for webhook validation */
68
+ secretToken?: string;
69
+ /** Path for webhook endpoint (default: '/webhook') */
70
+ webhookPath?: string;
71
+ /** Path for health check (default: '/healthz') */
72
+ healthPath?: string;
73
+ }
74
+
75
+ /**
76
+ * Extended Telegram configuration with new features
77
+ */
78
+ export interface TelegramAdapterConfig extends TelegramConfig {
79
+ /** Enable ACK reaction (👀) while processing messages */
80
+ ackReactionEnabled?: boolean;
81
+ /** Enable draft streaming for real-time response preview */
82
+ draftStreamingEnabled?: boolean;
83
+ /** Text fragment assembly timeout in ms (default: 1500) */
84
+ fragmentAssemblyTimeout?: number;
85
+ /** Enable message deduplication (default: true) */
86
+ deduplicationEnabled?: boolean;
87
+ /** Enable sequential message processing (default: true) */
88
+ sequentialProcessingEnabled?: boolean;
89
+ /** Exponential backoff configuration */
90
+ backoff?: BackoffConfig;
91
+ /** Webhook server configuration (if using webhook mode) */
92
+ webhookServer?: WebhookServerConfig;
93
+ }
94
+
95
+ /**
96
+ * Pending text fragment for assembly
97
+ */
98
+ interface TextFragment {
99
+ chatId: string;
100
+ userId: string;
101
+ messages: Array<{
102
+ messageId: string;
103
+ text: string;
104
+ timestamp: Date;
105
+ ctx: Context;
106
+ }>;
107
+ timer: ReturnType<typeof setTimeout>;
108
+ }
109
+
110
+ /**
111
+ * Draft message state for streaming
112
+ */
113
+ interface DraftState {
114
+ chatId: string;
115
+ messageId?: string;
116
+ currentText: string;
117
+ lastUpdateTime: number;
118
+ }
119
+
120
+ export class TelegramAdapter implements ChannelAdapter {
121
+ readonly type = 'telegram' as const;
122
+
123
+ private bot: Bot | null = null;
124
+ private _status: ChannelStatus = 'disconnected';
125
+ private _botUsername?: string;
126
+ private messageHandlers: MessageHandler[] = [];
127
+ private errorHandlers: ErrorHandler[] = [];
128
+ private statusHandlers: StatusHandler[] = [];
129
+ private callbackQueryHandlers: CallbackQueryHandler[] = [];
130
+ private config: TelegramAdapterConfig;
131
+
132
+ // Message deduplication: track processed update IDs
133
+ private processedUpdates: Map<number, number> = new Map(); // updateId -> timestamp
134
+ private readonly DEDUP_CACHE_TTL = 60000; // 1 minute
135
+ private readonly DEDUP_CACHE_MAX_SIZE = 1000;
136
+ private dedupCleanupTimer?: ReturnType<typeof setTimeout>;
137
+
138
+ // Text fragment assembly: buffer split messages
139
+ private pendingFragments: Map<string, TextFragment> = new Map(); // chatId:userId -> fragment
140
+ private readonly DEFAULT_FRAGMENT_TIMEOUT = 1500; // 1.5 seconds
141
+
142
+ // Draft streaming state
143
+ private draftStates: Map<string, DraftState> = new Map(); // chatId -> draft state
144
+ private readonly DRAFT_UPDATE_INTERVAL = 500; // Update draft every 500ms
145
+
146
+ // Exponential backoff state
147
+ private backoffAttempt = 0;
148
+ private backoffTimer?: ReturnType<typeof setTimeout>;
149
+ private isReconnecting = false;
150
+
151
+ // Webhook server
152
+ private webhookServer?: http.Server;
153
+
154
+ // Default backoff configuration
155
+ private readonly DEFAULT_BACKOFF: Required<BackoffConfig> = {
156
+ initialDelay: 2000,
157
+ maxDelay: 30000,
158
+ multiplier: 1.8,
159
+ jitter: 0.25,
160
+ maxAttempts: 10,
161
+ };
162
+
163
+ constructor(config: TelegramAdapterConfig) {
164
+ this.config = {
165
+ deduplicationEnabled: true,
166
+ ackReactionEnabled: true,
167
+ draftStreamingEnabled: true,
168
+ fragmentAssemblyTimeout: 1500,
169
+ sequentialProcessingEnabled: true,
170
+ ...config,
171
+ };
172
+ }
173
+
174
+ get status(): ChannelStatus {
175
+ return this._status;
176
+ }
177
+
178
+ get botUsername(): string | undefined {
179
+ return this._botUsername;
180
+ }
181
+
182
+ /**
183
+ * Connect to Telegram using long polling
184
+ */
185
+ async connect(): Promise<void> {
186
+ if (this._status === 'connected' || this._status === 'connecting') {
187
+ return;
188
+ }
189
+
190
+ this.setStatus('connecting');
191
+ this.resetBackoff();
192
+
193
+ try {
194
+ // Create bot instance
195
+ this.bot = new Bot(this.config.botToken);
196
+
197
+ // Add API throttling to prevent rate limits
198
+ const throttler = apiThrottler();
199
+ this.bot.api.config.use(throttler);
200
+
201
+ // Add sequential processing to prevent race conditions
202
+ if (this.config.sequentialProcessingEnabled) {
203
+ this.bot.use(sequentialize(this.getSequentialKey));
204
+ }
205
+
206
+ // Get bot info
207
+ const me = await this.bot.api.getMe();
208
+ this._botUsername = me.username;
209
+
210
+ // Register expanded bot commands for the "/" menu
211
+ await this.registerBotCommands();
212
+
213
+ // Set up message handler with deduplication and fragment assembly
214
+ this.bot.on('message:text', async (ctx) => {
215
+ await this.handleTextMessage(ctx);
216
+ });
217
+
218
+ // Set up callback query handler for inline keyboards
219
+ this.bot.on('callback_query:data', async (ctx) => {
220
+ await this.handleCallbackQuery(ctx);
221
+ });
222
+
223
+ // Handle errors with 409 detection and backoff
224
+ this.bot.catch(async (err) => {
225
+ await this.handleBotError(err);
226
+ });
227
+
228
+ // Start deduplication cleanup timer
229
+ if (this.config.deduplicationEnabled) {
230
+ this.startDedupCleanup();
231
+ }
232
+
233
+ // Start polling with error handling
234
+ await this.startPolling();
235
+ } catch (error) {
236
+ const err = error instanceof Error ? error : new Error(String(error));
237
+
238
+ // Check for connection conflict (409) during initial connection
239
+ if (this.isConnectionConflictError(error)) {
240
+ console.error('Connection conflict detected: Another bot instance is running');
241
+ this.setStatus('error', new Error('Connection conflict: Another bot instance is running. Stop the other instance first.'));
242
+ throw new Error('Connection conflict: Another bot instance is running');
243
+ }
244
+
245
+ this.setStatus('error', err);
246
+ throw err;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Get sequential key for message ordering
252
+ * Messages from the same chat are processed sequentially
253
+ */
254
+ private getSequentialKey = (ctx: Context): string | undefined => {
255
+ const chatId = ctx.chat?.id;
256
+ if (!chatId) return undefined;
257
+
258
+ // Use chat ID + thread ID for forum topics
259
+ const threadId = ctx.message?.message_thread_id;
260
+ if (threadId) {
261
+ return `${chatId}:${threadId}`;
262
+ }
263
+
264
+ return String(chatId);
265
+ };
266
+
267
+ /**
268
+ * Register bot commands for the "/" menu
269
+ */
270
+ private async registerBotCommands(): Promise<void> {
271
+ if (!this.bot) return;
272
+
273
+ await this.bot.api.setMyCommands([
274
+ // Core commands
275
+ { command: 'start', description: 'Start the bot and see welcome message' },
276
+ { command: 'help', description: 'Show all available commands' },
277
+ { command: 'status', description: 'Check bot connection and system status' },
278
+
279
+ // Workspace management
280
+ { command: 'workspaces', description: 'List all available workspaces' },
281
+ { command: 'workspace', description: 'Select or show current workspace' },
282
+ { command: 'addworkspace', description: 'Add a new workspace by path' },
283
+ { command: 'removeworkspace', description: 'Remove a workspace from the list' },
284
+
285
+ // Task management
286
+ { command: 'newtask', description: 'Start a fresh task/conversation' },
287
+ { command: 'cancel', description: 'Cancel the current running task' },
288
+ { command: 'retry', description: 'Retry the last failed task' },
289
+ { command: 'history', description: 'Show recent task history' },
290
+
291
+ // Model configuration
292
+ { command: 'provider', description: 'Change or show current LLM provider' },
293
+ { command: 'providers', description: 'List all available LLM providers' },
294
+ { command: 'model', description: 'Change or show current model' },
295
+ { command: 'models', description: 'List available AI models' },
296
+
297
+ // Skills management
298
+ { command: 'skills', description: 'List available skills' },
299
+ { command: 'skill', description: 'Enable or disable a skill' },
300
+
301
+ // Settings
302
+ { command: 'settings', description: 'View and modify bot settings' },
303
+ { command: 'debug', description: 'Toggle debug mode on/off' },
304
+ { command: 'version', description: 'Show bot version information' },
305
+ ]);
306
+ }
307
+
308
+ /**
309
+ * Start polling with error handling and reconnection
310
+ */
311
+ private async startPolling(): Promise<void> {
312
+ if (!this.bot) return;
313
+
314
+ this.bot.start({
315
+ onStart: () => {
316
+ console.log(`Telegram bot @${this._botUsername} started`);
317
+ this.setStatus('connected');
318
+ this.resetBackoff();
319
+ },
320
+ drop_pending_updates: true,
321
+ allowed_updates: ['message', 'message_reaction', 'callback_query'] as const,
322
+ });
323
+ }
324
+
325
+ /**
326
+ * Handle bot errors including 409 conflict detection
327
+ */
328
+ private async handleBotError(err: unknown): Promise<void> {
329
+ console.error('Telegram bot error:', err);
330
+
331
+ // Check for connection conflict (409)
332
+ if (this.isConnectionConflictError(err)) {
333
+ console.error('Connection conflict detected (409): Another bot instance may be running');
334
+ this.setStatus('error', new Error('Connection conflict: Another bot instance is running'));
335
+
336
+ // Don't reconnect on 409 - let the user resolve the conflict
337
+ this.handleError(new Error('Connection conflict: Another bot instance is running. Stop the other instance and restart.'), 'connection_conflict');
338
+ return;
339
+ }
340
+
341
+ // Check for network errors that warrant reconnection
342
+ if (this.isNetworkError(err)) {
343
+ console.log('Network error detected, will attempt reconnection with backoff');
344
+ await this.attemptReconnection();
345
+ return;
346
+ }
347
+
348
+ // Handle other errors normally
349
+ this.handleError(err instanceof Error ? err : new Error(String(err)), 'bot.catch');
350
+ }
351
+
352
+ /**
353
+ * Check if error is a connection conflict (409)
354
+ */
355
+ private isConnectionConflictError(err: unknown): boolean {
356
+ if (err instanceof GrammyError) {
357
+ return err.error_code === 409;
358
+ }
359
+ if (err instanceof HttpError) {
360
+ return (err as HttpError & { status?: number }).status === 409;
361
+ }
362
+ // Check error message for 409 indicators
363
+ const message = err instanceof Error ? err.message : String(err);
364
+ return message.includes('409') || message.includes('Conflict') || message.includes('terminated by other getUpdates');
365
+ }
366
+
367
+ /**
368
+ * Check if error is a network error that warrants reconnection
369
+ */
370
+ private isNetworkError(err: unknown): boolean {
371
+ if (err instanceof HttpError) {
372
+ return true;
373
+ }
374
+ const message = err instanceof Error ? err.message : String(err);
375
+ return (
376
+ message.includes('ECONNRESET') ||
377
+ message.includes('ETIMEDOUT') ||
378
+ message.includes('ENOTFOUND') ||
379
+ message.includes('network') ||
380
+ message.includes('socket') ||
381
+ message.includes('502') ||
382
+ message.includes('503') ||
383
+ message.includes('504')
384
+ );
385
+ }
386
+
387
+ /**
388
+ * Attempt reconnection with exponential backoff
389
+ */
390
+ private async attemptReconnection(): Promise<void> {
391
+ if (this.isReconnecting) {
392
+ console.log('Reconnection already in progress');
393
+ return;
394
+ }
395
+
396
+ const backoffConfig = { ...this.DEFAULT_BACKOFF, ...this.config.backoff };
397
+
398
+ if (this.backoffAttempt >= backoffConfig.maxAttempts) {
399
+ console.error(`Max reconnection attempts (${backoffConfig.maxAttempts}) reached`);
400
+ this.setStatus('error', new Error('Max reconnection attempts reached'));
401
+ this.handleError(new Error('Failed to reconnect after maximum attempts'), 'reconnection_failed');
402
+ return;
403
+ }
404
+
405
+ this.isReconnecting = true;
406
+ this.backoffAttempt++;
407
+
408
+ const delay = this.calculateBackoffDelay(backoffConfig);
409
+ console.log(`Reconnection attempt ${this.backoffAttempt}/${backoffConfig.maxAttempts} in ${delay}ms`);
410
+
411
+ this.backoffTimer = setTimeout(async () => {
412
+ try {
413
+ // Stop existing bot if any
414
+ if (this.bot) {
415
+ await this.bot.stop();
416
+ this.bot = null;
417
+ }
418
+
419
+ this.isReconnecting = false;
420
+ this.setStatus('disconnected');
421
+
422
+ // Attempt to reconnect
423
+ await this.connect();
424
+ } catch (error) {
425
+ this.isReconnecting = false;
426
+ console.error('Reconnection attempt failed:', error);
427
+
428
+ // Schedule next attempt if not a 409 conflict
429
+ if (!this.isConnectionConflictError(error)) {
430
+ await this.attemptReconnection();
431
+ }
432
+ }
433
+ }, delay);
434
+ }
435
+
436
+ /**
437
+ * Calculate backoff delay with jitter
438
+ */
439
+ private calculateBackoffDelay(config: Required<BackoffConfig>): number {
440
+ // Calculate base delay: initialDelay * multiplier^attempt
441
+ let delay = config.initialDelay * Math.pow(config.multiplier, this.backoffAttempt - 1);
442
+
443
+ // Cap at max delay
444
+ delay = Math.min(delay, config.maxDelay);
445
+
446
+ // Add jitter: delay ± (delay * jitter * random)
447
+ const jitterAmount = delay * config.jitter;
448
+ const jitter = (Math.random() * 2 - 1) * jitterAmount; // Random between -jitterAmount and +jitterAmount
449
+ delay = Math.round(delay + jitter);
450
+
451
+ // Ensure minimum delay of 1 second
452
+ return Math.max(1000, delay);
453
+ }
454
+
455
+ /**
456
+ * Reset backoff state
457
+ */
458
+ private resetBackoff(): void {
459
+ this.backoffAttempt = 0;
460
+ this.isReconnecting = false;
461
+ if (this.backoffTimer) {
462
+ clearTimeout(this.backoffTimer);
463
+ this.backoffTimer = undefined;
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Handle incoming text message with deduplication and fragment assembly
469
+ */
470
+ private async handleTextMessage(ctx: Context): Promise<void> {
471
+ const msg = ctx.message!;
472
+ const updateId = ctx.update.update_id;
473
+
474
+ // Feature 4: Message deduplication - check if already processed
475
+ if (this.config.deduplicationEnabled && this.isUpdateProcessed(updateId)) {
476
+ console.log(`Skipping duplicate update ${updateId}`);
477
+ return;
478
+ }
479
+
480
+ // Mark update as processed
481
+ if (this.config.deduplicationEnabled) {
482
+ this.markUpdateProcessed(updateId);
483
+ }
484
+
485
+ // Feature 3: Text fragment assembly - buffer split messages
486
+ const fragmentKey = `${msg.chat.id}:${msg.from!.id}`;
487
+ const existingFragment = this.pendingFragments.get(fragmentKey);
488
+
489
+ if (existingFragment) {
490
+ // Add to existing fragment
491
+ clearTimeout(existingFragment.timer);
492
+ existingFragment.messages.push({
493
+ messageId: msg.message_id.toString(),
494
+ text: msg.text || '',
495
+ timestamp: new Date(msg.date * 1000),
496
+ ctx,
497
+ });
498
+
499
+ // Reset timer
500
+ existingFragment.timer = setTimeout(() => {
501
+ this.processFragments(fragmentKey);
502
+ }, this.config.fragmentAssemblyTimeout || this.DEFAULT_FRAGMENT_TIMEOUT);
503
+ } else {
504
+ // Check if this might be a split message (long text arriving in chunks)
505
+ // Telegram splits messages at ~4096 chars, so check if message ends mid-sentence
506
+ const mightBeSplit = this.mightBeSplitMessage(msg.text || '');
507
+
508
+ if (mightBeSplit) {
509
+ // Start new fragment buffer
510
+ const timer = setTimeout(() => {
511
+ this.processFragments(fragmentKey);
512
+ }, this.config.fragmentAssemblyTimeout || this.DEFAULT_FRAGMENT_TIMEOUT);
513
+
514
+ this.pendingFragments.set(fragmentKey, {
515
+ chatId: msg.chat.id.toString(),
516
+ userId: msg.from!.id.toString(),
517
+ messages: [{
518
+ messageId: msg.message_id.toString(),
519
+ text: msg.text || '',
520
+ timestamp: new Date(msg.date * 1000),
521
+ ctx,
522
+ }],
523
+ timer,
524
+ });
525
+ } else {
526
+ // Process immediately (single message)
527
+ await this.processMessage(ctx);
528
+ }
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Check if a message might be part of a split message
534
+ */
535
+ private mightBeSplitMessage(text: string): boolean {
536
+ // Messages near Telegram's limit or ending abruptly might be split
537
+ if (text.length >= 4000) return true;
538
+
539
+ // Check if text ends mid-sentence (no terminal punctuation)
540
+ const trimmed = text.trim();
541
+ if (trimmed.length > 100) {
542
+ const lastChar = trimmed.charAt(trimmed.length - 1);
543
+ const terminalPunctuation = ['.', '!', '?', ')', ']', '}', '"', "'", '`'];
544
+ if (!terminalPunctuation.includes(lastChar)) {
545
+ return true;
546
+ }
547
+ }
548
+
549
+ return false;
550
+ }
551
+
552
+ /**
553
+ * Process assembled fragments
554
+ */
555
+ private async processFragments(fragmentKey: string): Promise<void> {
556
+ const fragment = this.pendingFragments.get(fragmentKey);
557
+ if (!fragment) return;
558
+
559
+ this.pendingFragments.delete(fragmentKey);
560
+
561
+ if (fragment.messages.length === 1) {
562
+ // Single message, process normally
563
+ await this.processMessage(fragment.messages[0].ctx);
564
+ } else {
565
+ // Multiple messages, combine them
566
+ const combinedText = fragment.messages
567
+ .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
568
+ .map(m => m.text)
569
+ .join('');
570
+
571
+ // Use the first message's context but with combined text
572
+ const firstCtx = fragment.messages[0].ctx;
573
+ const message = this.mapContextToMessage(firstCtx, combinedText);
574
+
575
+ console.log(`Assembled ${fragment.messages.length} text fragments into single message (${combinedText.length} chars)`);
576
+
577
+ await this.handleIncomingMessage(message);
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Process a single message (with ACK reaction)
583
+ */
584
+ private async processMessage(ctx: Context): Promise<void> {
585
+ const message = this.mapContextToMessage(ctx);
586
+
587
+ // Feature 2: Send ACK reaction (👀) while processing
588
+ if (this.config.ackReactionEnabled) {
589
+ try {
590
+ await this.sendAckReaction(ctx);
591
+ } catch (err) {
592
+ // Ignore reaction errors (might not have permission)
593
+ console.debug('Could not send ACK reaction:', err);
594
+ }
595
+ }
596
+
597
+ await this.handleIncomingMessage(message);
598
+ }
599
+
600
+ /**
601
+ * Send ACK reaction (👀) to indicate message received
602
+ */
603
+ private async sendAckReaction(ctx: Context): Promise<void> {
604
+ if (!this.bot || !ctx.message) return;
605
+
606
+ try {
607
+ await this.bot.api.setMessageReaction(
608
+ ctx.message.chat.id,
609
+ ctx.message.message_id,
610
+ [{ type: 'emoji', emoji: '👀' }]
611
+ );
612
+ } catch {
613
+ // Silently fail - reactions might not be available
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Remove ACK reaction after processing
619
+ */
620
+ async removeAckReaction(chatId: string, messageId: string): Promise<void> {
621
+ if (!this.bot) return;
622
+
623
+ try {
624
+ await this.bot.api.setMessageReaction(
625
+ chatId,
626
+ parseInt(messageId, 10),
627
+ [] // Empty array removes reactions
628
+ );
629
+ } catch {
630
+ // Silently fail
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Send a completion reaction when done
636
+ * Note: Telegram only allows specific reaction emojis, using 👍 for completion
637
+ */
638
+ async sendCompletionReaction(chatId: string, messageId: string): Promise<void> {
639
+ if (!this.bot) return;
640
+
641
+ try {
642
+ await this.bot.api.setMessageReaction(
643
+ chatId,
644
+ parseInt(messageId, 10),
645
+ [{ type: 'emoji', emoji: '👍' }]
646
+ );
647
+ } catch {
648
+ // Silently fail
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Check if update was already processed (deduplication)
654
+ */
655
+ private isUpdateProcessed(updateId: number): boolean {
656
+ return this.processedUpdates.has(updateId);
657
+ }
658
+
659
+ /**
660
+ * Mark update as processed
661
+ */
662
+ private markUpdateProcessed(updateId: number): void {
663
+ this.processedUpdates.set(updateId, Date.now());
664
+
665
+ // Prevent unbounded growth
666
+ if (this.processedUpdates.size > this.DEDUP_CACHE_MAX_SIZE) {
667
+ this.cleanupDedupCache();
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Start periodic cleanup of dedup cache
673
+ */
674
+ private startDedupCleanup(): void {
675
+ this.dedupCleanupTimer = setInterval(() => {
676
+ this.cleanupDedupCache();
677
+ }, this.DEDUP_CACHE_TTL);
678
+ }
679
+
680
+ /**
681
+ * Clean up old entries from dedup cache
682
+ */
683
+ private cleanupDedupCache(): void {
684
+ const now = Date.now();
685
+ for (const [updateId, timestamp] of this.processedUpdates) {
686
+ if (now - timestamp > this.DEDUP_CACHE_TTL) {
687
+ this.processedUpdates.delete(updateId);
688
+ }
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Disconnect from Telegram
694
+ */
695
+ async disconnect(): Promise<void> {
696
+ // Reset backoff state
697
+ this.resetBackoff();
698
+
699
+ // Clear timers
700
+ if (this.dedupCleanupTimer) {
701
+ clearInterval(this.dedupCleanupTimer);
702
+ this.dedupCleanupTimer = undefined;
703
+ }
704
+
705
+ // Clear pending fragments
706
+ for (const fragment of this.pendingFragments.values()) {
707
+ clearTimeout(fragment.timer);
708
+ }
709
+ this.pendingFragments.clear();
710
+
711
+ // Clear draft states
712
+ this.draftStates.clear();
713
+
714
+ // Clear dedup cache
715
+ this.processedUpdates.clear();
716
+
717
+ // Stop webhook server if running
718
+ if (this.webhookServer) {
719
+ await this.stopWebhookServer();
720
+ }
721
+
722
+ if (this.bot) {
723
+ await this.bot.stop();
724
+ this.bot = null;
725
+ }
726
+ this._botUsername = undefined;
727
+ this.setStatus('disconnected');
728
+ }
729
+
730
+ /**
731
+ * Send a message to a Telegram chat
732
+ */
733
+ async sendMessage(message: OutgoingMessage): Promise<string> {
734
+ if (!this.bot || this._status !== 'connected') {
735
+ throw new Error('Telegram bot is not connected');
736
+ }
737
+
738
+ // Handle image attachments first (send images before text)
739
+ let lastMessageId: string | undefined;
740
+ if (message.attachments && message.attachments.length > 0) {
741
+ for (const attachment of message.attachments) {
742
+ if (attachment.type === 'image' && attachment.url) {
743
+ try {
744
+ // attachment.url is the file path for local images
745
+ const msgId = await this.sendPhoto(message.chatId, attachment.url);
746
+ lastMessageId = msgId;
747
+ } catch (err) {
748
+ console.error('Failed to send image attachment:', err);
749
+ }
750
+ }
751
+ }
752
+ }
753
+
754
+ // If we have text to send, send it
755
+ if (message.text && message.text.trim()) {
756
+ // Process text for Telegram compatibility
757
+ let processedText = message.text;
758
+ if (message.parseMode === 'markdown') {
759
+ processedText = this.convertMarkdownForTelegram(message.text);
760
+ }
761
+
762
+ const options: Record<string, unknown> = {};
763
+
764
+ // Set parse mode
765
+ // Use legacy Markdown (not MarkdownV2) to avoid escaping issues with special characters
766
+ if (message.parseMode === 'markdown') {
767
+ options.parse_mode = 'Markdown';
768
+ } else if (message.parseMode === 'html') {
769
+ options.parse_mode = 'HTML';
770
+ }
771
+
772
+ // Reply to message if specified
773
+ if (message.replyTo) {
774
+ options.reply_to_message_id = parseInt(message.replyTo, 10);
775
+ }
776
+
777
+ // Forum topic thread support
778
+ if (message.threadId) {
779
+ options.message_thread_id = parseInt(message.threadId, 10);
780
+ }
781
+
782
+ // Link preview control
783
+ if (message.disableLinkPreview) {
784
+ options.link_preview_options = { is_disabled: true };
785
+ }
786
+
787
+ // Inline keyboard support
788
+ if (message.inlineKeyboard && message.inlineKeyboard.length > 0) {
789
+ options.reply_markup = this.buildInlineKeyboard(message.inlineKeyboard);
790
+ }
791
+
792
+ try {
793
+ const sent = await this.bot.api.sendMessage(message.chatId, processedText, options);
794
+ return sent.message_id.toString();
795
+ } catch (error: any) {
796
+ // If markdown parsing fails, retry without parse_mode
797
+ if (error?.error_code === 400 && error?.description?.includes("can't parse entities")) {
798
+ console.log('Markdown parsing failed, retrying without parse_mode');
799
+ const plainOptions: Record<string, unknown> = {
800
+ ...(message.threadId && { message_thread_id: parseInt(message.threadId, 10) }),
801
+ ...(message.disableLinkPreview && { link_preview_options: { is_disabled: true } }),
802
+ ...(message.inlineKeyboard && { reply_markup: this.buildInlineKeyboard(message.inlineKeyboard) }),
803
+ };
804
+ if (message.replyTo) {
805
+ plainOptions.reply_to_message_id = parseInt(message.replyTo, 10);
806
+ }
807
+ const sent = await this.bot.api.sendMessage(message.chatId, message.text, plainOptions);
808
+ return sent.message_id.toString();
809
+ }
810
+ throw error;
811
+ }
812
+ }
813
+
814
+ // If no text but had attachments, return the last attachment message ID
815
+ return lastMessageId || '';
816
+ }
817
+
818
+ /**
819
+ * Build grammY InlineKeyboard from our button format
820
+ */
821
+ private buildInlineKeyboard(buttons: InlineKeyboardButton[][]): InlineKeyboard {
822
+ const keyboard = new InlineKeyboard();
823
+ for (const row of buttons) {
824
+ for (const button of row) {
825
+ if (button.url) {
826
+ keyboard.url(button.text, button.url);
827
+ } else if (button.callbackData) {
828
+ keyboard.text(button.text, button.callbackData);
829
+ }
830
+ }
831
+ keyboard.row();
832
+ }
833
+ return keyboard;
834
+ }
835
+
836
+ /**
837
+ * Handle incoming callback query from inline keyboard button press
838
+ */
839
+ private async handleCallbackQuery(ctx: Context): Promise<void> {
840
+ const query = ctx.callbackQuery!;
841
+ if (!query.data || !query.message) {
842
+ return;
843
+ }
844
+
845
+ const callbackQuery: CallbackQuery = {
846
+ id: query.id,
847
+ userId: query.from.id.toString(),
848
+ userName: query.from.first_name + (query.from.last_name ? ` ${query.from.last_name}` : ''),
849
+ chatId: query.message.chat.id.toString(),
850
+ messageId: query.message.message_id.toString(),
851
+ data: query.data,
852
+ threadId: (query.message as { message_thread_id?: number }).message_thread_id?.toString(),
853
+ raw: ctx,
854
+ };
855
+
856
+ // Notify all registered handlers
857
+ for (const handler of this.callbackQueryHandlers) {
858
+ try {
859
+ await handler(callbackQuery);
860
+ } catch (error) {
861
+ console.error('Error in callback query handler:', error);
862
+ this.handleError(
863
+ error instanceof Error ? error : new Error(String(error)),
864
+ 'callbackQueryHandler'
865
+ );
866
+ }
867
+ }
868
+ }
869
+
870
+ /**
871
+ * Feature 1: Draft streaming - Start streaming a response
872
+ * Creates or updates a draft message that shows response as it generates
873
+ */
874
+ async startDraftStream(chatId: string): Promise<void> {
875
+ if (!this.config.draftStreamingEnabled) return;
876
+
877
+ this.draftStates.set(chatId, {
878
+ chatId,
879
+ currentText: '',
880
+ lastUpdateTime: Date.now(),
881
+ });
882
+ }
883
+
884
+ /**
885
+ * Update draft stream with new content
886
+ */
887
+ async updateDraftStream(chatId: string, text: string): Promise<void> {
888
+ if (!this.bot || !this.config.draftStreamingEnabled) return;
889
+
890
+ const state = this.draftStates.get(chatId);
891
+ if (!state) return;
892
+
893
+ const now = Date.now();
894
+
895
+ // Throttle updates to prevent API spam
896
+ if (now - state.lastUpdateTime < this.DRAFT_UPDATE_INTERVAL) {
897
+ // Just update the text, don't send yet
898
+ state.currentText = text;
899
+ return;
900
+ }
901
+
902
+ // Add typing indicator suffix
903
+ const displayText = text + ' ▌';
904
+
905
+ try {
906
+ if (state.messageId) {
907
+ // Edit existing message
908
+ await this.bot.api.editMessageText(
909
+ chatId,
910
+ parseInt(state.messageId, 10),
911
+ displayText
912
+ );
913
+ } else {
914
+ // Create new message
915
+ const sent = await this.bot.api.sendMessage(chatId, displayText);
916
+ state.messageId = sent.message_id.toString();
917
+ }
918
+
919
+ state.currentText = text;
920
+ state.lastUpdateTime = now;
921
+ } catch (error: any) {
922
+ // Ignore "message not modified" errors
923
+ if (!error?.description?.includes('message is not modified')) {
924
+ console.error('Draft stream update error:', error);
925
+ }
926
+ }
927
+ }
928
+
929
+ /**
930
+ * Finalize draft stream with final content
931
+ */
932
+ async finalizeDraftStream(chatId: string, finalText: string): Promise<string> {
933
+ if (!this.bot) throw new Error('Bot not connected');
934
+
935
+ const state = this.draftStates.get(chatId);
936
+ this.draftStates.delete(chatId);
937
+
938
+ if (!this.config.draftStreamingEnabled || !state?.messageId) {
939
+ // No draft exists, send as new message
940
+ const sent = await this.bot.api.sendMessage(chatId, finalText);
941
+ return sent.message_id.toString();
942
+ }
943
+
944
+ try {
945
+ // Edit the draft message to final content (remove typing indicator)
946
+ await this.bot.api.editMessageText(
947
+ chatId,
948
+ parseInt(state.messageId, 10),
949
+ finalText
950
+ );
951
+ return state.messageId;
952
+ } catch (error: any) {
953
+ // If edit fails, send as new message
954
+ console.error('Failed to finalize draft, sending new message:', error);
955
+ const sent = await this.bot.api.sendMessage(chatId, finalText);
956
+ return sent.message_id.toString();
957
+ }
958
+ }
959
+
960
+ /**
961
+ * Cancel draft stream (delete the draft message)
962
+ */
963
+ async cancelDraftStream(chatId: string): Promise<void> {
964
+ const state = this.draftStates.get(chatId);
965
+ this.draftStates.delete(chatId);
966
+
967
+ if (state?.messageId && this.bot) {
968
+ try {
969
+ await this.bot.api.deleteMessage(chatId, parseInt(state.messageId, 10));
970
+ } catch {
971
+ // Ignore deletion errors
972
+ }
973
+ }
974
+ }
975
+
976
+ /**
977
+ * Convert GitHub-flavored markdown to Telegram-compatible format
978
+ * Telegram legacy Markdown only supports: *bold*, _italic_, `code`, ```code blocks```, [links](url)
979
+ */
980
+ private convertMarkdownForTelegram(text: string): string {
981
+ let result = text;
982
+
983
+ // Convert markdown headers (## Header) to bold (*Header*)
984
+ // Must be done before ** conversion
985
+ result = result.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');
986
+
987
+ // Convert markdown tables to code blocks
988
+ // Tables start with | and have a separator line like |---|---|
989
+ const tableRegex = /(\|[^\n]+\|\n)+/g;
990
+ const hasSeparatorLine = /\|[\s-:]+\|/;
991
+
992
+ result = result.replace(tableRegex, (match) => {
993
+ // Check if this looks like a table (has separator line with dashes)
994
+ if (hasSeparatorLine.test(match)) {
995
+ // Convert table to code block for monospace display
996
+ // Remove the separator line (|---|---|) as it's just formatting
997
+ const lines = match.split('\n').filter(line => line.trim());
998
+ const cleanedLines = lines.filter(line => !(/^\|[\s-:]+\|$/.test(line.trim())));
999
+
1000
+ // Format table nicely
1001
+ const formattedTable = cleanedLines.map(line => {
1002
+ // Remove leading/trailing pipes and clean up
1003
+ return line.replace(/^\||\|$/g, '').trim();
1004
+ }).join('\n');
1005
+
1006
+ return '```\n' + formattedTable + '\n```\n';
1007
+ }
1008
+ return match;
1009
+ });
1010
+
1011
+ // Convert **bold** to *bold* (Telegram uses single asterisk)
1012
+ result = result.replace(/\*\*([^*]+)\*\*/g, '*$1*');
1013
+
1014
+ // Convert __bold__ to *bold* (alternative bold syntax)
1015
+ result = result.replace(/__([^_]+)__/g, '*$1*');
1016
+
1017
+ // Convert horizontal rules (---, ***) to a line
1018
+ result = result.replace(/^[-*]{3,}$/gm, '─────────────────');
1019
+
1020
+ return result;
1021
+ }
1022
+
1023
+ /**
1024
+ * Edit an existing message
1025
+ */
1026
+ async editMessage(chatId: string, messageId: string, text: string): Promise<void> {
1027
+ if (!this.bot || this._status !== 'connected') {
1028
+ throw new Error('Telegram bot is not connected');
1029
+ }
1030
+
1031
+ const msgId = parseInt(messageId, 10);
1032
+ if (isNaN(msgId)) {
1033
+ throw new Error(`Invalid message ID: ${messageId}`);
1034
+ }
1035
+
1036
+ await this.bot.api.editMessageText(chatId, msgId, text);
1037
+ }
1038
+
1039
+ /**
1040
+ * Delete a message
1041
+ */
1042
+ async deleteMessage(chatId: string, messageId: string): Promise<void> {
1043
+ if (!this.bot || this._status !== 'connected') {
1044
+ throw new Error('Telegram bot is not connected');
1045
+ }
1046
+
1047
+ const msgId = parseInt(messageId, 10);
1048
+ if (isNaN(msgId)) {
1049
+ throw new Error(`Invalid message ID: ${messageId}`);
1050
+ }
1051
+
1052
+ await this.bot.api.deleteMessage(chatId, msgId);
1053
+ }
1054
+
1055
+ /**
1056
+ * Send a document/file to a chat
1057
+ */
1058
+ async sendDocument(chatId: string, filePath: string, caption?: string): Promise<string> {
1059
+ if (!this.bot || this._status !== 'connected') {
1060
+ throw new Error('Telegram bot is not connected');
1061
+ }
1062
+
1063
+ // Check if file exists
1064
+ if (!fs.existsSync(filePath)) {
1065
+ throw new Error(`File not found: ${filePath}`);
1066
+ }
1067
+
1068
+ const fileName = path.basename(filePath);
1069
+ const fileBuffer = fs.readFileSync(filePath);
1070
+
1071
+ const sent = await this.bot.api.sendDocument(
1072
+ chatId,
1073
+ new InputFile(fileBuffer, fileName),
1074
+ { caption }
1075
+ );
1076
+
1077
+ return sent.message_id.toString();
1078
+ }
1079
+
1080
+ /**
1081
+ * Send a photo/image to a chat
1082
+ */
1083
+ async sendPhoto(chatId: string, filePath: string, caption?: string): Promise<string> {
1084
+ if (!this.bot || this._status !== 'connected') {
1085
+ throw new Error('Telegram bot is not connected');
1086
+ }
1087
+
1088
+ // Check if file exists
1089
+ if (!fs.existsSync(filePath)) {
1090
+ throw new Error(`File not found: ${filePath}`);
1091
+ }
1092
+
1093
+ const fileName = path.basename(filePath);
1094
+ const fileBuffer = fs.readFileSync(filePath);
1095
+
1096
+ const sent = await this.bot.api.sendPhoto(
1097
+ chatId,
1098
+ new InputFile(fileBuffer, fileName),
1099
+ { caption }
1100
+ );
1101
+
1102
+ return sent.message_id.toString();
1103
+ }
1104
+
1105
+ /**
1106
+ * Register a message handler
1107
+ */
1108
+ onMessage(handler: MessageHandler): void {
1109
+ this.messageHandlers.push(handler);
1110
+ }
1111
+
1112
+ /**
1113
+ * Register a callback query handler (for inline keyboard buttons)
1114
+ */
1115
+ onCallbackQuery(handler: CallbackQueryHandler): void {
1116
+ this.callbackQueryHandlers.push(handler);
1117
+ }
1118
+
1119
+ /**
1120
+ * Answer a callback query (acknowledge button press)
1121
+ * Call this to remove the loading state from the button.
1122
+ */
1123
+ async answerCallbackQuery(queryId: string, text?: string, showAlert?: boolean): Promise<void> {
1124
+ if (!this.bot || this._status !== 'connected') {
1125
+ throw new Error('Telegram bot is not connected');
1126
+ }
1127
+
1128
+ await this.bot.api.answerCallbackQuery(queryId, {
1129
+ text,
1130
+ show_alert: showAlert,
1131
+ });
1132
+ }
1133
+
1134
+ /**
1135
+ * Edit a message with a new inline keyboard
1136
+ */
1137
+ async editMessageWithKeyboard(
1138
+ chatId: string,
1139
+ messageId: string,
1140
+ text?: string,
1141
+ inlineKeyboard?: InlineKeyboardButton[][]
1142
+ ): Promise<void> {
1143
+ if (!this.bot || this._status !== 'connected') {
1144
+ throw new Error('Telegram bot is not connected');
1145
+ }
1146
+
1147
+ const msgId = parseInt(messageId, 10);
1148
+ if (isNaN(msgId)) {
1149
+ throw new Error(`Invalid message ID: ${messageId}`);
1150
+ }
1151
+
1152
+ const options: Record<string, unknown> = {};
1153
+ if (inlineKeyboard && inlineKeyboard.length > 0) {
1154
+ options.reply_markup = this.buildInlineKeyboard(inlineKeyboard);
1155
+ }
1156
+
1157
+ if (text) {
1158
+ await this.bot.api.editMessageText(chatId, msgId, text, options);
1159
+ } else if (inlineKeyboard) {
1160
+ await this.bot.api.editMessageReplyMarkup(chatId, msgId, options);
1161
+ }
1162
+ }
1163
+
1164
+ // ============================================================================
1165
+ // Extended Features
1166
+ // ============================================================================
1167
+
1168
+ /**
1169
+ * Send typing indicator (chat action)
1170
+ */
1171
+ async sendTyping(chatId: string, threadId?: string): Promise<void> {
1172
+ if (!this.bot || this._status !== 'connected') {
1173
+ throw new Error('Telegram bot is not connected');
1174
+ }
1175
+
1176
+ const options: Record<string, unknown> = {};
1177
+ if (threadId) {
1178
+ options.message_thread_id = parseInt(threadId, 10);
1179
+ }
1180
+
1181
+ await this.bot.api.sendChatAction(chatId, 'typing', options);
1182
+ }
1183
+
1184
+ /**
1185
+ * Add reaction to a message
1186
+ */
1187
+ async addReaction(chatId: string, messageId: string, emoji: string): Promise<void> {
1188
+ if (!this.bot || this._status !== 'connected') {
1189
+ throw new Error('Telegram bot is not connected');
1190
+ }
1191
+
1192
+ const msgId = parseInt(messageId, 10);
1193
+ // Cast emoji to the expected type - Telegram will reject invalid emojis at runtime
1194
+ await this.bot.api.setMessageReaction(chatId, msgId, [{ type: 'emoji', emoji: emoji as '👍' }]);
1195
+ }
1196
+
1197
+ /**
1198
+ * Remove reaction from a message
1199
+ */
1200
+ async removeReaction(chatId: string, messageId: string): Promise<void> {
1201
+ if (!this.bot || this._status !== 'connected') {
1202
+ throw new Error('Telegram bot is not connected');
1203
+ }
1204
+
1205
+ const msgId = parseInt(messageId, 10);
1206
+ await this.bot.api.setMessageReaction(chatId, msgId, []);
1207
+ }
1208
+
1209
+ /**
1210
+ * Send a poll
1211
+ */
1212
+ async sendPoll(chatId: string, poll: Poll, threadId?: string): Promise<string> {
1213
+ if (!this.bot || this._status !== 'connected') {
1214
+ throw new Error('Telegram bot is not connected');
1215
+ }
1216
+
1217
+ const options: Record<string, unknown> = {
1218
+ is_anonymous: poll.isAnonymous ?? true,
1219
+ allows_multiple_answers: poll.allowsMultipleAnswers ?? false,
1220
+ };
1221
+
1222
+ if (threadId) {
1223
+ options.message_thread_id = parseInt(threadId, 10);
1224
+ }
1225
+
1226
+ if (poll.type === 'quiz' && poll.correctOptionId !== undefined) {
1227
+ options.type = 'quiz';
1228
+ options.correct_option_id = poll.correctOptionId;
1229
+ if (poll.explanation) {
1230
+ options.explanation = poll.explanation;
1231
+ }
1232
+ }
1233
+
1234
+ if (poll.openPeriod) {
1235
+ options.open_period = poll.openPeriod;
1236
+ } else if (poll.closeDate) {
1237
+ options.close_date = Math.floor(poll.closeDate.getTime() / 1000);
1238
+ }
1239
+
1240
+ const sent = await this.bot.api.sendPoll(
1241
+ chatId,
1242
+ poll.question,
1243
+ poll.options.map(o => o.text),
1244
+ options
1245
+ );
1246
+
1247
+ return sent.message_id.toString();
1248
+ }
1249
+
1250
+ /**
1251
+ * Send message with reply keyboard (persistent keyboard below input)
1252
+ */
1253
+ async sendWithReplyKeyboard(
1254
+ chatId: string,
1255
+ text: string,
1256
+ keyboard: ReplyKeyboard,
1257
+ threadId?: string
1258
+ ): Promise<string> {
1259
+ if (!this.bot || this._status !== 'connected') {
1260
+ throw new Error('Telegram bot is not connected');
1261
+ }
1262
+
1263
+ const replyMarkup = {
1264
+ keyboard: keyboard.buttons.map(row =>
1265
+ row.map(btn => ({
1266
+ text: btn.text,
1267
+ request_contact: btn.requestContact,
1268
+ request_location: btn.requestLocation,
1269
+ }))
1270
+ ),
1271
+ resize_keyboard: keyboard.resizeKeyboard ?? true,
1272
+ one_time_keyboard: keyboard.oneTimeKeyboard ?? false,
1273
+ input_field_placeholder: keyboard.inputPlaceholder,
1274
+ };
1275
+
1276
+ const options: Record<string, unknown> = {
1277
+ reply_markup: replyMarkup,
1278
+ };
1279
+
1280
+ if (threadId) {
1281
+ options.message_thread_id = parseInt(threadId, 10);
1282
+ }
1283
+
1284
+ const sent = await this.bot.api.sendMessage(chatId, text, options);
1285
+ return sent.message_id.toString();
1286
+ }
1287
+
1288
+ /**
1289
+ * Remove reply keyboard (send message that hides the keyboard)
1290
+ */
1291
+ async removeReplyKeyboard(chatId: string, text: string, threadId?: string): Promise<string> {
1292
+ if (!this.bot || this._status !== 'connected') {
1293
+ throw new Error('Telegram bot is not connected');
1294
+ }
1295
+
1296
+ const options: Record<string, unknown> = {
1297
+ reply_markup: { remove_keyboard: true },
1298
+ };
1299
+
1300
+ if (threadId) {
1301
+ options.message_thread_id = parseInt(threadId, 10);
1302
+ }
1303
+
1304
+ const sent = await this.bot.api.sendMessage(chatId, text, options);
1305
+ return sent.message_id.toString();
1306
+ }
1307
+
1308
+ /**
1309
+ * Send a sticker
1310
+ */
1311
+ async sendSticker(chatId: string, stickerId: string, threadId?: string): Promise<string> {
1312
+ if (!this.bot || this._status !== 'connected') {
1313
+ throw new Error('Telegram bot is not connected');
1314
+ }
1315
+
1316
+ const options: Record<string, unknown> = {};
1317
+ if (threadId) {
1318
+ options.message_thread_id = parseInt(threadId, 10);
1319
+ }
1320
+
1321
+ const sent = await this.bot.api.sendSticker(chatId, stickerId, options);
1322
+ return sent.message_id.toString();
1323
+ }
1324
+
1325
+ /**
1326
+ * Send location
1327
+ */
1328
+ async sendLocation(
1329
+ chatId: string,
1330
+ latitude: number,
1331
+ longitude: number,
1332
+ threadId?: string
1333
+ ): Promise<string> {
1334
+ if (!this.bot || this._status !== 'connected') {
1335
+ throw new Error('Telegram bot is not connected');
1336
+ }
1337
+
1338
+ const options: Record<string, unknown> = {};
1339
+ if (threadId) {
1340
+ options.message_thread_id = parseInt(threadId, 10);
1341
+ }
1342
+
1343
+ const sent = await this.bot.api.sendLocation(chatId, latitude, longitude, options);
1344
+ return sent.message_id.toString();
1345
+ }
1346
+
1347
+ /**
1348
+ * Send a media group (album)
1349
+ */
1350
+ async sendMediaGroup(
1351
+ chatId: string,
1352
+ media: Array<{ type: 'photo' | 'video'; filePath: string; caption?: string }>,
1353
+ threadId?: string
1354
+ ): Promise<string[]> {
1355
+ if (!this.bot || this._status !== 'connected') {
1356
+ throw new Error('Telegram bot is not connected');
1357
+ }
1358
+
1359
+ const inputMedia = media.map((m, index) => {
1360
+ const fileBuffer = fs.readFileSync(m.filePath);
1361
+ const fileName = path.basename(m.filePath);
1362
+ return {
1363
+ type: m.type,
1364
+ media: new InputFile(fileBuffer, fileName),
1365
+ caption: index === 0 ? m.caption : undefined, // Caption on first item only
1366
+ };
1367
+ });
1368
+
1369
+ const options: Record<string, unknown> = {};
1370
+ if (threadId) {
1371
+ options.message_thread_id = parseInt(threadId, 10);
1372
+ }
1373
+
1374
+ const sent = await this.bot.api.sendMediaGroup(chatId, inputMedia as any, options);
1375
+ return sent.map(m => m.message_id.toString());
1376
+ }
1377
+
1378
+ // ============================================================================
1379
+ // Handler Registration
1380
+ // ============================================================================
1381
+
1382
+ /**
1383
+ * Register an error handler
1384
+ */
1385
+ onError(handler: ErrorHandler): void {
1386
+ this.errorHandlers.push(handler);
1387
+ }
1388
+
1389
+ /**
1390
+ * Register a status change handler
1391
+ */
1392
+ onStatusChange(handler: StatusHandler): void {
1393
+ this.statusHandlers.push(handler);
1394
+ }
1395
+
1396
+ /**
1397
+ * Get channel info
1398
+ */
1399
+ async getInfo(): Promise<ChannelInfo> {
1400
+ let botId: string | undefined;
1401
+ let botDisplayName: string | undefined;
1402
+
1403
+ if (this.bot && this._status === 'connected') {
1404
+ try {
1405
+ const me = await this.bot.api.getMe();
1406
+ botId = me.id.toString();
1407
+ botDisplayName = me.first_name;
1408
+ this._botUsername = me.username;
1409
+ } catch {
1410
+ // Ignore errors getting info
1411
+ }
1412
+ }
1413
+
1414
+ return {
1415
+ type: 'telegram',
1416
+ status: this._status,
1417
+ botId,
1418
+ botUsername: this._botUsername,
1419
+ botDisplayName,
1420
+ };
1421
+ }
1422
+
1423
+ /**
1424
+ * Get webhook callback for Express/Fastify/etc.
1425
+ * Use this when running in webhook mode instead of polling.
1426
+ */
1427
+ getWebhookCallback(): (req: Request, res: Response) => Promise<void> {
1428
+ if (!this.bot) {
1429
+ throw new Error('Bot not initialized');
1430
+ }
1431
+ return webhookCallback(this.bot, 'express') as unknown as (req: Request, res: Response) => Promise<void>;
1432
+ }
1433
+
1434
+ /**
1435
+ * Set webhook URL
1436
+ */
1437
+ async setWebhook(url: string, secretToken?: string): Promise<void> {
1438
+ if (!this.bot) {
1439
+ throw new Error('Bot not initialized');
1440
+ }
1441
+
1442
+ await this.bot.api.setWebhook(url, {
1443
+ secret_token: secretToken,
1444
+ allowed_updates: ['message'] as const,
1445
+ });
1446
+ }
1447
+
1448
+ /**
1449
+ * Remove webhook
1450
+ */
1451
+ async deleteWebhook(): Promise<void> {
1452
+ if (!this.bot) {
1453
+ throw new Error('Bot not initialized');
1454
+ }
1455
+
1456
+ await this.bot.api.deleteWebhook();
1457
+ }
1458
+
1459
+ /**
1460
+ * Start webhook server with health check endpoint
1461
+ * This creates an HTTP server that handles both webhook callbacks and health checks.
1462
+ */
1463
+ async startWebhookServer(config: WebhookServerConfig): Promise<void> {
1464
+ if (this.webhookServer) {
1465
+ throw new Error('Webhook server is already running');
1466
+ }
1467
+
1468
+ if (!this.bot) {
1469
+ throw new Error('Bot not initialized. Call connect() first or initialize bot manually.');
1470
+ }
1471
+
1472
+ const {
1473
+ port,
1474
+ host = '0.0.0.0',
1475
+ secretToken,
1476
+ webhookPath = '/webhook',
1477
+ healthPath = '/healthz',
1478
+ } = config;
1479
+
1480
+ // Create HTTP server
1481
+ this.webhookServer = http.createServer(async (req, res) => {
1482
+ const url = req.url || '/';
1483
+
1484
+ // Health check endpoint
1485
+ if (req.method === 'GET' && url === healthPath) {
1486
+ await this.handleHealthCheck(req, res);
1487
+ return;
1488
+ }
1489
+
1490
+ // Webhook endpoint
1491
+ if (req.method === 'POST' && url === webhookPath) {
1492
+ // Validate secret token if configured
1493
+ if (secretToken) {
1494
+ const requestToken = req.headers['x-telegram-bot-api-secret-token'];
1495
+ if (requestToken !== secretToken) {
1496
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1497
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
1498
+ return;
1499
+ }
1500
+ }
1501
+
1502
+ // Handle webhook callback
1503
+ await this.handleWebhookRequest(req, res);
1504
+ return;
1505
+ }
1506
+
1507
+ // 404 for unknown routes
1508
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1509
+ res.end(JSON.stringify({ error: 'Not found' }));
1510
+ });
1511
+
1512
+ // Start listening
1513
+ return new Promise((resolve, reject) => {
1514
+ this.webhookServer!.listen(port, host, () => {
1515
+ console.log(`Telegram webhook server listening on ${host}:${port}`);
1516
+ console.log(` Webhook endpoint: ${webhookPath}`);
1517
+ console.log(` Health check: ${healthPath}`);
1518
+ resolve();
1519
+ });
1520
+
1521
+ this.webhookServer!.on('error', (error) => {
1522
+ console.error('Webhook server error:', error);
1523
+ reject(error);
1524
+ });
1525
+ });
1526
+ }
1527
+
1528
+ /**
1529
+ * Stop the webhook server
1530
+ */
1531
+ async stopWebhookServer(): Promise<void> {
1532
+ if (!this.webhookServer) {
1533
+ return;
1534
+ }
1535
+
1536
+ return new Promise((resolve) => {
1537
+ this.webhookServer!.close(() => {
1538
+ console.log('Webhook server stopped');
1539
+ this.webhookServer = undefined;
1540
+ resolve();
1541
+ });
1542
+ });
1543
+ }
1544
+
1545
+ /**
1546
+ * Handle health check requests
1547
+ */
1548
+ private async handleHealthCheck(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1549
+ const health = {
1550
+ status: this._status === 'connected' ? 'healthy' : 'unhealthy',
1551
+ timestamp: new Date().toISOString(),
1552
+ bot: {
1553
+ status: this._status,
1554
+ username: this._botUsername || null,
1555
+ connected: this._status === 'connected',
1556
+ },
1557
+ uptime: process.uptime(),
1558
+ memory: process.memoryUsage(),
1559
+ };
1560
+
1561
+ const statusCode = health.status === 'healthy' ? 200 : 503;
1562
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1563
+ res.end(JSON.stringify(health, null, 2));
1564
+ }
1565
+
1566
+ /**
1567
+ * Handle webhook requests
1568
+ */
1569
+ private async handleWebhookRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1570
+ let body = '';
1571
+
1572
+ req.on('data', (chunk) => {
1573
+ body += chunk.toString();
1574
+ });
1575
+
1576
+ req.on('end', async () => {
1577
+ try {
1578
+ const update = JSON.parse(body);
1579
+
1580
+ // Process the update using grammY's webhook handler
1581
+ if (this.bot) {
1582
+ await this.bot.handleUpdate(update);
1583
+ }
1584
+
1585
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1586
+ res.end(JSON.stringify({ ok: true }));
1587
+ } catch (error) {
1588
+ console.error('Error processing webhook:', error);
1589
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1590
+ res.end(JSON.stringify({ error: 'Internal server error' }));
1591
+ }
1592
+ });
1593
+ }
1594
+
1595
+ /**
1596
+ * Connect using webhook mode instead of polling
1597
+ * Sets up the bot, registers commands, and starts the webhook server.
1598
+ */
1599
+ async connectWithWebhook(webhookUrl: string, serverConfig: WebhookServerConfig): Promise<void> {
1600
+ if (this._status === 'connected' || this._status === 'connecting') {
1601
+ return;
1602
+ }
1603
+
1604
+ this.setStatus('connecting');
1605
+ this.resetBackoff();
1606
+
1607
+ try {
1608
+ // Create bot instance
1609
+ this.bot = new Bot(this.config.botToken);
1610
+
1611
+ // Add API throttling
1612
+ const throttler = apiThrottler();
1613
+ this.bot.api.config.use(throttler);
1614
+
1615
+ // Add sequential processing
1616
+ if (this.config.sequentialProcessingEnabled) {
1617
+ this.bot.use(sequentialize(this.getSequentialKey));
1618
+ }
1619
+
1620
+ // Get bot info
1621
+ const me = await this.bot.api.getMe();
1622
+ this._botUsername = me.username;
1623
+
1624
+ // Register bot commands
1625
+ await this.registerBotCommands();
1626
+
1627
+ // Set up message handler
1628
+ this.bot.on('message:text', async (ctx) => {
1629
+ await this.handleTextMessage(ctx);
1630
+ });
1631
+
1632
+ // Handle errors
1633
+ this.bot.catch(async (err) => {
1634
+ await this.handleBotError(err);
1635
+ });
1636
+
1637
+ // Start deduplication cleanup
1638
+ if (this.config.deduplicationEnabled) {
1639
+ this.startDedupCleanup();
1640
+ }
1641
+
1642
+ // Start webhook server
1643
+ await this.startWebhookServer(serverConfig);
1644
+
1645
+ // Set webhook URL with Telegram
1646
+ await this.setWebhook(webhookUrl, serverConfig.secretToken);
1647
+
1648
+ console.log(`Telegram bot @${this._botUsername} connected via webhook`);
1649
+ this.setStatus('connected');
1650
+ } catch (error) {
1651
+ const err = error instanceof Error ? error : new Error(String(error));
1652
+ this.setStatus('error', err);
1653
+ throw err;
1654
+ }
1655
+ }
1656
+
1657
+ // Private methods
1658
+
1659
+ private mapContextToMessage(ctx: Context, overrideText?: string): IncomingMessage {
1660
+ const msg = ctx.message!;
1661
+ const from = msg.from!;
1662
+ const chat = msg.chat;
1663
+
1664
+ // Check for forum topic (message_thread_id indicates a forum topic)
1665
+ const threadId = msg.message_thread_id?.toString();
1666
+ const isForumTopic = msg.is_topic_message === true || threadId !== undefined;
1667
+
1668
+ return {
1669
+ messageId: msg.message_id.toString(),
1670
+ channel: 'telegram',
1671
+ userId: from.id.toString(),
1672
+ userName: from.first_name + (from.last_name ? ` ${from.last_name}` : ''),
1673
+ chatId: chat.id.toString(),
1674
+ text: overrideText ?? msg.text ?? '',
1675
+ timestamp: new Date(msg.date * 1000),
1676
+ replyTo: msg.reply_to_message?.message_id.toString(),
1677
+ threadId,
1678
+ isForumTopic,
1679
+ raw: ctx,
1680
+ };
1681
+ }
1682
+
1683
+ private async handleIncomingMessage(message: IncomingMessage): Promise<void> {
1684
+ for (const handler of this.messageHandlers) {
1685
+ try {
1686
+ await handler(message);
1687
+ } catch (error) {
1688
+ console.error('Error in message handler:', error);
1689
+ this.handleError(
1690
+ error instanceof Error ? error : new Error(String(error)),
1691
+ 'messageHandler'
1692
+ );
1693
+ }
1694
+ }
1695
+ }
1696
+
1697
+ private handleError(error: Error, context?: string): void {
1698
+ for (const handler of this.errorHandlers) {
1699
+ try {
1700
+ handler(error, context);
1701
+ } catch (e) {
1702
+ console.error('Error in error handler:', e);
1703
+ }
1704
+ }
1705
+ }
1706
+
1707
+ private setStatus(status: ChannelStatus, error?: Error): void {
1708
+ this._status = status;
1709
+ for (const handler of this.statusHandlers) {
1710
+ try {
1711
+ handler(status, error);
1712
+ } catch (e) {
1713
+ console.error('Error in status handler:', e);
1714
+ }
1715
+ }
1716
+ }
1717
+ }
1718
+
1719
+ /**
1720
+ * Create a Telegram adapter from configuration
1721
+ */
1722
+ export function createTelegramAdapter(config: TelegramAdapterConfig): TelegramAdapter {
1723
+ if (!config.botToken) {
1724
+ throw new Error('Telegram bot token is required');
1725
+ }
1726
+ return new TelegramAdapter(config);
1727
+ }