agent-messenger 2.10.2 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.env.template +4 -1
  3. package/README.md +77 -27
  4. package/bun.lock +26 -0
  5. package/dist/package.json +21 -1
  6. package/dist/src/platforms/channeltalk/commands/auth.d.ts +2 -1
  7. package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
  8. package/dist/src/platforms/channeltalk/commands/auth.js +5 -3
  9. package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
  10. package/dist/src/platforms/channeltalk/token-extractor.d.ts +2 -1
  11. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  12. package/dist/src/platforms/channeltalk/token-extractor.js +22 -6
  13. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  14. package/dist/src/platforms/channeltalkbot/cli.d.ts.map +1 -1
  15. package/dist/src/platforms/channeltalkbot/cli.js +11 -1
  16. package/dist/src/platforms/channeltalkbot/cli.js.map +1 -1
  17. package/dist/src/platforms/channeltalkbot/commands/auth.d.ts.map +1 -1
  18. package/dist/src/platforms/channeltalkbot/commands/auth.js +1 -5
  19. package/dist/src/platforms/channeltalkbot/commands/auth.js.map +1 -1
  20. package/dist/src/platforms/channeltalkbot/commands/bot.d.ts.map +1 -1
  21. package/dist/src/platforms/channeltalkbot/commands/bot.js +1 -6
  22. package/dist/src/platforms/channeltalkbot/commands/bot.js.map +1 -1
  23. package/dist/src/platforms/channeltalkbot/commands/chat.d.ts.map +1 -1
  24. package/dist/src/platforms/channeltalkbot/commands/chat.js +1 -6
  25. package/dist/src/platforms/channeltalkbot/commands/chat.js.map +1 -1
  26. package/dist/src/platforms/channeltalkbot/commands/group.d.ts.map +1 -1
  27. package/dist/src/platforms/channeltalkbot/commands/group.js +1 -6
  28. package/dist/src/platforms/channeltalkbot/commands/group.js.map +1 -1
  29. package/dist/src/platforms/channeltalkbot/commands/manager.d.ts.map +1 -1
  30. package/dist/src/platforms/channeltalkbot/commands/manager.js +1 -6
  31. package/dist/src/platforms/channeltalkbot/commands/manager.js.map +1 -1
  32. package/dist/src/platforms/channeltalkbot/commands/message.d.ts.map +1 -1
  33. package/dist/src/platforms/channeltalkbot/commands/message.js +1 -6
  34. package/dist/src/platforms/channeltalkbot/commands/message.js.map +1 -1
  35. package/dist/src/platforms/channeltalkbot/commands/whoami.d.ts.map +1 -1
  36. package/dist/src/platforms/channeltalkbot/commands/whoami.js +1 -6
  37. package/dist/src/platforms/channeltalkbot/commands/whoami.js.map +1 -1
  38. package/dist/src/platforms/channeltalkbot/credential-manager.d.ts +5 -0
  39. package/dist/src/platforms/channeltalkbot/credential-manager.d.ts.map +1 -1
  40. package/dist/src/platforms/channeltalkbot/credential-manager.js +34 -4
  41. package/dist/src/platforms/channeltalkbot/credential-manager.js.map +1 -1
  42. package/dist/src/platforms/discord/commands/auth.d.ts +1 -0
  43. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  44. package/dist/src/platforms/discord/commands/auth.js +3 -1
  45. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  46. package/dist/src/platforms/discord/listener.d.ts +2 -0
  47. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  48. package/dist/src/platforms/discord/listener.js +51 -21
  49. package/dist/src/platforms/discord/listener.js.map +1 -1
  50. package/dist/src/platforms/discord/token-extractor.d.ts +2 -1
  51. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  52. package/dist/src/platforms/discord/token-extractor.js +21 -6
  53. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  54. package/dist/src/platforms/discordbot/cli.d.ts.map +1 -1
  55. package/dist/src/platforms/discordbot/cli.js +12 -1
  56. package/dist/src/platforms/discordbot/cli.js.map +1 -1
  57. package/dist/src/platforms/discordbot/client.d.ts +3 -0
  58. package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
  59. package/dist/src/platforms/discordbot/client.js +3 -0
  60. package/dist/src/platforms/discordbot/client.js.map +1 -1
  61. package/dist/src/platforms/discordbot/commands/auth.d.ts.map +1 -1
  62. package/dist/src/platforms/discordbot/commands/auth.js +1 -5
  63. package/dist/src/platforms/discordbot/commands/auth.js.map +1 -1
  64. package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
  65. package/dist/src/platforms/discordbot/commands/message.js +1 -6
  66. package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
  67. package/dist/src/platforms/discordbot/commands/server.d.ts.map +1 -1
  68. package/dist/src/platforms/discordbot/commands/server.js +1 -4
  69. package/dist/src/platforms/discordbot/commands/server.js.map +1 -1
  70. package/dist/src/platforms/discordbot/commands/whoami.d.ts.map +1 -1
  71. package/dist/src/platforms/discordbot/commands/whoami.js +1 -6
  72. package/dist/src/platforms/discordbot/commands/whoami.js.map +1 -1
  73. package/dist/src/platforms/discordbot/index.d.ts +3 -1
  74. package/dist/src/platforms/discordbot/index.d.ts.map +1 -1
  75. package/dist/src/platforms/discordbot/index.js +2 -1
  76. package/dist/src/platforms/discordbot/index.js.map +1 -1
  77. package/dist/src/platforms/discordbot/listener.d.ts +43 -0
  78. package/dist/src/platforms/discordbot/listener.d.ts.map +1 -0
  79. package/dist/src/platforms/discordbot/listener.js +292 -0
  80. package/dist/src/platforms/discordbot/listener.js.map +1 -0
  81. package/dist/src/platforms/discordbot/types.d.ts +161 -0
  82. package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
  83. package/dist/src/platforms/discordbot/types.js +34 -0
  84. package/dist/src/platforms/discordbot/types.js.map +1 -1
  85. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  86. package/dist/src/platforms/instagram/commands/auth.js +3 -1
  87. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  88. package/dist/src/platforms/instagram/token-extractor.d.ts +2 -1
  89. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -1
  90. package/dist/src/platforms/instagram/token-extractor.js +11 -2
  91. package/dist/src/platforms/instagram/token-extractor.js.map +1 -1
  92. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  93. package/dist/src/platforms/slack/commands/auth.js +4 -2
  94. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  95. package/dist/src/platforms/slack/token-extractor.d.ts +4 -1
  96. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  97. package/dist/src/platforms/slack/token-extractor.js +64 -15
  98. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  99. package/dist/src/platforms/slackbot/cli.d.ts.map +1 -1
  100. package/dist/src/platforms/slackbot/cli.js +15 -3
  101. package/dist/src/platforms/slackbot/cli.js.map +1 -1
  102. package/dist/src/platforms/slackbot/client.d.ts +22 -1
  103. package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
  104. package/dist/src/platforms/slackbot/client.js +104 -1
  105. package/dist/src/platforms/slackbot/client.js.map +1 -1
  106. package/dist/src/platforms/slackbot/commands/auth.d.ts.map +1 -1
  107. package/dist/src/platforms/slackbot/commands/auth.js +1 -5
  108. package/dist/src/platforms/slackbot/commands/auth.js.map +1 -1
  109. package/dist/src/platforms/slackbot/commands/file.d.ts +3 -0
  110. package/dist/src/platforms/slackbot/commands/file.d.ts.map +1 -0
  111. package/dist/src/platforms/slackbot/commands/file.js +164 -0
  112. package/dist/src/platforms/slackbot/commands/file.js.map +1 -0
  113. package/dist/src/platforms/slackbot/commands/index.d.ts +1 -0
  114. package/dist/src/platforms/slackbot/commands/index.d.ts.map +1 -1
  115. package/dist/src/platforms/slackbot/commands/index.js +1 -0
  116. package/dist/src/platforms/slackbot/commands/index.js.map +1 -1
  117. package/dist/src/platforms/slackbot/commands/message.d.ts.map +1 -1
  118. package/dist/src/platforms/slackbot/commands/message.js +19 -0
  119. package/dist/src/platforms/slackbot/commands/message.js.map +1 -1
  120. package/dist/src/platforms/slackbot/commands/whoami.d.ts.map +1 -1
  121. package/dist/src/platforms/slackbot/commands/whoami.js +1 -6
  122. package/dist/src/platforms/slackbot/commands/whoami.js.map +1 -1
  123. package/dist/src/platforms/slackbot/credential-manager.d.ts +1 -0
  124. package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -1
  125. package/dist/src/platforms/slackbot/credential-manager.js +30 -2
  126. package/dist/src/platforms/slackbot/credential-manager.js.map +1 -1
  127. package/dist/src/platforms/slackbot/index.d.ts +4 -1
  128. package/dist/src/platforms/slackbot/index.d.ts.map +1 -1
  129. package/dist/src/platforms/slackbot/index.js +1 -0
  130. package/dist/src/platforms/slackbot/index.js.map +1 -1
  131. package/dist/src/platforms/slackbot/listener.d.ts +44 -0
  132. package/dist/src/platforms/slackbot/listener.d.ts.map +1 -0
  133. package/dist/src/platforms/slackbot/listener.js +313 -0
  134. package/dist/src/platforms/slackbot/listener.js.map +1 -0
  135. package/dist/src/platforms/slackbot/types.d.ts +196 -1
  136. package/dist/src/platforms/slackbot/types.d.ts.map +1 -1
  137. package/dist/src/platforms/slackbot/types.js +4 -1
  138. package/dist/src/platforms/slackbot/types.js.map +1 -1
  139. package/dist/src/platforms/teams/commands/auth.d.ts +1 -0
  140. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  141. package/dist/src/platforms/teams/commands/auth.js +37 -6
  142. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  143. package/dist/src/platforms/teams/ensure-auth.js +31 -9
  144. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  145. package/dist/src/platforms/teams/token-extractor.d.ts +4 -1
  146. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  147. package/dist/src/platforms/teams/token-extractor.js +71 -29
  148. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  149. package/dist/src/platforms/webex/commands/auth.d.ts +1 -0
  150. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  151. package/dist/src/platforms/webex/commands/auth.js +3 -1
  152. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  153. package/dist/src/platforms/webex/token-extractor.d.ts +3 -1
  154. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  155. package/dist/src/platforms/webex/token-extractor.js +16 -2
  156. package/dist/src/platforms/webex/token-extractor.js.map +1 -1
  157. package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -1
  158. package/dist/src/platforms/wechatbot/cli.js +11 -1
  159. package/dist/src/platforms/wechatbot/cli.js.map +1 -1
  160. package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -1
  161. package/dist/src/platforms/wechatbot/commands/auth.js +1 -5
  162. package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -1
  163. package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -1
  164. package/dist/src/platforms/wechatbot/commands/message.js +1 -6
  165. package/dist/src/platforms/wechatbot/commands/message.js.map +1 -1
  166. package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -1
  167. package/dist/src/platforms/wechatbot/commands/template.js +1 -6
  168. package/dist/src/platforms/wechatbot/commands/template.js.map +1 -1
  169. package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -1
  170. package/dist/src/platforms/wechatbot/commands/user.js +1 -6
  171. package/dist/src/platforms/wechatbot/commands/user.js.map +1 -1
  172. package/dist/src/platforms/wechatbot/commands/whoami.d.ts.map +1 -1
  173. package/dist/src/platforms/wechatbot/commands/whoami.js +1 -6
  174. package/dist/src/platforms/wechatbot/commands/whoami.js.map +1 -1
  175. package/dist/src/platforms/whatsappbot/cli.d.ts.map +1 -1
  176. package/dist/src/platforms/whatsappbot/cli.js +11 -1
  177. package/dist/src/platforms/whatsappbot/cli.js.map +1 -1
  178. package/dist/src/platforms/whatsappbot/commands/auth.d.ts.map +1 -1
  179. package/dist/src/platforms/whatsappbot/commands/auth.js +1 -5
  180. package/dist/src/platforms/whatsappbot/commands/auth.js.map +1 -1
  181. package/dist/src/platforms/whatsappbot/commands/message.d.ts.map +1 -1
  182. package/dist/src/platforms/whatsappbot/commands/message.js +1 -6
  183. package/dist/src/platforms/whatsappbot/commands/message.js.map +1 -1
  184. package/dist/src/platforms/whatsappbot/commands/template.d.ts.map +1 -1
  185. package/dist/src/platforms/whatsappbot/commands/template.js +1 -6
  186. package/dist/src/platforms/whatsappbot/commands/template.js.map +1 -1
  187. package/dist/src/platforms/whatsappbot/commands/whoami.d.ts.map +1 -1
  188. package/dist/src/platforms/whatsappbot/commands/whoami.js +1 -6
  189. package/dist/src/platforms/whatsappbot/commands/whoami.js.map +1 -1
  190. package/dist/src/shared/chromium/browsers.d.ts +8 -0
  191. package/dist/src/shared/chromium/browsers.d.ts.map +1 -1
  192. package/dist/src/shared/chromium/browsers.js +58 -3
  193. package/dist/src/shared/chromium/browsers.js.map +1 -1
  194. package/dist/src/shared/chromium/cli-options.d.ts +5 -0
  195. package/dist/src/shared/chromium/cli-options.d.ts.map +1 -0
  196. package/dist/src/shared/chromium/cli-options.js +8 -0
  197. package/dist/src/shared/chromium/cli-options.js.map +1 -0
  198. package/dist/src/shared/chromium/index.d.ts +3 -1
  199. package/dist/src/shared/chromium/index.d.ts.map +1 -1
  200. package/dist/src/shared/chromium/index.js +2 -1
  201. package/dist/src/shared/chromium/index.js.map +1 -1
  202. package/dist/src/shared/utils/cli-output.d.ts +7 -0
  203. package/dist/src/shared/utils/cli-output.d.ts.map +1 -0
  204. package/dist/src/shared/utils/cli-output.js +7 -0
  205. package/dist/src/shared/utils/cli-output.js.map +1 -0
  206. package/dist/src/tui/app.d.ts.map +1 -1
  207. package/dist/src/tui/app.js +73 -20
  208. package/dist/src/tui/app.js.map +1 -1
  209. package/docs/content/docs/cli/channeltalk.mdx +4 -0
  210. package/docs/content/docs/cli/discord.mdx +5 -0
  211. package/docs/content/docs/cli/instagram.mdx +3 -0
  212. package/docs/content/docs/cli/slack.mdx +5 -0
  213. package/docs/content/docs/cli/slackbot.mdx +60 -22
  214. package/docs/content/docs/cli/teams.mdx +5 -0
  215. package/docs/content/docs/cli/webex.mdx +3 -0
  216. package/docs/content/docs/sdk/channeltalkbot.mdx +38 -1
  217. package/docs/content/docs/sdk/discordbot.mdx +501 -0
  218. package/docs/content/docs/sdk/meta.json +2 -0
  219. package/docs/content/docs/sdk/slackbot.mdx +576 -0
  220. package/e2e/README.md +1 -1
  221. package/e2e/config.ts +9 -4
  222. package/examples/discordbot-listen.ts +65 -0
  223. package/examples/slackbot-listen.ts +65 -0
  224. package/package.json +21 -1
  225. package/skills/agent-channeltalk/SKILL.md +5 -1
  226. package/skills/agent-channeltalk/references/authentication.md +5 -1
  227. package/skills/agent-channeltalkbot/SKILL.md +17 -3
  228. package/skills/agent-channeltalkbot/references/authentication.md +7 -5
  229. package/skills/agent-discord/SKILL.md +5 -1
  230. package/skills/agent-discord/references/authentication.md +7 -1
  231. package/skills/agent-discordbot/SKILL.md +13 -2
  232. package/skills/agent-discordbot/references/common-patterns.md +1 -1
  233. package/skills/agent-instagram/SKILL.md +7 -1
  234. package/skills/agent-instagram/references/authentication.md +6 -0
  235. package/skills/agent-kakaotalk/SKILL.md +1 -1
  236. package/skills/agent-line/SKILL.md +1 -1
  237. package/skills/agent-slack/SKILL.md +5 -1
  238. package/skills/agent-slack/references/authentication.md +7 -1
  239. package/skills/agent-slackbot/SKILL.md +56 -4
  240. package/skills/agent-slackbot/references/authentication.md +4 -0
  241. package/skills/agent-teams/SKILL.md +5 -1
  242. package/skills/agent-teams/references/authentication.md +7 -1
  243. package/skills/agent-telegram/SKILL.md +1 -1
  244. package/skills/agent-webex/SKILL.md +7 -1
  245. package/skills/agent-webex/references/authentication.md +6 -0
  246. package/skills/agent-wechatbot/SKILL.md +16 -1
  247. package/skills/agent-wechatbot/references/authentication.md +219 -0
  248. package/skills/agent-wechatbot/references/common-patterns.md +358 -0
  249. package/skills/agent-wechatbot/templates/account-summary.sh +122 -0
  250. package/skills/agent-wechatbot/templates/post-message.sh +122 -0
  251. package/skills/agent-wechatbot/templates/send-template.sh +152 -0
  252. package/skills/agent-whatsapp/SKILL.md +1 -1
  253. package/skills/agent-whatsappbot/SKILL.md +30 -1
  254. package/src/platforms/channeltalk/commands/auth.test.ts +15 -3
  255. package/src/platforms/channeltalk/commands/auth.ts +15 -5
  256. package/src/platforms/channeltalk/token-extractor.ts +24 -5
  257. package/src/platforms/channeltalkbot/cli.ts +9 -0
  258. package/src/platforms/channeltalkbot/commands/auth.ts +1 -5
  259. package/src/platforms/channeltalkbot/commands/bot.ts +1 -6
  260. package/src/platforms/channeltalkbot/commands/chat.ts +1 -6
  261. package/src/platforms/channeltalkbot/commands/group.ts +1 -6
  262. package/src/platforms/channeltalkbot/commands/manager.ts +1 -6
  263. package/src/platforms/channeltalkbot/commands/message.ts +1 -6
  264. package/src/platforms/channeltalkbot/commands/whoami.test.ts +2 -0
  265. package/src/platforms/channeltalkbot/commands/whoami.ts +1 -6
  266. package/src/platforms/channeltalkbot/credential-manager.test.ts +96 -2
  267. package/src/platforms/channeltalkbot/credential-manager.ts +37 -4
  268. package/src/platforms/discord/commands/auth.ts +13 -2
  269. package/src/platforms/discord/listener.test.ts +59 -1
  270. package/src/platforms/discord/listener.ts +43 -19
  271. package/src/platforms/discord/token-extractor.ts +30 -6
  272. package/src/platforms/discordbot/cli.ts +10 -0
  273. package/src/platforms/discordbot/client.ts +4 -0
  274. package/src/platforms/discordbot/commands/auth.ts +1 -5
  275. package/src/platforms/discordbot/commands/message.ts +1 -6
  276. package/src/platforms/discordbot/commands/server.ts +1 -5
  277. package/src/platforms/discordbot/commands/whoami.ts +1 -6
  278. package/src/platforms/discordbot/index.test.ts +82 -0
  279. package/src/platforms/discordbot/index.ts +27 -9
  280. package/src/platforms/discordbot/listener.test.ts +1002 -0
  281. package/src/platforms/discordbot/listener.ts +321 -0
  282. package/src/platforms/discordbot/types.ts +163 -0
  283. package/src/platforms/instagram/commands/auth.ts +9 -1
  284. package/src/platforms/instagram/token-extractor.ts +13 -1
  285. package/src/platforms/slack/commands/auth.ts +11 -2
  286. package/src/platforms/slack/token-extractor.test.ts +96 -0
  287. package/src/platforms/slack/token-extractor.ts +76 -13
  288. package/src/platforms/slackbot/cli.ts +13 -1
  289. package/src/platforms/slackbot/client.test.ts +274 -0
  290. package/src/platforms/slackbot/client.ts +130 -2
  291. package/src/platforms/slackbot/commands/auth.ts +1 -5
  292. package/src/platforms/slackbot/commands/file.test.ts +201 -0
  293. package/src/platforms/slackbot/commands/file.ts +212 -0
  294. package/src/platforms/slackbot/commands/index.ts +1 -0
  295. package/src/platforms/slackbot/commands/message.ts +22 -0
  296. package/src/platforms/slackbot/commands/whoami.ts +1 -6
  297. package/src/platforms/slackbot/credential-manager.test.ts +62 -2
  298. package/src/platforms/slackbot/credential-manager.ts +32 -2
  299. package/src/platforms/slackbot/index.test.ts +59 -0
  300. package/src/platforms/slackbot/index.ts +31 -7
  301. package/src/platforms/slackbot/listener.test.ts +1012 -0
  302. package/src/platforms/slackbot/listener.ts +362 -0
  303. package/src/platforms/slackbot/types.ts +224 -1
  304. package/src/platforms/teams/commands/auth.test.ts +1 -1
  305. package/src/platforms/teams/commands/auth.ts +66 -7
  306. package/src/platforms/teams/ensure-auth.test.ts +56 -5
  307. package/src/platforms/teams/ensure-auth.ts +39 -11
  308. package/src/platforms/teams/token-extractor.test.ts +146 -24
  309. package/src/platforms/teams/token-extractor.ts +87 -29
  310. package/src/platforms/webex/commands/auth.ts +13 -2
  311. package/src/platforms/webex/token-extractor.ts +25 -3
  312. package/src/platforms/wechatbot/cli.ts +9 -0
  313. package/src/platforms/wechatbot/commands/auth.ts +1 -5
  314. package/src/platforms/wechatbot/commands/message.ts +1 -6
  315. package/src/platforms/wechatbot/commands/template.ts +1 -6
  316. package/src/platforms/wechatbot/commands/user.ts +1 -6
  317. package/src/platforms/wechatbot/commands/whoami.ts +1 -6
  318. package/src/platforms/whatsappbot/cli.ts +9 -0
  319. package/src/platforms/whatsappbot/commands/auth.ts +1 -5
  320. package/src/platforms/whatsappbot/commands/message.ts +1 -6
  321. package/src/platforms/whatsappbot/commands/template.ts +1 -6
  322. package/src/platforms/whatsappbot/commands/whoami.ts +1 -6
  323. package/src/shared/chromium/browsers.test.ts +80 -0
  324. package/src/shared/chromium/browsers.ts +72 -3
  325. package/src/shared/chromium/cli-options.test.ts +22 -0
  326. package/src/shared/chromium/cli-options.ts +12 -0
  327. package/src/shared/chromium/index.ts +3 -0
  328. package/src/shared/utils/cli-output.test.ts +57 -0
  329. package/src/shared/utils/cli-output.ts +8 -0
  330. package/src/tui/app.ts +129 -20
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
 
