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,1390 @@
1
+ "use strict";
2
+ /**
3
+ * Telegram Channel Adapter
4
+ *
5
+ * Implements the ChannelAdapter interface using grammY for Telegram Bot API.
6
+ * Supports both polling and webhook modes.
7
+ *
8
+ * Features:
9
+ * - API throttling to prevent rate limits
10
+ * - Message deduplication to prevent double processing
11
+ * - Text fragment assembly for split long messages
12
+ * - ACK reactions while processing
13
+ * - Draft streaming for real-time response preview
14
+ * - Sequential message processing to prevent race conditions
15
+ * - Connection conflict detection (409 errors)
16
+ * - Exponential backoff with jitter for error recovery
17
+ * - Health check endpoint for webhook mode
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.TelegramAdapter = void 0;
54
+ exports.createTelegramAdapter = createTelegramAdapter;
55
+ const grammy_1 = require("grammy");
56
+ const runner_1 = require("@grammyjs/runner");
57
+ const transformer_throttler_1 = require("@grammyjs/transformer-throttler");
58
+ const fs = __importStar(require("fs"));
59
+ const path = __importStar(require("path"));
60
+ const http = __importStar(require("http"));
61
+ class TelegramAdapter {
62
+ constructor(config) {
63
+ this.type = 'telegram';
64
+ this.bot = null;
65
+ this._status = 'disconnected';
66
+ this.messageHandlers = [];
67
+ this.errorHandlers = [];
68
+ this.statusHandlers = [];
69
+ this.callbackQueryHandlers = [];
70
+ // Message deduplication: track processed update IDs
71
+ this.processedUpdates = new Map(); // updateId -> timestamp
72
+ this.DEDUP_CACHE_TTL = 60000; // 1 minute
73
+ this.DEDUP_CACHE_MAX_SIZE = 1000;
74
+ // Text fragment assembly: buffer split messages
75
+ this.pendingFragments = new Map(); // chatId:userId -> fragment
76
+ this.DEFAULT_FRAGMENT_TIMEOUT = 1500; // 1.5 seconds
77
+ // Draft streaming state
78
+ this.draftStates = new Map(); // chatId -> draft state
79
+ this.DRAFT_UPDATE_INTERVAL = 500; // Update draft every 500ms
80
+ // Exponential backoff state
81
+ this.backoffAttempt = 0;
82
+ this.isReconnecting = false;
83
+ // Default backoff configuration
84
+ this.DEFAULT_BACKOFF = {
85
+ initialDelay: 2000,
86
+ maxDelay: 30000,
87
+ multiplier: 1.8,
88
+ jitter: 0.25,
89
+ maxAttempts: 10,
90
+ };
91
+ /**
92
+ * Get sequential key for message ordering
93
+ * Messages from the same chat are processed sequentially
94
+ */
95
+ this.getSequentialKey = (ctx) => {
96
+ const chatId = ctx.chat?.id;
97
+ if (!chatId)
98
+ return undefined;
99
+ // Use chat ID + thread ID for forum topics
100
+ const threadId = ctx.message?.message_thread_id;
101
+ if (threadId) {
102
+ return `${chatId}:${threadId}`;
103
+ }
104
+ return String(chatId);
105
+ };
106
+ this.config = {
107
+ deduplicationEnabled: true,
108
+ ackReactionEnabled: true,
109
+ draftStreamingEnabled: true,
110
+ fragmentAssemblyTimeout: 1500,
111
+ sequentialProcessingEnabled: true,
112
+ ...config,
113
+ };
114
+ }
115
+ get status() {
116
+ return this._status;
117
+ }
118
+ get botUsername() {
119
+ return this._botUsername;
120
+ }
121
+ /**
122
+ * Connect to Telegram using long polling
123
+ */
124
+ async connect() {
125
+ if (this._status === 'connected' || this._status === 'connecting') {
126
+ return;
127
+ }
128
+ this.setStatus('connecting');
129
+ this.resetBackoff();
130
+ try {
131
+ // Create bot instance
132
+ this.bot = new grammy_1.Bot(this.config.botToken);
133
+ // Add API throttling to prevent rate limits
134
+ const throttler = (0, transformer_throttler_1.apiThrottler)();
135
+ this.bot.api.config.use(throttler);
136
+ // Add sequential processing to prevent race conditions
137
+ if (this.config.sequentialProcessingEnabled) {
138
+ this.bot.use((0, runner_1.sequentialize)(this.getSequentialKey));
139
+ }
140
+ // Get bot info
141
+ const me = await this.bot.api.getMe();
142
+ this._botUsername = me.username;
143
+ // Register expanded bot commands for the "/" menu
144
+ await this.registerBotCommands();
145
+ // Set up message handler with deduplication and fragment assembly
146
+ this.bot.on('message:text', async (ctx) => {
147
+ await this.handleTextMessage(ctx);
148
+ });
149
+ // Set up callback query handler for inline keyboards
150
+ this.bot.on('callback_query:data', async (ctx) => {
151
+ await this.handleCallbackQuery(ctx);
152
+ });
153
+ // Handle errors with 409 detection and backoff
154
+ this.bot.catch(async (err) => {
155
+ await this.handleBotError(err);
156
+ });
157
+ // Start deduplication cleanup timer
158
+ if (this.config.deduplicationEnabled) {
159
+ this.startDedupCleanup();
160
+ }
161
+ // Start polling with error handling
162
+ await this.startPolling();
163
+ }
164
+ catch (error) {
165
+ const err = error instanceof Error ? error : new Error(String(error));
166
+ // Check for connection conflict (409) during initial connection
167
+ if (this.isConnectionConflictError(error)) {
168
+ console.error('Connection conflict detected: Another bot instance is running');
169
+ this.setStatus('error', new Error('Connection conflict: Another bot instance is running. Stop the other instance first.'));
170
+ throw new Error('Connection conflict: Another bot instance is running');
171
+ }
172
+ this.setStatus('error', err);
173
+ throw err;
174
+ }
175
+ }
176
+ /**
177
+ * Register bot commands for the "/" menu
178
+ */
179
+ async registerBotCommands() {
180
+ if (!this.bot)
181
+ return;
182
+ await this.bot.api.setMyCommands([
183
+ // Core commands
184
+ { command: 'start', description: 'Start the bot and see welcome message' },
185
+ { command: 'help', description: 'Show all available commands' },
186
+ { command: 'status', description: 'Check bot connection and system status' },
187
+ // Workspace management
188
+ { command: 'workspaces', description: 'List all available workspaces' },
189
+ { command: 'workspace', description: 'Select or show current workspace' },
190
+ { command: 'addworkspace', description: 'Add a new workspace by path' },
191
+ { command: 'removeworkspace', description: 'Remove a workspace from the list' },
192
+ // Task management
193
+ { command: 'newtask', description: 'Start a fresh task/conversation' },
194
+ { command: 'cancel', description: 'Cancel the current running task' },
195
+ { command: 'retry', description: 'Retry the last failed task' },
196
+ { command: 'history', description: 'Show recent task history' },
197
+ // Model configuration
198
+ { command: 'provider', description: 'Change or show current LLM provider' },
199
+ { command: 'providers', description: 'List all available LLM providers' },
200
+ { command: 'model', description: 'Change or show current model' },
201
+ { command: 'models', description: 'List available AI models' },
202
+ // Skills management
203
+ { command: 'skills', description: 'List available skills' },
204
+ { command: 'skill', description: 'Enable or disable a skill' },
205
+ // Settings
206
+ { command: 'settings', description: 'View and modify bot settings' },
207
+ { command: 'debug', description: 'Toggle debug mode on/off' },
208
+ { command: 'version', description: 'Show bot version information' },
209
+ ]);
210
+ }
211
+ /**
212
+ * Start polling with error handling and reconnection
213
+ */
214
+ async startPolling() {
215
+ if (!this.bot)
216
+ return;
217
+ this.bot.start({
218
+ onStart: () => {
219
+ console.log(`Telegram bot @${this._botUsername} started`);
220
+ this.setStatus('connected');
221
+ this.resetBackoff();
222
+ },
223
+ drop_pending_updates: true,
224
+ allowed_updates: ['message', 'message_reaction', 'callback_query'],
225
+ });
226
+ }
227
+ /**
228
+ * Handle bot errors including 409 conflict detection
229
+ */
230
+ async handleBotError(err) {
231
+ console.error('Telegram bot error:', err);
232
+ // Check for connection conflict (409)
233
+ if (this.isConnectionConflictError(err)) {
234
+ console.error('Connection conflict detected (409): Another bot instance may be running');
235
+ this.setStatus('error', new Error('Connection conflict: Another bot instance is running'));
236
+ // Don't reconnect on 409 - let the user resolve the conflict
237
+ this.handleError(new Error('Connection conflict: Another bot instance is running. Stop the other instance and restart.'), 'connection_conflict');
238
+ return;
239
+ }
240
+ // Check for network errors that warrant reconnection
241
+ if (this.isNetworkError(err)) {
242
+ console.log('Network error detected, will attempt reconnection with backoff');
243
+ await this.attemptReconnection();
244
+ return;
245
+ }
246
+ // Handle other errors normally
247
+ this.handleError(err instanceof Error ? err : new Error(String(err)), 'bot.catch');
248
+ }
249
+ /**
250
+ * Check if error is a connection conflict (409)
251
+ */
252
+ isConnectionConflictError(err) {
253
+ if (err instanceof grammy_1.GrammyError) {
254
+ return err.error_code === 409;
255
+ }
256
+ if (err instanceof grammy_1.HttpError) {
257
+ return err.status === 409;
258
+ }
259
+ // Check error message for 409 indicators
260
+ const message = err instanceof Error ? err.message : String(err);
261
+ return message.includes('409') || message.includes('Conflict') || message.includes('terminated by other getUpdates');
262
+ }
263
+ /**
264
+ * Check if error is a network error that warrants reconnection
265
+ */
266
+ isNetworkError(err) {
267
+ if (err instanceof grammy_1.HttpError) {
268
+ return true;
269
+ }
270
+ const message = err instanceof Error ? err.message : String(err);
271
+ return (message.includes('ECONNRESET') ||
272
+ message.includes('ETIMEDOUT') ||
273
+ message.includes('ENOTFOUND') ||
274
+ message.includes('network') ||
275
+ message.includes('socket') ||
276
+ message.includes('502') ||
277
+ message.includes('503') ||
278
+ message.includes('504'));
279
+ }
280
+ /**
281
+ * Attempt reconnection with exponential backoff
282
+ */
283
+ async attemptReconnection() {
284
+ if (this.isReconnecting) {
285
+ console.log('Reconnection already in progress');
286
+ return;
287
+ }
288
+ const backoffConfig = { ...this.DEFAULT_BACKOFF, ...this.config.backoff };
289
+ if (this.backoffAttempt >= backoffConfig.maxAttempts) {
290
+ console.error(`Max reconnection attempts (${backoffConfig.maxAttempts}) reached`);
291
+ this.setStatus('error', new Error('Max reconnection attempts reached'));
292
+ this.handleError(new Error('Failed to reconnect after maximum attempts'), 'reconnection_failed');
293
+ return;
294
+ }
295
+ this.isReconnecting = true;
296
+ this.backoffAttempt++;
297
+ const delay = this.calculateBackoffDelay(backoffConfig);
298
+ console.log(`Reconnection attempt ${this.backoffAttempt}/${backoffConfig.maxAttempts} in ${delay}ms`);
299
+ this.backoffTimer = setTimeout(async () => {
300
+ try {
301
+ // Stop existing bot if any
302
+ if (this.bot) {
303
+ await this.bot.stop();
304
+ this.bot = null;
305
+ }
306
+ this.isReconnecting = false;
307
+ this.setStatus('disconnected');
308
+ // Attempt to reconnect
309
+ await this.connect();
310
+ }
311
+ catch (error) {
312
+ this.isReconnecting = false;
313
+ console.error('Reconnection attempt failed:', error);
314
+ // Schedule next attempt if not a 409 conflict
315
+ if (!this.isConnectionConflictError(error)) {
316
+ await this.attemptReconnection();
317
+ }
318
+ }
319
+ }, delay);
320
+ }
321
+ /**
322
+ * Calculate backoff delay with jitter
323
+ */
324
+ calculateBackoffDelay(config) {
325
+ // Calculate base delay: initialDelay * multiplier^attempt
326
+ let delay = config.initialDelay * Math.pow(config.multiplier, this.backoffAttempt - 1);
327
+ // Cap at max delay
328
+ delay = Math.min(delay, config.maxDelay);
329
+ // Add jitter: delay ± (delay * jitter * random)
330
+ const jitterAmount = delay * config.jitter;
331
+ const jitter = (Math.random() * 2 - 1) * jitterAmount; // Random between -jitterAmount and +jitterAmount
332
+ delay = Math.round(delay + jitter);
333
+ // Ensure minimum delay of 1 second
334
+ return Math.max(1000, delay);
335
+ }
336
+ /**
337
+ * Reset backoff state
338
+ */
339
+ resetBackoff() {
340
+ this.backoffAttempt = 0;
341
+ this.isReconnecting = false;
342
+ if (this.backoffTimer) {
343
+ clearTimeout(this.backoffTimer);
344
+ this.backoffTimer = undefined;
345
+ }
346
+ }
347
+ /**
348
+ * Handle incoming text message with deduplication and fragment assembly
349
+ */
350
+ async handleTextMessage(ctx) {
351
+ const msg = ctx.message;
352
+ const updateId = ctx.update.update_id;
353
+ // Feature 4: Message deduplication - check if already processed
354
+ if (this.config.deduplicationEnabled && this.isUpdateProcessed(updateId)) {
355
+ console.log(`Skipping duplicate update ${updateId}`);
356
+ return;
357
+ }
358
+ // Mark update as processed
359
+ if (this.config.deduplicationEnabled) {
360
+ this.markUpdateProcessed(updateId);
361
+ }
362
+ // Feature 3: Text fragment assembly - buffer split messages
363
+ const fragmentKey = `${msg.chat.id}:${msg.from.id}`;
364
+ const existingFragment = this.pendingFragments.get(fragmentKey);
365
+ if (existingFragment) {
366
+ // Add to existing fragment
367
+ clearTimeout(existingFragment.timer);
368
+ existingFragment.messages.push({
369
+ messageId: msg.message_id.toString(),
370
+ text: msg.text || '',
371
+ timestamp: new Date(msg.date * 1000),
372
+ ctx,
373
+ });
374
+ // Reset timer
375
+ existingFragment.timer = setTimeout(() => {
376
+ this.processFragments(fragmentKey);
377
+ }, this.config.fragmentAssemblyTimeout || this.DEFAULT_FRAGMENT_TIMEOUT);
378
+ }
379
+ else {
380
+ // Check if this might be a split message (long text arriving in chunks)
381
+ // Telegram splits messages at ~4096 chars, so check if message ends mid-sentence
382
+ const mightBeSplit = this.mightBeSplitMessage(msg.text || '');
383
+ if (mightBeSplit) {
384
+ // Start new fragment buffer
385
+ const timer = setTimeout(() => {
386
+ this.processFragments(fragmentKey);
387
+ }, this.config.fragmentAssemblyTimeout || this.DEFAULT_FRAGMENT_TIMEOUT);
388
+ this.pendingFragments.set(fragmentKey, {
389
+ chatId: msg.chat.id.toString(),
390
+ userId: msg.from.id.toString(),
391
+ messages: [{
392
+ messageId: msg.message_id.toString(),
393
+ text: msg.text || '',
394
+ timestamp: new Date(msg.date * 1000),
395
+ ctx,
396
+ }],
397
+ timer,
398
+ });
399
+ }
400
+ else {
401
+ // Process immediately (single message)
402
+ await this.processMessage(ctx);
403
+ }
404
+ }
405
+ }
406
+ /**
407
+ * Check if a message might be part of a split message
408
+ */
409
+ mightBeSplitMessage(text) {
410
+ // Messages near Telegram's limit or ending abruptly might be split
411
+ if (text.length >= 4000)
412
+ return true;
413
+ // Check if text ends mid-sentence (no terminal punctuation)
414
+ const trimmed = text.trim();
415
+ if (trimmed.length > 100) {
416
+ const lastChar = trimmed.charAt(trimmed.length - 1);
417
+ const terminalPunctuation = ['.', '!', '?', ')', ']', '}', '"', "'", '`'];
418
+ if (!terminalPunctuation.includes(lastChar)) {
419
+ return true;
420
+ }
421
+ }
422
+ return false;
423
+ }
424
+ /**
425
+ * Process assembled fragments
426
+ */
427
+ async processFragments(fragmentKey) {
428
+ const fragment = this.pendingFragments.get(fragmentKey);
429
+ if (!fragment)
430
+ return;
431
+ this.pendingFragments.delete(fragmentKey);
432
+ if (fragment.messages.length === 1) {
433
+ // Single message, process normally
434
+ await this.processMessage(fragment.messages[0].ctx);
435
+ }
436
+ else {
437
+ // Multiple messages, combine them
438
+ const combinedText = fragment.messages
439
+ .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
440
+ .map(m => m.text)
441
+ .join('');
442
+ // Use the first message's context but with combined text
443
+ const firstCtx = fragment.messages[0].ctx;
444
+ const message = this.mapContextToMessage(firstCtx, combinedText);
445
+ console.log(`Assembled ${fragment.messages.length} text fragments into single message (${combinedText.length} chars)`);
446
+ await this.handleIncomingMessage(message);
447
+ }
448
+ }
449
+ /**
450
+ * Process a single message (with ACK reaction)
451
+ */
452
+ async processMessage(ctx) {
453
+ const message = this.mapContextToMessage(ctx);
454
+ // Feature 2: Send ACK reaction (👀) while processing
455
+ if (this.config.ackReactionEnabled) {
456
+ try {
457
+ await this.sendAckReaction(ctx);
458
+ }
459
+ catch (err) {
460
+ // Ignore reaction errors (might not have permission)
461
+ console.debug('Could not send ACK reaction:', err);
462
+ }
463
+ }
464
+ await this.handleIncomingMessage(message);
465
+ }
466
+ /**
467
+ * Send ACK reaction (👀) to indicate message received
468
+ */
469
+ async sendAckReaction(ctx) {
470
+ if (!this.bot || !ctx.message)
471
+ return;
472
+ try {
473
+ await this.bot.api.setMessageReaction(ctx.message.chat.id, ctx.message.message_id, [{ type: 'emoji', emoji: '👀' }]);
474
+ }
475
+ catch {
476
+ // Silently fail - reactions might not be available
477
+ }
478
+ }
479
+ /**
480
+ * Remove ACK reaction after processing
481
+ */
482
+ async removeAckReaction(chatId, messageId) {
483
+ if (!this.bot)
484
+ return;
485
+ try {
486
+ await this.bot.api.setMessageReaction(chatId, parseInt(messageId, 10), [] // Empty array removes reactions
487
+ );
488
+ }
489
+ catch {
490
+ // Silently fail
491
+ }
492
+ }
493
+ /**
494
+ * Send a completion reaction when done
495
+ * Note: Telegram only allows specific reaction emojis, using 👍 for completion
496
+ */
497
+ async sendCompletionReaction(chatId, messageId) {
498
+ if (!this.bot)
499
+ return;
500
+ try {
501
+ await this.bot.api.setMessageReaction(chatId, parseInt(messageId, 10), [{ type: 'emoji', emoji: '👍' }]);
502
+ }
503
+ catch {
504
+ // Silently fail
505
+ }
506
+ }
507
+ /**
508
+ * Check if update was already processed (deduplication)
509
+ */
510
+ isUpdateProcessed(updateId) {
511
+ return this.processedUpdates.has(updateId);
512
+ }
513
+ /**
514
+ * Mark update as processed
515
+ */
516
+ markUpdateProcessed(updateId) {
517
+ this.processedUpdates.set(updateId, Date.now());
518
+ // Prevent unbounded growth
519
+ if (this.processedUpdates.size > this.DEDUP_CACHE_MAX_SIZE) {
520
+ this.cleanupDedupCache();
521
+ }
522
+ }
523
+ /**
524
+ * Start periodic cleanup of dedup cache
525
+ */
526
+ startDedupCleanup() {
527
+ this.dedupCleanupTimer = setInterval(() => {
528
+ this.cleanupDedupCache();
529
+ }, this.DEDUP_CACHE_TTL);
530
+ }
531
+ /**
532
+ * Clean up old entries from dedup cache
533
+ */
534
+ cleanupDedupCache() {
535
+ const now = Date.now();
536
+ for (const [updateId, timestamp] of this.processedUpdates) {
537
+ if (now - timestamp > this.DEDUP_CACHE_TTL) {
538
+ this.processedUpdates.delete(updateId);
539
+ }
540
+ }
541
+ }
542
+ /**
543
+ * Disconnect from Telegram
544
+ */
545
+ async disconnect() {
546
+ // Reset backoff state
547
+ this.resetBackoff();
548
+ // Clear timers
549
+ if (this.dedupCleanupTimer) {
550
+ clearInterval(this.dedupCleanupTimer);
551
+ this.dedupCleanupTimer = undefined;
552
+ }
553
+ // Clear pending fragments
554
+ for (const fragment of this.pendingFragments.values()) {
555
+ clearTimeout(fragment.timer);
556
+ }
557
+ this.pendingFragments.clear();
558
+ // Clear draft states
559
+ this.draftStates.clear();
560
+ // Clear dedup cache
561
+ this.processedUpdates.clear();
562
+ // Stop webhook server if running
563
+ if (this.webhookServer) {
564
+ await this.stopWebhookServer();
565
+ }
566
+ if (this.bot) {
567
+ await this.bot.stop();
568
+ this.bot = null;
569
+ }
570
+ this._botUsername = undefined;
571
+ this.setStatus('disconnected');
572
+ }
573
+ /**
574
+ * Send a message to a Telegram chat
575
+ */
576
+ async sendMessage(message) {
577
+ if (!this.bot || this._status !== 'connected') {
578
+ throw new Error('Telegram bot is not connected');
579
+ }
580
+ // Handle image attachments first (send images before text)
581
+ let lastMessageId;
582
+ if (message.attachments && message.attachments.length > 0) {
583
+ for (const attachment of message.attachments) {
584
+ if (attachment.type === 'image' && attachment.url) {
585
+ try {
586
+ // attachment.url is the file path for local images
587
+ const msgId = await this.sendPhoto(message.chatId, attachment.url);
588
+ lastMessageId = msgId;
589
+ }
590
+ catch (err) {
591
+ console.error('Failed to send image attachment:', err);
592
+ }
593
+ }
594
+ }
595
+ }
596
+ // If we have text to send, send it
597
+ if (message.text && message.text.trim()) {
598
+ // Process text for Telegram compatibility
599
+ let processedText = message.text;
600
+ if (message.parseMode === 'markdown') {
601
+ processedText = this.convertMarkdownForTelegram(message.text);
602
+ }
603
+ const options = {};
604
+ // Set parse mode
605
+ // Use legacy Markdown (not MarkdownV2) to avoid escaping issues with special characters
606
+ if (message.parseMode === 'markdown') {
607
+ options.parse_mode = 'Markdown';
608
+ }
609
+ else if (message.parseMode === 'html') {
610
+ options.parse_mode = 'HTML';
611
+ }
612
+ // Reply to message if specified
613
+ if (message.replyTo) {
614
+ options.reply_to_message_id = parseInt(message.replyTo, 10);
615
+ }
616
+ // Forum topic thread support
617
+ if (message.threadId) {
618
+ options.message_thread_id = parseInt(message.threadId, 10);
619
+ }
620
+ // Link preview control
621
+ if (message.disableLinkPreview) {
622
+ options.link_preview_options = { is_disabled: true };
623
+ }
624
+ // Inline keyboard support
625
+ if (message.inlineKeyboard && message.inlineKeyboard.length > 0) {
626
+ options.reply_markup = this.buildInlineKeyboard(message.inlineKeyboard);
627
+ }
628
+ try {
629
+ const sent = await this.bot.api.sendMessage(message.chatId, processedText, options);
630
+ return sent.message_id.toString();
631
+ }
632
+ catch (error) {
633
+ // If markdown parsing fails, retry without parse_mode
634
+ if (error?.error_code === 400 && error?.description?.includes("can't parse entities")) {
635
+ console.log('Markdown parsing failed, retrying without parse_mode');
636
+ const plainOptions = {
637
+ ...(message.threadId && { message_thread_id: parseInt(message.threadId, 10) }),
638
+ ...(message.disableLinkPreview && { link_preview_options: { is_disabled: true } }),
639
+ ...(message.inlineKeyboard && { reply_markup: this.buildInlineKeyboard(message.inlineKeyboard) }),
640
+ };
641
+ if (message.replyTo) {
642
+ plainOptions.reply_to_message_id = parseInt(message.replyTo, 10);
643
+ }
644
+ const sent = await this.bot.api.sendMessage(message.chatId, message.text, plainOptions);
645
+ return sent.message_id.toString();
646
+ }
647
+ throw error;
648
+ }
649
+ }
650
+ // If no text but had attachments, return the last attachment message ID
651
+ return lastMessageId || '';
652
+ }
653
+ /**
654
+ * Build grammY InlineKeyboard from our button format
655
+ */
656
+ buildInlineKeyboard(buttons) {
657
+ const keyboard = new grammy_1.InlineKeyboard();
658
+ for (const row of buttons) {
659
+ for (const button of row) {
660
+ if (button.url) {
661
+ keyboard.url(button.text, button.url);
662
+ }
663
+ else if (button.callbackData) {
664
+ keyboard.text(button.text, button.callbackData);
665
+ }
666
+ }
667
+ keyboard.row();
668
+ }
669
+ return keyboard;
670
+ }
671
+ /**
672
+ * Handle incoming callback query from inline keyboard button press
673
+ */
674
+ async handleCallbackQuery(ctx) {
675
+ const query = ctx.callbackQuery;
676
+ if (!query.data || !query.message) {
677
+ return;
678
+ }
679
+ const callbackQuery = {
680
+ id: query.id,
681
+ userId: query.from.id.toString(),
682
+ userName: query.from.first_name + (query.from.last_name ? ` ${query.from.last_name}` : ''),
683
+ chatId: query.message.chat.id.toString(),
684
+ messageId: query.message.message_id.toString(),
685
+ data: query.data,
686
+ threadId: query.message.message_thread_id?.toString(),
687
+ raw: ctx,
688
+ };
689
+ // Notify all registered handlers
690
+ for (const handler of this.callbackQueryHandlers) {
691
+ try {
692
+ await handler(callbackQuery);
693
+ }
694
+ catch (error) {
695
+ console.error('Error in callback query handler:', error);
696
+ this.handleError(error instanceof Error ? error : new Error(String(error)), 'callbackQueryHandler');
697
+ }
698
+ }
699
+ }
700
+ /**
701
+ * Feature 1: Draft streaming - Start streaming a response
702
+ * Creates or updates a draft message that shows response as it generates
703
+ */
704
+ async startDraftStream(chatId) {
705
+ if (!this.config.draftStreamingEnabled)
706
+ return;
707
+ this.draftStates.set(chatId, {
708
+ chatId,
709
+ currentText: '',
710
+ lastUpdateTime: Date.now(),
711
+ });
712
+ }
713
+ /**
714
+ * Update draft stream with new content
715
+ */
716
+ async updateDraftStream(chatId, text) {
717
+ if (!this.bot || !this.config.draftStreamingEnabled)
718
+ return;
719
+ const state = this.draftStates.get(chatId);
720
+ if (!state)
721
+ return;
722
+ const now = Date.now();
723
+ // Throttle updates to prevent API spam
724
+ if (now - state.lastUpdateTime < this.DRAFT_UPDATE_INTERVAL) {
725
+ // Just update the text, don't send yet
726
+ state.currentText = text;
727
+ return;
728
+ }
729
+ // Add typing indicator suffix
730
+ const displayText = text + ' ▌';
731
+ try {
732
+ if (state.messageId) {
733
+ // Edit existing message
734
+ await this.bot.api.editMessageText(chatId, parseInt(state.messageId, 10), displayText);
735
+ }
736
+ else {
737
+ // Create new message
738
+ const sent = await this.bot.api.sendMessage(chatId, displayText);
739
+ state.messageId = sent.message_id.toString();
740
+ }
741
+ state.currentText = text;
742
+ state.lastUpdateTime = now;
743
+ }
744
+ catch (error) {
745
+ // Ignore "message not modified" errors
746
+ if (!error?.description?.includes('message is not modified')) {
747
+ console.error('Draft stream update error:', error);
748
+ }
749
+ }
750
+ }
751
+ /**
752
+ * Finalize draft stream with final content
753
+ */
754
+ async finalizeDraftStream(chatId, finalText) {
755
+ if (!this.bot)
756
+ throw new Error('Bot not connected');
757
+ const state = this.draftStates.get(chatId);
758
+ this.draftStates.delete(chatId);
759
+ if (!this.config.draftStreamingEnabled || !state?.messageId) {
760
+ // No draft exists, send as new message
761
+ const sent = await this.bot.api.sendMessage(chatId, finalText);
762
+ return sent.message_id.toString();
763
+ }
764
+ try {
765
+ // Edit the draft message to final content (remove typing indicator)
766
+ await this.bot.api.editMessageText(chatId, parseInt(state.messageId, 10), finalText);
767
+ return state.messageId;
768
+ }
769
+ catch (error) {
770
+ // If edit fails, send as new message
771
+ console.error('Failed to finalize draft, sending new message:', error);
772
+ const sent = await this.bot.api.sendMessage(chatId, finalText);
773
+ return sent.message_id.toString();
774
+ }
775
+ }
776
+ /**
777
+ * Cancel draft stream (delete the draft message)
778
+ */
779
+ async cancelDraftStream(chatId) {
780
+ const state = this.draftStates.get(chatId);
781
+ this.draftStates.delete(chatId);
782
+ if (state?.messageId && this.bot) {
783
+ try {
784
+ await this.bot.api.deleteMessage(chatId, parseInt(state.messageId, 10));
785
+ }
786
+ catch {
787
+ // Ignore deletion errors
788
+ }
789
+ }
790
+ }
791
+ /**
792
+ * Convert GitHub-flavored markdown to Telegram-compatible format
793
+ * Telegram legacy Markdown only supports: *bold*, _italic_, `code`, ```code blocks```, [links](url)
794
+ */
795
+ convertMarkdownForTelegram(text) {
796
+ let result = text;
797
+ // Convert markdown headers (## Header) to bold (*Header*)
798
+ // Must be done before ** conversion
799
+ result = result.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');
800
+ // Convert markdown tables to code blocks
801
+ // Tables start with | and have a separator line like |---|---|
802
+ const tableRegex = /(\|[^\n]+\|\n)+/g;
803
+ const hasSeparatorLine = /\|[\s-:]+\|/;
804
+ result = result.replace(tableRegex, (match) => {
805
+ // Check if this looks like a table (has separator line with dashes)
806
+ if (hasSeparatorLine.test(match)) {
807
+ // Convert table to code block for monospace display
808
+ // Remove the separator line (|---|---|) as it's just formatting
809
+ const lines = match.split('\n').filter(line => line.trim());
810
+ const cleanedLines = lines.filter(line => !(/^\|[\s-:]+\|$/.test(line.trim())));
811
+ // Format table nicely
812
+ const formattedTable = cleanedLines.map(line => {
813
+ // Remove leading/trailing pipes and clean up
814
+ return line.replace(/^\||\|$/g, '').trim();
815
+ }).join('\n');
816
+ return '```\n' + formattedTable + '\n```\n';
817
+ }
818
+ return match;
819
+ });
820
+ // Convert **bold** to *bold* (Telegram uses single asterisk)
821
+ result = result.replace(/\*\*([^*]+)\*\*/g, '*$1*');
822
+ // Convert __bold__ to *bold* (alternative bold syntax)
823
+ result = result.replace(/__([^_]+)__/g, '*$1*');
824
+ // Convert horizontal rules (---, ***) to a line
825
+ result = result.replace(/^[-*]{3,}$/gm, '─────────────────');
826
+ return result;
827
+ }
828
+ /**
829
+ * Edit an existing message
830
+ */
831
+ async editMessage(chatId, messageId, text) {
832
+ if (!this.bot || this._status !== 'connected') {
833
+ throw new Error('Telegram bot is not connected');
834
+ }
835
+ const msgId = parseInt(messageId, 10);
836
+ if (isNaN(msgId)) {
837
+ throw new Error(`Invalid message ID: ${messageId}`);
838
+ }
839
+ await this.bot.api.editMessageText(chatId, msgId, text);
840
+ }
841
+ /**
842
+ * Delete a message
843
+ */
844
+ async deleteMessage(chatId, messageId) {
845
+ if (!this.bot || this._status !== 'connected') {
846
+ throw new Error('Telegram bot is not connected');
847
+ }
848
+ const msgId = parseInt(messageId, 10);
849
+ if (isNaN(msgId)) {
850
+ throw new Error(`Invalid message ID: ${messageId}`);
851
+ }
852
+ await this.bot.api.deleteMessage(chatId, msgId);
853
+ }
854
+ /**
855
+ * Send a document/file to a chat
856
+ */
857
+ async sendDocument(chatId, filePath, caption) {
858
+ if (!this.bot || this._status !== 'connected') {
859
+ throw new Error('Telegram bot is not connected');
860
+ }
861
+ // Check if file exists
862
+ if (!fs.existsSync(filePath)) {
863
+ throw new Error(`File not found: ${filePath}`);
864
+ }
865
+ const fileName = path.basename(filePath);
866
+ const fileBuffer = fs.readFileSync(filePath);
867
+ const sent = await this.bot.api.sendDocument(chatId, new grammy_1.InputFile(fileBuffer, fileName), { caption });
868
+ return sent.message_id.toString();
869
+ }
870
+ /**
871
+ * Send a photo/image to a chat
872
+ */
873
+ async sendPhoto(chatId, filePath, caption) {
874
+ if (!this.bot || this._status !== 'connected') {
875
+ throw new Error('Telegram bot is not connected');
876
+ }
877
+ // Check if file exists
878
+ if (!fs.existsSync(filePath)) {
879
+ throw new Error(`File not found: ${filePath}`);
880
+ }
881
+ const fileName = path.basename(filePath);
882
+ const fileBuffer = fs.readFileSync(filePath);
883
+ const sent = await this.bot.api.sendPhoto(chatId, new grammy_1.InputFile(fileBuffer, fileName), { caption });
884
+ return sent.message_id.toString();
885
+ }
886
+ /**
887
+ * Register a message handler
888
+ */
889
+ onMessage(handler) {
890
+ this.messageHandlers.push(handler);
891
+ }
892
+ /**
893
+ * Register a callback query handler (for inline keyboard buttons)
894
+ */
895
+ onCallbackQuery(handler) {
896
+ this.callbackQueryHandlers.push(handler);
897
+ }
898
+ /**
899
+ * Answer a callback query (acknowledge button press)
900
+ * Call this to remove the loading state from the button.
901
+ */
902
+ async answerCallbackQuery(queryId, text, showAlert) {
903
+ if (!this.bot || this._status !== 'connected') {
904
+ throw new Error('Telegram bot is not connected');
905
+ }
906
+ await this.bot.api.answerCallbackQuery(queryId, {
907
+ text,
908
+ show_alert: showAlert,
909
+ });
910
+ }
911
+ /**
912
+ * Edit a message with a new inline keyboard
913
+ */
914
+ async editMessageWithKeyboard(chatId, messageId, text, inlineKeyboard) {
915
+ if (!this.bot || this._status !== 'connected') {
916
+ throw new Error('Telegram bot is not connected');
917
+ }
918
+ const msgId = parseInt(messageId, 10);
919
+ if (isNaN(msgId)) {
920
+ throw new Error(`Invalid message ID: ${messageId}`);
921
+ }
922
+ const options = {};
923
+ if (inlineKeyboard && inlineKeyboard.length > 0) {
924
+ options.reply_markup = this.buildInlineKeyboard(inlineKeyboard);
925
+ }
926
+ if (text) {
927
+ await this.bot.api.editMessageText(chatId, msgId, text, options);
928
+ }
929
+ else if (inlineKeyboard) {
930
+ await this.bot.api.editMessageReplyMarkup(chatId, msgId, options);
931
+ }
932
+ }
933
+ // ============================================================================
934
+ // Extended Features
935
+ // ============================================================================
936
+ /**
937
+ * Send typing indicator (chat action)
938
+ */
939
+ async sendTyping(chatId, threadId) {
940
+ if (!this.bot || this._status !== 'connected') {
941
+ throw new Error('Telegram bot is not connected');
942
+ }
943
+ const options = {};
944
+ if (threadId) {
945
+ options.message_thread_id = parseInt(threadId, 10);
946
+ }
947
+ await this.bot.api.sendChatAction(chatId, 'typing', options);
948
+ }
949
+ /**
950
+ * Add reaction to a message
951
+ */
952
+ async addReaction(chatId, messageId, emoji) {
953
+ if (!this.bot || this._status !== 'connected') {
954
+ throw new Error('Telegram bot is not connected');
955
+ }
956
+ const msgId = parseInt(messageId, 10);
957
+ // Cast emoji to the expected type - Telegram will reject invalid emojis at runtime
958
+ await this.bot.api.setMessageReaction(chatId, msgId, [{ type: 'emoji', emoji: emoji }]);
959
+ }
960
+ /**
961
+ * Remove reaction from a message
962
+ */
963
+ async removeReaction(chatId, messageId) {
964
+ if (!this.bot || this._status !== 'connected') {
965
+ throw new Error('Telegram bot is not connected');
966
+ }
967
+ const msgId = parseInt(messageId, 10);
968
+ await this.bot.api.setMessageReaction(chatId, msgId, []);
969
+ }
970
+ /**
971
+ * Send a poll
972
+ */
973
+ async sendPoll(chatId, poll, threadId) {
974
+ if (!this.bot || this._status !== 'connected') {
975
+ throw new Error('Telegram bot is not connected');
976
+ }
977
+ const options = {
978
+ is_anonymous: poll.isAnonymous ?? true,
979
+ allows_multiple_answers: poll.allowsMultipleAnswers ?? false,
980
+ };
981
+ if (threadId) {
982
+ options.message_thread_id = parseInt(threadId, 10);
983
+ }
984
+ if (poll.type === 'quiz' && poll.correctOptionId !== undefined) {
985
+ options.type = 'quiz';
986
+ options.correct_option_id = poll.correctOptionId;
987
+ if (poll.explanation) {
988
+ options.explanation = poll.explanation;
989
+ }
990
+ }
991
+ if (poll.openPeriod) {
992
+ options.open_period = poll.openPeriod;
993
+ }
994
+ else if (poll.closeDate) {
995
+ options.close_date = Math.floor(poll.closeDate.getTime() / 1000);
996
+ }
997
+ const sent = await this.bot.api.sendPoll(chatId, poll.question, poll.options.map(o => o.text), options);
998
+ return sent.message_id.toString();
999
+ }
1000
+ /**
1001
+ * Send message with reply keyboard (persistent keyboard below input)
1002
+ */
1003
+ async sendWithReplyKeyboard(chatId, text, keyboard, threadId) {
1004
+ if (!this.bot || this._status !== 'connected') {
1005
+ throw new Error('Telegram bot is not connected');
1006
+ }
1007
+ const replyMarkup = {
1008
+ keyboard: keyboard.buttons.map(row => row.map(btn => ({
1009
+ text: btn.text,
1010
+ request_contact: btn.requestContact,
1011
+ request_location: btn.requestLocation,
1012
+ }))),
1013
+ resize_keyboard: keyboard.resizeKeyboard ?? true,
1014
+ one_time_keyboard: keyboard.oneTimeKeyboard ?? false,
1015
+ input_field_placeholder: keyboard.inputPlaceholder,
1016
+ };
1017
+ const options = {
1018
+ reply_markup: replyMarkup,
1019
+ };
1020
+ if (threadId) {
1021
+ options.message_thread_id = parseInt(threadId, 10);
1022
+ }
1023
+ const sent = await this.bot.api.sendMessage(chatId, text, options);
1024
+ return sent.message_id.toString();
1025
+ }
1026
+ /**
1027
+ * Remove reply keyboard (send message that hides the keyboard)
1028
+ */
1029
+ async removeReplyKeyboard(chatId, text, threadId) {
1030
+ if (!this.bot || this._status !== 'connected') {
1031
+ throw new Error('Telegram bot is not connected');
1032
+ }
1033
+ const options = {
1034
+ reply_markup: { remove_keyboard: true },
1035
+ };
1036
+ if (threadId) {
1037
+ options.message_thread_id = parseInt(threadId, 10);
1038
+ }
1039
+ const sent = await this.bot.api.sendMessage(chatId, text, options);
1040
+ return sent.message_id.toString();
1041
+ }
1042
+ /**
1043
+ * Send a sticker
1044
+ */
1045
+ async sendSticker(chatId, stickerId, threadId) {
1046
+ if (!this.bot || this._status !== 'connected') {
1047
+ throw new Error('Telegram bot is not connected');
1048
+ }
1049
+ const options = {};
1050
+ if (threadId) {
1051
+ options.message_thread_id = parseInt(threadId, 10);
1052
+ }
1053
+ const sent = await this.bot.api.sendSticker(chatId, stickerId, options);
1054
+ return sent.message_id.toString();
1055
+ }
1056
+ /**
1057
+ * Send location
1058
+ */
1059
+ async sendLocation(chatId, latitude, longitude, threadId) {
1060
+ if (!this.bot || this._status !== 'connected') {
1061
+ throw new Error('Telegram bot is not connected');
1062
+ }
1063
+ const options = {};
1064
+ if (threadId) {
1065
+ options.message_thread_id = parseInt(threadId, 10);
1066
+ }
1067
+ const sent = await this.bot.api.sendLocation(chatId, latitude, longitude, options);
1068
+ return sent.message_id.toString();
1069
+ }
1070
+ /**
1071
+ * Send a media group (album)
1072
+ */
1073
+ async sendMediaGroup(chatId, media, threadId) {
1074
+ if (!this.bot || this._status !== 'connected') {
1075
+ throw new Error('Telegram bot is not connected');
1076
+ }
1077
+ const inputMedia = media.map((m, index) => {
1078
+ const fileBuffer = fs.readFileSync(m.filePath);
1079
+ const fileName = path.basename(m.filePath);
1080
+ return {
1081
+ type: m.type,
1082
+ media: new grammy_1.InputFile(fileBuffer, fileName),
1083
+ caption: index === 0 ? m.caption : undefined, // Caption on first item only
1084
+ };
1085
+ });
1086
+ const options = {};
1087
+ if (threadId) {
1088
+ options.message_thread_id = parseInt(threadId, 10);
1089
+ }
1090
+ const sent = await this.bot.api.sendMediaGroup(chatId, inputMedia, options);
1091
+ return sent.map(m => m.message_id.toString());
1092
+ }
1093
+ // ============================================================================
1094
+ // Handler Registration
1095
+ // ============================================================================
1096
+ /**
1097
+ * Register an error handler
1098
+ */
1099
+ onError(handler) {
1100
+ this.errorHandlers.push(handler);
1101
+ }
1102
+ /**
1103
+ * Register a status change handler
1104
+ */
1105
+ onStatusChange(handler) {
1106
+ this.statusHandlers.push(handler);
1107
+ }
1108
+ /**
1109
+ * Get channel info
1110
+ */
1111
+ async getInfo() {
1112
+ let botId;
1113
+ let botDisplayName;
1114
+ if (this.bot && this._status === 'connected') {
1115
+ try {
1116
+ const me = await this.bot.api.getMe();
1117
+ botId = me.id.toString();
1118
+ botDisplayName = me.first_name;
1119
+ this._botUsername = me.username;
1120
+ }
1121
+ catch {
1122
+ // Ignore errors getting info
1123
+ }
1124
+ }
1125
+ return {
1126
+ type: 'telegram',
1127
+ status: this._status,
1128
+ botId,
1129
+ botUsername: this._botUsername,
1130
+ botDisplayName,
1131
+ };
1132
+ }
1133
+ /**
1134
+ * Get webhook callback for Express/Fastify/etc.
1135
+ * Use this when running in webhook mode instead of polling.
1136
+ */
1137
+ getWebhookCallback() {
1138
+ if (!this.bot) {
1139
+ throw new Error('Bot not initialized');
1140
+ }
1141
+ return (0, grammy_1.webhookCallback)(this.bot, 'express');
1142
+ }
1143
+ /**
1144
+ * Set webhook URL
1145
+ */
1146
+ async setWebhook(url, secretToken) {
1147
+ if (!this.bot) {
1148
+ throw new Error('Bot not initialized');
1149
+ }
1150
+ await this.bot.api.setWebhook(url, {
1151
+ secret_token: secretToken,
1152
+ allowed_updates: ['message'],
1153
+ });
1154
+ }
1155
+ /**
1156
+ * Remove webhook
1157
+ */
1158
+ async deleteWebhook() {
1159
+ if (!this.bot) {
1160
+ throw new Error('Bot not initialized');
1161
+ }
1162
+ await this.bot.api.deleteWebhook();
1163
+ }
1164
+ /**
1165
+ * Start webhook server with health check endpoint
1166
+ * This creates an HTTP server that handles both webhook callbacks and health checks.
1167
+ */
1168
+ async startWebhookServer(config) {
1169
+ if (this.webhookServer) {
1170
+ throw new Error('Webhook server is already running');
1171
+ }
1172
+ if (!this.bot) {
1173
+ throw new Error('Bot not initialized. Call connect() first or initialize bot manually.');
1174
+ }
1175
+ const { port, host = '0.0.0.0', secretToken, webhookPath = '/webhook', healthPath = '/healthz', } = config;
1176
+ // Create HTTP server
1177
+ this.webhookServer = http.createServer(async (req, res) => {
1178
+ const url = req.url || '/';
1179
+ // Health check endpoint
1180
+ if (req.method === 'GET' && url === healthPath) {
1181
+ await this.handleHealthCheck(req, res);
1182
+ return;
1183
+ }
1184
+ // Webhook endpoint
1185
+ if (req.method === 'POST' && url === webhookPath) {
1186
+ // Validate secret token if configured
1187
+ if (secretToken) {
1188
+ const requestToken = req.headers['x-telegram-bot-api-secret-token'];
1189
+ if (requestToken !== secretToken) {
1190
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1191
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
1192
+ return;
1193
+ }
1194
+ }
1195
+ // Handle webhook callback
1196
+ await this.handleWebhookRequest(req, res);
1197
+ return;
1198
+ }
1199
+ // 404 for unknown routes
1200
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1201
+ res.end(JSON.stringify({ error: 'Not found' }));
1202
+ });
1203
+ // Start listening
1204
+ return new Promise((resolve, reject) => {
1205
+ this.webhookServer.listen(port, host, () => {
1206
+ console.log(`Telegram webhook server listening on ${host}:${port}`);
1207
+ console.log(` Webhook endpoint: ${webhookPath}`);
1208
+ console.log(` Health check: ${healthPath}`);
1209
+ resolve();
1210
+ });
1211
+ this.webhookServer.on('error', (error) => {
1212
+ console.error('Webhook server error:', error);
1213
+ reject(error);
1214
+ });
1215
+ });
1216
+ }
1217
+ /**
1218
+ * Stop the webhook server
1219
+ */
1220
+ async stopWebhookServer() {
1221
+ if (!this.webhookServer) {
1222
+ return;
1223
+ }
1224
+ return new Promise((resolve) => {
1225
+ this.webhookServer.close(() => {
1226
+ console.log('Webhook server stopped');
1227
+ this.webhookServer = undefined;
1228
+ resolve();
1229
+ });
1230
+ });
1231
+ }
1232
+ /**
1233
+ * Handle health check requests
1234
+ */
1235
+ async handleHealthCheck(_req, res) {
1236
+ const health = {
1237
+ status: this._status === 'connected' ? 'healthy' : 'unhealthy',
1238
+ timestamp: new Date().toISOString(),
1239
+ bot: {
1240
+ status: this._status,
1241
+ username: this._botUsername || null,
1242
+ connected: this._status === 'connected',
1243
+ },
1244
+ uptime: process.uptime(),
1245
+ memory: process.memoryUsage(),
1246
+ };
1247
+ const statusCode = health.status === 'healthy' ? 200 : 503;
1248
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1249
+ res.end(JSON.stringify(health, null, 2));
1250
+ }
1251
+ /**
1252
+ * Handle webhook requests
1253
+ */
1254
+ async handleWebhookRequest(req, res) {
1255
+ let body = '';
1256
+ req.on('data', (chunk) => {
1257
+ body += chunk.toString();
1258
+ });
1259
+ req.on('end', async () => {
1260
+ try {
1261
+ const update = JSON.parse(body);
1262
+ // Process the update using grammY's webhook handler
1263
+ if (this.bot) {
1264
+ await this.bot.handleUpdate(update);
1265
+ }
1266
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1267
+ res.end(JSON.stringify({ ok: true }));
1268
+ }
1269
+ catch (error) {
1270
+ console.error('Error processing webhook:', error);
1271
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1272
+ res.end(JSON.stringify({ error: 'Internal server error' }));
1273
+ }
1274
+ });
1275
+ }
1276
+ /**
1277
+ * Connect using webhook mode instead of polling
1278
+ * Sets up the bot, registers commands, and starts the webhook server.
1279
+ */
1280
+ async connectWithWebhook(webhookUrl, serverConfig) {
1281
+ if (this._status === 'connected' || this._status === 'connecting') {
1282
+ return;
1283
+ }
1284
+ this.setStatus('connecting');
1285
+ this.resetBackoff();
1286
+ try {
1287
+ // Create bot instance
1288
+ this.bot = new grammy_1.Bot(this.config.botToken);
1289
+ // Add API throttling
1290
+ const throttler = (0, transformer_throttler_1.apiThrottler)();
1291
+ this.bot.api.config.use(throttler);
1292
+ // Add sequential processing
1293
+ if (this.config.sequentialProcessingEnabled) {
1294
+ this.bot.use((0, runner_1.sequentialize)(this.getSequentialKey));
1295
+ }
1296
+ // Get bot info
1297
+ const me = await this.bot.api.getMe();
1298
+ this._botUsername = me.username;
1299
+ // Register bot commands
1300
+ await this.registerBotCommands();
1301
+ // Set up message handler
1302
+ this.bot.on('message:text', async (ctx) => {
1303
+ await this.handleTextMessage(ctx);
1304
+ });
1305
+ // Handle errors
1306
+ this.bot.catch(async (err) => {
1307
+ await this.handleBotError(err);
1308
+ });
1309
+ // Start deduplication cleanup
1310
+ if (this.config.deduplicationEnabled) {
1311
+ this.startDedupCleanup();
1312
+ }
1313
+ // Start webhook server
1314
+ await this.startWebhookServer(serverConfig);
1315
+ // Set webhook URL with Telegram
1316
+ await this.setWebhook(webhookUrl, serverConfig.secretToken);
1317
+ console.log(`Telegram bot @${this._botUsername} connected via webhook`);
1318
+ this.setStatus('connected');
1319
+ }
1320
+ catch (error) {
1321
+ const err = error instanceof Error ? error : new Error(String(error));
1322
+ this.setStatus('error', err);
1323
+ throw err;
1324
+ }
1325
+ }
1326
+ // Private methods
1327
+ mapContextToMessage(ctx, overrideText) {
1328
+ const msg = ctx.message;
1329
+ const from = msg.from;
1330
+ const chat = msg.chat;
1331
+ // Check for forum topic (message_thread_id indicates a forum topic)
1332
+ const threadId = msg.message_thread_id?.toString();
1333
+ const isForumTopic = msg.is_topic_message === true || threadId !== undefined;
1334
+ return {
1335
+ messageId: msg.message_id.toString(),
1336
+ channel: 'telegram',
1337
+ userId: from.id.toString(),
1338
+ userName: from.first_name + (from.last_name ? ` ${from.last_name}` : ''),
1339
+ chatId: chat.id.toString(),
1340
+ text: overrideText ?? msg.text ?? '',
1341
+ timestamp: new Date(msg.date * 1000),
1342
+ replyTo: msg.reply_to_message?.message_id.toString(),
1343
+ threadId,
1344
+ isForumTopic,
1345
+ raw: ctx,
1346
+ };
1347
+ }
1348
+ async handleIncomingMessage(message) {
1349
+ for (const handler of this.messageHandlers) {
1350
+ try {
1351
+ await handler(message);
1352
+ }
1353
+ catch (error) {
1354
+ console.error('Error in message handler:', error);
1355
+ this.handleError(error instanceof Error ? error : new Error(String(error)), 'messageHandler');
1356
+ }
1357
+ }
1358
+ }
1359
+ handleError(error, context) {
1360
+ for (const handler of this.errorHandlers) {
1361
+ try {
1362
+ handler(error, context);
1363
+ }
1364
+ catch (e) {
1365
+ console.error('Error in error handler:', e);
1366
+ }
1367
+ }
1368
+ }
1369
+ setStatus(status, error) {
1370
+ this._status = status;
1371
+ for (const handler of this.statusHandlers) {
1372
+ try {
1373
+ handler(status, error);
1374
+ }
1375
+ catch (e) {
1376
+ console.error('Error in status handler:', e);
1377
+ }
1378
+ }
1379
+ }
1380
+ }
1381
+ exports.TelegramAdapter = TelegramAdapter;
1382
+ /**
1383
+ * Create a Telegram adapter from configuration
1384
+ */
1385
+ function createTelegramAdapter(config) {
1386
+ if (!config.botToken) {
1387
+ throw new Error('Telegram bot token is required');
1388
+ }
1389
+ return new TelegramAdapter(config);
1390
+ }