3
- import { formatOutput } from '@/shared/utils/output'
3
+ import { cliOutput } from '@/shared/utils/cli-output'
4
4
 
5
5
  import type { WorkspaceOption } from './shared'
6
6
  import { getClient } from './shared'
@@ -89,11 +89,6 @@ export async function deleteAction(botId: string, options: BotOptions): Promise<
89
89
  }
90
90
  }
91
91
 
92
- function cliOutput(result: BotResult, pretty?: boolean): void {
93
- console.log(formatOutput(result, pretty))
94
- if (result.error) process.exit(1)
95
- }
96
-
97
92
  export const botCommand = new Command('bot')
98
93
  .description('Bot management commands')
99
94
  .addCommand(
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
 
3
- import { formatOutput } from '@/shared/utils/output'
3
+ import { cliOutput } from '@/shared/utils/cli-output'
4
4
 
5
5
  import type { WorkspaceOption } from './shared'
6
6
  import { getClient, getDefaultBotName } from './shared'
@@ -120,11 +120,6 @@ export async function deleteAction(chatId: string, options: ChatOptions): Promis
120
120
  }
121
121
  }
122
122
 
123
- function cliOutput(result: ChatResult, pretty?: boolean): void {
124
- console.log(formatOutput(result, pretty))
125
- if (result.error) process.exit(1)
126
- }
127
-
128
123
  export const chatCommand = new Command('chat')
129
124
  .description('UserChat management commands')
130
125
  .addCommand(
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
 
3
- import { formatOutput } from '@/shared/utils/output'
3
+ import { cliOutput } from '@/shared/utils/cli-output'
4
4
 
5
5
  import type { WorkspaceOption } from './shared'
6
6
  import { getClient } from './shared'
@@ -93,11 +93,6 @@ export async function messagesAction(group: string, options: GroupOptions): Prom
93
93
  }
94
94
  }
95
95
 
96
- function cliOutput(result: GroupResult, pretty?: boolean): void {
97
- console.log(formatOutput(result, pretty))
98
- if (result.error) process.exit(1)
99
- }
100
-
101
96
  export const groupCommand = new Command('group')
102
97
  .description('Group management commands')
103
98
  .addCommand(
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
 
3
- import { formatOutput } from '@/shared/utils/output'
3
+ import { cliOutput } from '@/shared/utils/cli-output'
4
4
 
5
5
  import type { WorkspaceOption } from './shared'
6
6
  import { getClient } from './shared'
@@ -68,11 +68,6 @@ export async function getAction(managerId: string, options: ManagerOptions): Pro
68
68
  }
69
69
  }
70
70
 
71
- function cliOutput(result: ManagerResult, pretty?: boolean): void {
72
- console.log(formatOutput(result, pretty))
73
- if (result.error) process.exit(1)
74
- }
75
-
76
71
  export const managerCommand = new Command('manager')
77
72
  .description('Manager commands')
78
73
  .addCommand(
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
 
3
- import { formatOutput } from '@/shared/utils/output'
3
+ import { cliOutput } from '@/shared/utils/cli-output'
4
4
 
5
5
  import { wrapTextInBlocks } from '../message-utils'
6
6
  import type { WorkspaceOption } from './shared'
@@ -140,11 +140,6 @@ function detectTargetType(target: string): 'userchat' | 'group' {
140
140
  return 'userchat'
141
141
  }
142
142
 
143
- function cliOutput(result: MessageResult, pretty?: boolean): void {
144
- console.log(formatOutput(result, pretty))
145
- if (result.error) process.exit(1)
146
- }
147
-
148
143
  export const messageCommand = new Command('message')
149
144
  .description('Message commands')
150
145
  .addCommand(
@@ -35,6 +35,8 @@ describe('whoami command', () => {
35
35
  originalEnv = { ...process.env }
36
36
  delete process.env.E2E_CHANNELBOT_ACCESS_KEY
37
37
  delete process.env.E2E_CHANNELBOT_ACCESS_SECRET
38
+ delete process.env.E2E_CHANNELTALKBOT_ACCESS_KEY
39
+ delete process.env.E2E_CHANNELTALKBOT_ACCESS_SECRET
38
40
  mockGetChannel.mockClear()
39
41
  })
40
42
 
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
 
3
- import { formatOutput } from '@/shared/utils/output'
3
+ import { cliOutput } from '@/shared/utils/cli-output'
4
4
 
5
5
  import type { WorkspaceOption } from './shared'
6
6
  import { getClient } from './shared'
@@ -28,11 +28,6 @@ export async function whoamiAction(options: WorkspaceOption): Promise<WhoamiResu
28
28
  }
29
29
  }
30
30
 
31
- function cliOutput(result: WhoamiResult, pretty?: boolean): void {
32
- console.log(formatOutput(result, pretty))
33
- if (result.error) process.exit(1)
34
- }
35
-
36
31
  export const whoamiCommand = new Command('whoami')
37
32
  .description('Show current authenticated bot')
38
33
  .option('--workspace <id>', 'Workspace ID to use')
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2
2
  import { existsSync, rmSync } from 'node:fs'
3
- import { mkdir, stat } from 'node:fs/promises'
3
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
 
@@ -36,6 +36,8 @@ describe('ChannelBotCredentialManager', () => {
36
36
  }
37
37
  delete process.env.E2E_CHANNELBOT_ACCESS_KEY
38
38
  delete process.env.E2E_CHANNELBOT_ACCESS_SECRET
39
+ delete process.env.E2E_CHANNELTALKBOT_ACCESS_KEY
40
+ delete process.env.E2E_CHANNELTALKBOT_ACCESS_SECRET
39
41
  })
40
42
 
41
43
  describe('load', () => {
@@ -299,10 +301,102 @@ describe('ChannelBotCredentialManager', () => {
299
301
  it('saves file with secure permissions (600)', async () => {
300
302
  await manager.setCredentials(WORKSPACE_A)
301
303
 
302
- const credPath = join(tempDir, 'channelbot-credentials.json')
304
+ const credPath = join(tempDir, 'channeltalkbot-credentials.json')
303
305
  const stats = await stat(credPath)
304
306
 
305
307
  expect(stats.mode & 0o777).toBe(0o600)
306
308
  })
307
309
  })
310
+
311
+ describe('legacy filename migration', () => {
312
+ it('renames channelbot-credentials.json to channeltalkbot-credentials.json on load', async () => {
313
+ const legacyPath = join(tempDir, 'channelbot-credentials.json')
314
+ const newPath = join(tempDir, 'channeltalkbot-credentials.json')
315
+ const legacyConfig = {
316
+ current: { workspace_id: WORKSPACE_A.workspace_id },
317
+ workspaces: { [WORKSPACE_A.workspace_id]: WORKSPACE_A },
318
+ default_bot: null,
319
+ }
320
+ await writeFile(legacyPath, JSON.stringify(legacyConfig))
321
+
322
+ const config = await manager.load()
323
+
324
+ expect(config.workspaces[WORKSPACE_A.workspace_id]).toEqual(WORKSPACE_A)
325
+ expect(existsSync(legacyPath)).toBe(false)
326
+ expect(existsSync(newPath)).toBe(true)
327
+ const migrated = JSON.parse(await readFile(newPath, 'utf-8'))
328
+ expect(migrated.workspaces[WORKSPACE_A.workspace_id]).toEqual(WORKSPACE_A)
329
+ })
330
+
331
+ it('does not overwrite an existing channeltalkbot-credentials.json', async () => {
332
+ const legacyPath = join(tempDir, 'channelbot-credentials.json')
333
+ const newPath = join(tempDir, 'channeltalkbot-credentials.json')
334
+ await writeFile(legacyPath, JSON.stringify({ workspaces: { stale: WORKSPACE_A } }))
335
+ const newConfig = {
336
+ current: { workspace_id: WORKSPACE_B.workspace_id },
337
+ workspaces: { [WORKSPACE_B.workspace_id]: WORKSPACE_B },
338
+ default_bot: null,
339
+ }
340
+ await writeFile(newPath, JSON.stringify(newConfig))
341
+
342
+ const config = await manager.load()
343
+
344
+ expect(config.workspaces[WORKSPACE_B.workspace_id]).toEqual(WORKSPACE_B)
345
+ expect(config.workspaces['stale']).toBeUndefined()
346
+ expect(existsSync(legacyPath)).toBe(true)
347
+ })
348
+
349
+ it('does not write back to legacy path when migration fails (no split-brain)', async () => {
350
+ const legacyPath = join(tempDir, 'channelbot-credentials.json')
351
+ const newPath = join(tempDir, 'channeltalkbot-credentials.json')
352
+ await writeFile(legacyPath, JSON.stringify({ workspaces: {}, default_bot: null }))
353
+
354
+ // Inject a rename that fails on first call to simulate a concurrent migration losing the race.
355
+ class FailingMigrationManager extends ChannelBotCredentialManager {
356
+ constructor(dir: string) {
357
+ super(dir)
358
+ let called = false
359
+ this.renameFile = async () => {
360
+ if (!called) {
361
+ called = true
362
+ throw new Error('ENOENT: simulated concurrent rename winner')
363
+ }
364
+ }
365
+ }
366
+ }
367
+ const failingManager = new FailingMigrationManager(tempDir)
368
+
369
+ await failingManager.load()
370
+ await failingManager.setCredentials(WORKSPACE_A)
371
+
372
+ // After failure, the manager must write to the NEW path, never re-create the legacy file.
373
+ expect(existsSync(newPath)).toBe(true)
374
+ const persisted = JSON.parse(await readFile(newPath, 'utf-8'))
375
+ expect(persisted.workspaces[WORKSPACE_A.workspace_id]).toEqual(WORKSPACE_A)
376
+ })
377
+ })
378
+
379
+ describe('env var prefix compatibility', () => {
380
+ it('prefers E2E_CHANNELTALKBOT_* over E2E_CHANNELBOT_*', async () => {
381
+ process.env.E2E_CHANNELBOT_ACCESS_KEY = 'old-key'
382
+ process.env.E2E_CHANNELBOT_ACCESS_SECRET = 'old-secret'
383
+ process.env.E2E_CHANNELTALKBOT_ACCESS_KEY = 'new-key'
384
+ process.env.E2E_CHANNELTALKBOT_ACCESS_SECRET = 'new-secret'
385
+
386
+ const creds = await manager.getCredentials()
387
+
388
+ expect(creds?.access_key).toBe('new-key')
389
+ expect(creds?.access_secret).toBe('new-secret')
390
+ })
391
+
392
+ it('falls back to E2E_CHANNELBOT_* when E2E_CHANNELTALKBOT_* is unset', async () => {
393
+ process.env.E2E_CHANNELBOT_ACCESS_KEY = 'legacy-key'
394
+ process.env.E2E_CHANNELBOT_ACCESS_SECRET = 'legacy-secret'
395
+
396
+ const creds = await manager.getCredentials()
397
+
398
+ expect(creds?.access_key).toBe('legacy-key')
399
+ expect(creds?.access_secret).toBe('legacy-secret')
400
+ })
401
+ })
308
402
  })
@@ -1,21 +1,30 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
2
+ import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises'
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
6
  import type { ChannelBotConfig, ChannelBotCredentials, ChannelBotWorkspaceEntry } from './types'
7
7
  import { ChannelBotConfigSchema } from './types'
8
8
 
9
+ const LEGACY_FILENAME = 'channelbot-credentials.json'
10
+ const CREDENTIALS_FILENAME = 'channeltalkbot-credentials.json'
11
+
9
12
  export class ChannelBotCredentialManager {
10
13
  private configDir: string
11
14
  private credentialsPath: string
15
+ private legacyPath: string
16
+ private migratedLegacyFile = false
17
+ protected renameFile: typeof rename = rename
12
18
 
13
19
  constructor(configDir?: string) {
14
20
  this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
15
- this.credentialsPath = join(this.configDir, 'channelbot-credentials.json')
21
+ this.credentialsPath = join(this.configDir, CREDENTIALS_FILENAME)
22
+ this.legacyPath = join(this.configDir, LEGACY_FILENAME)
16
23
  }
17
24
 
18
25
  async load(): Promise<ChannelBotConfig> {
26
+ await this.migrateLegacyFileIfNeeded()
27
+
19
28
  if (!existsSync(this.credentialsPath)) {
20
29
  return { current: null, workspaces: {}, default_bot: null }
21
30
  }
@@ -34,6 +43,30 @@ export class ChannelBotCredentialManager {
34
43
  return parsed.data
35
44
  }
36
45
 
46
+ private async migrateLegacyFileIfNeeded(): Promise<void> {
47
+ if (this.migratedLegacyFile) return
48
+ if (existsSync(this.credentialsPath)) {
49
+ this.migratedLegacyFile = true
50
+ return
51
+ }
52
+ if (!existsSync(this.legacyPath)) {
53
+ this.migratedLegacyFile = true
54
+ return
55
+ }
56
+ try {
57
+ await this.renameFile(this.legacyPath, this.credentialsPath)
58
+ process.stderr.write(
59
+ `[agent-channeltalkbot] Migrated credentials: ${LEGACY_FILENAME} -> ${CREDENTIALS_FILENAME}\n`,
60
+ )
61
+ } catch {
62
+ // Rename failed. If a concurrent process succeeded, the new file now exists — use it.
63
+ // Otherwise (real failure: permissions, etc.) keep the new path; load() will return
64
+ // empty config and the user can re-run `auth set`. Never fall back to writing the
65
+ // legacy path, which would resurrect the split-brain we are migrating away from.
66
+ }
67
+ this.migratedLegacyFile = true
68
+ }
69
+
37
70
  async save(config: ChannelBotConfig): Promise<void> {
38
71
  await mkdir(this.configDir, { recursive: true })
39
72
  await writeFile(this.credentialsPath, JSON.stringify(config, null, 2), { mode: 0o600 })
@@ -41,8 +74,8 @@ export class ChannelBotCredentialManager {
41
74
  }
42
75
 
43
76
  async getCredentials(workspaceId?: string): Promise<ChannelBotCredentials | null> {
44
- const envAccessKey = process.env.E2E_CHANNELBOT_ACCESS_KEY
45
- const envAccessSecret = process.env.E2E_CHANNELBOT_ACCESS_SECRET
77
+ const envAccessKey = process.env.E2E_CHANNELTALKBOT_ACCESS_KEY ?? process.env.E2E_CHANNELBOT_ACCESS_KEY
78
+ const envAccessSecret = process.env.E2E_CHANNELTALKBOT_ACCESS_SECRET ?? process.env.E2E_CHANNELBOT_ACCESS_SECRET
46
79
 
47
80
  if (envAccessKey && envAccessSecret && !workspaceId) {
48
81
  return {
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander'
2
2
 
3
+ import { collectBrowserProfileOption } from '@/shared/chromium'
3
4
  import { handleError } from '@/shared/utils/error-handler'
4
5
  import { formatOutput } from '@/shared/utils/output'
5
6
  import { debug } from '@/shared/utils/stderr'
@@ -8,9 +9,13 @@ import { DiscordClient } from '../client'
8
9
  import { DiscordCredentialManager } from '../credential-manager'
9
10
  import { DiscordTokenExtractor } from '../token-extractor'
10
11
 
11
- export async function extractAction(options: { pretty?: boolean; debug?: boolean }): Promise<void> {
12
+ export async function extractAction(options: {
13
+ pretty?: boolean
14
+ debug?: boolean
15
+ browserProfile?: string[]
16
+ }): Promise<void> {
12
17
  try {
13
- const extractor = new DiscordTokenExtractor()
18
+ const extractor = new DiscordTokenExtractor(undefined, undefined, undefined, undefined, options.browserProfile)
14
19
 
15
20
  if (process.platform === 'darwin') {
16
21
  console.log('')
@@ -195,6 +200,12 @@ export const authCommand = new Command('auth')
195
200
  .description('Extract token from Discord desktop app or a supported Chromium browser')
196
201
  .option('--pretty', 'Pretty print JSON output')
197
202
  .option('--debug', 'Show debug output for troubleshooting')
203
+ .option(
204
+ '--browser-profile <path>',
205
+ 'Additional Chromium profile/user-data directory to scan (repeatable, comma-separated supported)',
206
+ collectBrowserProfileOption,
207
+ [],
208
+ )
198
209
  .action(extractAction),
199
210
  )
200
211
  .addCommand(
@@ -16,12 +16,16 @@ let mockWsInstance: MockWs
16
16
  class MockWs {
17
17
  static OPEN = 1
18
18
  static CLOSED = 3
19
+ static lastUrl: string | null = null
19
20
  readyState = MockWs.OPEN
20
21
 
21
22
  private handlers = new Map<string, WsHandler[]>()
22
23
  sent: string[] = []
24
+ url: string
23
25
 
24
- constructor(_url: string, _options?: any) {
26
+ constructor(url: string, _options?: any) {
27
+ this.url = url
28
+ MockWs.lastUrl = url
25
29
  // oxlint-disable-next-line typescript-eslint/no-this-alias
26
30
  mockWsInstance = this
27
31
  }
@@ -785,4 +789,58 @@ describe('DiscordListener', () => {
785
789
  expect((listener as any).reconnectAttempts).toBe(0)
786
790
  })
787
791
  })
792
+
793
+ describe('reconnect URL', () => {
794
+ it('appends ?v=10&encoding=json to resume_gateway_url on reconnect', async () => {
795
+ const client = createMockClient()
796
+ listener = new DiscordListener(client)
797
+
798
+ await listener.start()
799
+ mockWsInstance.simulateOpen()
800
+ mockWsInstance.simulateHello()
801
+ mockWsInstance.simulateMessage({
802
+ op: 0,
803
+ t: 'READY',
804
+ s: 1,
805
+ d: {
806
+ session_id: 'session_xyz',
807
+ resume_gateway_url: 'wss://gateway-us-east1-b.discord.gg',
808
+ user: { id: 'U_SELF', username: 'user' },
809
+ },
810
+ })
811
+
812
+ mockWsInstance.simulateClose()
813
+ await new Promise((r) => setTimeout(r, 1500))
814
+
815
+ expect(MockWs.lastUrl).toBe('wss://gateway-us-east1-b.discord.gg?v=10&encoding=json')
816
+ })
817
+ })
818
+
819
+ describe('reconnectAttempts deferred to READY/RESUMED', () => {
820
+ it('does not reset reconnectAttempts on socket open alone', async () => {
821
+ const client = createMockClient()
822
+ listener = new DiscordListener(client)
823
+
824
+ await listener.start()
825
+ ;(listener as any).reconnectAttempts = 5
826
+
827
+ mockWsInstance.simulateOpen()
828
+
829
+ expect((listener as any).reconnectAttempts).toBe(5)
830
+ })
831
+
832
+ it('resets reconnectAttempts on READY dispatch', async () => {
833
+ const client = createMockClient()
834
+ listener = new DiscordListener(client)
835
+
836
+ await listener.start()
837
+ ;(listener as any).reconnectAttempts = 5
838
+
839
+ mockWsInstance.simulateOpen()
840
+ mockWsInstance.simulateHello()
841
+ mockWsInstance.simulateReady()
842
+
843
+ expect((listener as any).reconnectAttempts).toBe(0)
844
+ })
845
+ })
788
846
  })
@@ -7,6 +7,7 @@ import type { DiscordListenerEventMap, DiscordGatewayGenericEvent } from './type
7
7
  import { DiscordGatewayOpcode, DiscordIntent } from './types'
8
8
 
9
9
  const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json'
10
+ const GATEWAY_QUERY = '?v=10&encoding=json'
10
11
  const RECONNECT_BASE_DELAY = 1_000
11
12
  const RECONNECT_MAX_DELAY = 30_000
12
13
  const NON_RECOVERABLE_CLOSE_CODES = [4004, 4010, 4011, 4012, 4013, 4014]
@@ -40,6 +41,7 @@ export class DiscordListener {
40
41
  private resumeGatewayUrl: string | null = null
41
42
  private token: string | null = null
42
43
  private cachedUser: { id: string; username: string } | null = null
44
+ private generation = 0
43
45
 
44
46
  constructor(client: DiscordClient, options?: { intents?: number }) {
45
47
  this.client = client
@@ -50,11 +52,13 @@ export class DiscordListener {
50
52
  if (this.running) return
51
53
  this.running = true
52
54
  this.reconnectAttempts = 0
53
- await this.connect()
55
+ this.generation++
56
+ await this.connect(this.generation)
54
57
  }
55
58
 
56
59
  stop(): void {
57
60
  this.running = false
61
+ this.generation++
58
62
  this.clearTimers()
59
63
  if (this.ws) {
60
64
  this.ws.close()
@@ -82,39 +86,46 @@ export class DiscordListener {
82
86
  return this
83
87
  }
84
88
 
85
- private async connect(): Promise<void> {
86
- if (!this.running) return
89
+ private isCurrent(generation: number, ws?: WebSocket): boolean {
90
+ if (generation !== this.generation || !this.running) return false
91
+ if (ws !== undefined && this.ws !== ws) return false
92
+ return true
93
+ }
94
+
95
+ private async connect(generation: number): Promise<void> {
96
+ if (!this.isCurrent(generation)) return
87
97
 
88
98
  try {
89
99
  const { token } = await this.client.gatewayConnect()
90
- if (!this.running) return
100
+ if (!this.isCurrent(generation)) return
91
101
 
92
102
  this.token = token
93
103
 
94
- const url = this.resumeGatewayUrl ?? GATEWAY_URL
104
+ const url = this.resumeGatewayUrl ? `${this.resumeGatewayUrl}${GATEWAY_QUERY}` : GATEWAY_URL
95
105
  const ws = new WebSocket(url)
96
106
  this.ws = ws
97
107
 
98
108
  ws.on('open', () => {
99
- if (!this.running) {
109
+ if (!this.isCurrent(generation, ws)) {
100
110
  ws.close()
101
111
  return
102
112
  }
103
- this.reconnectAttempts = 0
104
113
  })
105
114
 
106
115
  ws.on('message', (raw) => {
116
+ if (!this.isCurrent(generation, ws)) return
107
117
  try {
108
118
  const data = JSON.parse(raw.toString())
109
- this.handleMessage(data)
119
+ this.handleMessage(data, generation, ws)
110
120
  } catch {
111
- // malformed message, ignore
121
+ // malformed gateway frame; ignore and let heartbeat handle liveness
112
122
  }
113
123
  })
114
124
 
115
125
  ws.on('close', (code) => {
126
+ if (!this.isCurrent(generation, ws)) return
116
127
  this.clearTimers()
117
- if (this.ws === ws) this.ws = null
128
+ this.ws = null
118
129
  if (NON_RECOVERABLE_CLOSE_CODES.includes(code)) {
119
130
  this.emitter.emit('error', new Error(`Discord gateway closed with non-recoverable code ${code}`))
120
131
  this.running = false
@@ -132,9 +143,11 @@ export class DiscordListener {
132
143
  })
133
144
 
134
145
  ws.on('error', (err) => {
146
+ if (!this.isCurrent(generation, ws)) return
135
147
  this.emitter.emit('error', err instanceof Error ? err : new Error(String(err)))
136
148
  })
137
149
  } catch (error) {
150
+ if (!this.isCurrent(generation)) return
138
151
  this.emitter.emit('error', error instanceof Error ? error : new Error(String(error)))
139
152
  if (this.running) {
140
153
  this.scheduleReconnect()
@@ -142,12 +155,13 @@ export class DiscordListener {
142
155
  }
143
156
  }
144
157
 
145
- private handleMessage(data: { op: number; d: any; s?: number; t?: string }): void {
158
+ private handleMessage(data: { op: number; d: any; s?: number; t?: string }, generation: number, ws: WebSocket): void {
159
+ if (!this.isCurrent(generation, ws)) return
146
160
  const { op, d, s, t } = data
147
161
 
148
162
  switch (op) {
149
163
  case DiscordGatewayOpcode.Hello:
150
- this.startHeartbeat(d.heartbeat_interval)
164
+ this.startHeartbeat(d.heartbeat_interval, generation, ws)
151
165
  if (this.sessionId) {
152
166
  this.sendResume()
153
167
  } else {
@@ -166,22 +180,21 @@ export class DiscordListener {
166
180
 
167
181
  case DiscordGatewayOpcode.Reconnect:
168
182
  this.reconnectAttempts = 0
169
- this.ws?.close()
183
+ ws.close()
170
184
  break
171
185
 
172
186
  case DiscordGatewayOpcode.InvalidSession: {
173
- const currentWs = this.ws
174
187
  if (d === true) {
175
188
  const delay = 1000 + Math.random() * 4000
176
189
  this.invalidSessionTimer = setTimeout(() => {
177
190
  this.invalidSessionTimer = null
178
- if (currentWs && currentWs === this.ws) currentWs.close()
191
+ if (this.isCurrent(generation, ws)) ws.close()
179
192
  }, delay)
180
193
  } else {
181
194
  this.sequence = null
182
195
  this.sessionId = null
183
196
  this.resumeGatewayUrl = null
184
- currentWs?.close()
197
+ ws.close()
185
198
  }
186
199
  break
187
200
  }
@@ -197,11 +210,13 @@ export class DiscordListener {
197
210
  this.sessionId = d.session_id
198
211
  this.resumeGatewayUrl = d.resume_gateway_url
199
212
  this.cachedUser = d.user
213
+ this.reconnectAttempts = 0
200
214
  this.emitter.emit('connected', { user: d.user, sessionId: d.session_id })
201
215
  return
202
216
  }
203
217
 
204
218
  if (t === 'RESUMED') {
219
+ this.reconnectAttempts = 0
205
220
  this.emitter.emit('connected', { user: this.cachedUser!, sessionId: this.sessionId! })
206
221
  return
207
222
  }
@@ -246,18 +261,23 @@ export class DiscordListener {
246
261
  this.ws?.send(JSON.stringify({ op: DiscordGatewayOpcode.Heartbeat, d: this.sequence }))
247
262
  }
248
263
 
249
- private startHeartbeat(interval: number): void {
264
+ private startHeartbeat(interval: number, generation: number, ws: WebSocket): void {
250
265
  this.clearHeartbeatTimers()
251
266
  this.heartbeatAckReceived = true
252
267
 
253
268
  this.heartbeatJitterTimer = setTimeout(() => {
254
269
  this.heartbeatJitterTimer = null
270
+ if (!this.isCurrent(generation, ws)) return
255
271
  this.heartbeatAckReceived = false
256
272
  this.sendHeartbeat()
257
273
 
258
274
  this.heartbeatTimer = setInterval(() => {
275
+ if (!this.isCurrent(generation, ws)) {
276
+ this.clearHeartbeatTimers()
277
+ return
278
+ }
259
279
  if (!this.heartbeatAckReceived) {
260
- this.ws?.close()
280
+ ws.close()
261
281
  return
262
282
  }
263
283
  this.heartbeatAckReceived = false
@@ -269,7 +289,11 @@ export class DiscordListener {
269
289
  private scheduleReconnect(): void {
270
290
  const delay = Math.min(RECONNECT_BASE_DELAY * 2 ** this.reconnectAttempts, RECONNECT_MAX_DELAY)
271
291
  this.reconnectAttempts++
272
- this.reconnectTimer = setTimeout(() => this.connect(), delay)
292
+ const generation = this.generation
293
+ this.reconnectTimer = setTimeout(() => {
294
+ this.reconnectTimer = null
295
+ this.connect(generation)
296
+ }, delay)
273
297
  }
274
298
 
275
299
  private clearHeartbeatTimers(): void {