agent-messenger 2.29.1 → 2.31.0

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 (300) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -0
  3. package/bun.lock +42 -2
  4. package/dist/package.json +2 -2
  5. package/dist/src/cli.d.ts.map +1 -1
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/platforms/channeltalk/cli.d.ts.map +1 -1
  8. package/dist/src/platforms/channeltalk/client.d.ts.map +1 -1
  9. package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
  10. package/dist/src/platforms/channeltalk/commands/bot.js.map +1 -1
  11. package/dist/src/platforms/channeltalk/commands/chat.js.map +1 -1
  12. package/dist/src/platforms/channeltalk/commands/group.js.map +1 -1
  13. package/dist/src/platforms/channeltalk/commands/manager.js.map +1 -1
  14. package/dist/src/platforms/channeltalk/commands/message.js.map +1 -1
  15. package/dist/src/platforms/channeltalk/commands/shared.js.map +1 -1
  16. package/dist/src/platforms/channeltalk/commands/snapshot.js.map +1 -1
  17. package/dist/src/platforms/channeltalk/commands/whoami.js.map +1 -1
  18. package/dist/src/platforms/channeltalk/credential-manager.d.ts.map +1 -1
  19. package/dist/src/platforms/channeltalk/ensure-auth.d.ts.map +1 -1
  20. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  21. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  22. package/dist/src/platforms/channeltalk/types.d.ts.map +1 -1
  23. package/dist/src/platforms/channeltalkbot/cli.d.ts.map +1 -1
  24. package/dist/src/platforms/channeltalkbot/client.d.ts.map +1 -1
  25. package/dist/src/platforms/channeltalkbot/client.js.map +1 -1
  26. package/dist/src/platforms/channeltalkbot/credential-manager.d.ts.map +1 -1
  27. package/dist/src/platforms/channeltalkbot/types.d.ts +1 -1
  28. package/dist/src/platforms/channeltalkbot/types.d.ts.map +1 -1
  29. package/dist/src/platforms/discord/cli.d.ts.map +1 -1
  30. package/dist/src/platforms/discord/client.d.ts +1 -0
  31. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  32. package/dist/src/platforms/discord/client.js +39 -17
  33. package/dist/src/platforms/discord/client.js.map +1 -1
  34. package/dist/src/platforms/discord/commands/message.d.ts +3 -0
  35. package/dist/src/platforms/discord/commands/message.d.ts.map +1 -1
  36. package/dist/src/platforms/discord/commands/message.js +30 -0
  37. package/dist/src/platforms/discord/commands/message.js.map +1 -1
  38. package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
  39. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  40. package/dist/src/platforms/discord/listener.js.map +1 -1
  41. package/dist/src/platforms/discord/remote-auth.js.map +1 -1
  42. package/dist/src/platforms/discord/super-properties.js.map +1 -1
  43. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  44. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  45. package/dist/src/platforms/discord/types.d.ts +13 -0
  46. package/dist/src/platforms/discord/types.d.ts.map +1 -1
  47. package/dist/src/platforms/discord/types.js +6 -0
  48. package/dist/src/platforms/discord/types.js.map +1 -1
  49. package/dist/src/platforms/discordbot/cli.d.ts.map +1 -1
  50. package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
  51. package/dist/src/platforms/discordbot/client.js.map +1 -1
  52. package/dist/src/platforms/discordbot/commands/message.d.ts +1 -1
  53. package/dist/src/platforms/discordbot/commands/message.d.ts.map +1 -1
  54. package/dist/src/platforms/discordbot/commands/message.js +4 -4
  55. package/dist/src/platforms/discordbot/commands/message.js.map +1 -1
  56. package/dist/src/platforms/discordbot/credential-manager.d.ts.map +1 -1
  57. package/dist/src/platforms/discordbot/listener.d.ts.map +1 -1
  58. package/dist/src/platforms/discordbot/listener.js.map +1 -1
  59. package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
  60. package/dist/src/platforms/imessage/cli.d.ts.map +1 -1
  61. package/dist/src/platforms/imessage/client.d.ts.map +1 -1
  62. package/dist/src/platforms/imessage/client.js.map +1 -1
  63. package/dist/src/platforms/imessage/credential-manager.d.ts.map +1 -1
  64. package/dist/src/platforms/imessage/errors.js.map +1 -1
  65. package/dist/src/platforms/imessage/rpc.d.ts.map +1 -1
  66. package/dist/src/platforms/imessage/rpc.js.map +1 -1
  67. package/dist/src/platforms/imessage/types.d.ts.map +1 -1
  68. package/dist/src/platforms/imessage/types.js.map +1 -1
  69. package/dist/src/platforms/instagram/cli.d.ts.map +1 -1
  70. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  71. package/dist/src/platforms/instagram/client.js.map +1 -1
  72. package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
  73. package/dist/src/platforms/instagram/hybrid-listener.d.ts.map +1 -1
  74. package/dist/src/platforms/instagram/hybrid-listener.js.map +1 -1
  75. package/dist/src/platforms/instagram/listener.d.ts.map +1 -1
  76. package/dist/src/platforms/instagram/mqtt/thrift.d.ts.map +1 -1
  77. package/dist/src/platforms/instagram/mqtt/transport.d.ts.map +1 -1
  78. package/dist/src/platforms/instagram/mqtt/transport.js.map +1 -1
  79. package/dist/src/platforms/instagram/realtime-listener.d.ts.map +1 -1
  80. package/dist/src/platforms/instagram/realtime-listener.js.map +1 -1
  81. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -1
  82. package/dist/src/platforms/instagram/types.d.ts.map +1 -1
  83. package/dist/src/platforms/instagram/types.js.map +1 -1
  84. package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
  85. package/dist/src/platforms/kakaotalk/client.d.ts +5 -1
  86. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  87. package/dist/src/platforms/kakaotalk/client.js +55 -0
  88. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  89. package/dist/src/platforms/kakaotalk/commands/chat.d.ts.map +1 -1
  90. package/dist/src/platforms/kakaotalk/commands/chat.js +16 -1
  91. package/dist/src/platforms/kakaotalk/commands/chat.js.map +1 -1
  92. package/dist/src/platforms/kakaotalk/credential-manager.d.ts.map +1 -1
  93. package/dist/src/platforms/kakaotalk/index.d.ts +2 -2
  94. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  95. package/dist/src/platforms/kakaotalk/index.js +1 -1
  96. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  97. package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
  98. package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
  99. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
  100. package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
  101. package/dist/src/platforms/kakaotalk/protocol/crypto.d.ts.map +1 -1
  102. package/dist/src/platforms/kakaotalk/protocol/session.d.ts +23 -0
  103. package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
  104. package/dist/src/platforms/kakaotalk/protocol/session.js +50 -0
  105. package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
  106. package/dist/src/platforms/kakaotalk/token-extractor.d.ts.map +1 -1
  107. package/dist/src/platforms/kakaotalk/types.d.ts +30 -10
  108. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  109. package/dist/src/platforms/kakaotalk/types.js +10 -0
  110. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  111. package/dist/src/platforms/line/cli.d.ts.map +1 -1
  112. package/dist/src/platforms/line/client.d.ts.map +1 -1
  113. package/dist/src/platforms/line/credential-manager.d.ts.map +1 -1
  114. package/dist/src/platforms/line/listener.d.ts.map +1 -1
  115. package/dist/src/platforms/line/types.d.ts +6 -6
  116. package/dist/src/platforms/line/types.d.ts.map +1 -1
  117. package/dist/src/platforms/slack/cli.d.ts.map +1 -1
  118. package/dist/src/platforms/slack/client.d.ts.map +1 -1
  119. package/dist/src/platforms/slack/client.js.map +1 -1
  120. package/dist/src/platforms/slack/commands/message.js +4 -4
  121. package/dist/src/platforms/slack/commands/message.js.map +1 -1
  122. package/dist/src/platforms/slack/credential-manager.d.ts.map +1 -1
  123. package/dist/src/platforms/slack/ensure-auth.js.map +1 -1
  124. package/dist/src/platforms/slack/listener.d.ts.map +1 -1
  125. package/dist/src/platforms/slack/qr-http-login.js.map +1 -1
  126. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  127. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  128. package/dist/src/platforms/slackbot/cli.d.ts.map +1 -1
  129. package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
  130. package/dist/src/platforms/slackbot/commands/message.js +4 -4
  131. package/dist/src/platforms/slackbot/commands/message.js.map +1 -1
  132. package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -1
  133. package/dist/src/platforms/slackbot/listener.d.ts.map +1 -1
  134. package/dist/src/platforms/slackbot/listener.js.map +1 -1
  135. package/dist/src/platforms/slackbot/types.d.ts.map +1 -1
  136. package/dist/src/platforms/teams/app-config.js.map +1 -1
  137. package/dist/src/platforms/teams/cli.d.ts.map +1 -1
  138. package/dist/src/platforms/teams/client.d.ts +1 -0
  139. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  140. package/dist/src/platforms/teams/client.js +19 -0
  141. package/dist/src/platforms/teams/client.js.map +1 -1
  142. package/dist/src/platforms/teams/commands/chat.d.ts +3 -0
  143. package/dist/src/platforms/teams/commands/chat.d.ts.map +1 -1
  144. package/dist/src/platforms/teams/commands/chat.js +34 -1
  145. package/dist/src/platforms/teams/commands/chat.js.map +1 -1
  146. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  147. package/dist/src/platforms/teams/device-code.js.map +1 -1
  148. package/dist/src/platforms/teams/device-login.d.ts.map +1 -1
  149. package/dist/src/platforms/teams/device-login.js.map +1 -1
  150. package/dist/src/platforms/teams/listener.d.ts.map +1 -1
  151. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  152. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  153. package/dist/src/platforms/teams/token-provider.d.ts.map +1 -1
  154. package/dist/src/platforms/teams/token-provider.js.map +1 -1
  155. package/dist/src/platforms/teams/types.d.ts +14 -14
  156. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  157. package/dist/src/platforms/telegram/cli.d.ts.map +1 -1
  158. package/dist/src/platforms/telegram/client.d.ts +1 -0
  159. package/dist/src/platforms/telegram/client.d.ts.map +1 -1
  160. package/dist/src/platforms/telegram/client.js +21 -0
  161. package/dist/src/platforms/telegram/client.js.map +1 -1
  162. package/dist/src/platforms/telegram/commands/message.d.ts.map +1 -1
  163. package/dist/src/platforms/telegram/commands/message.js +25 -1
  164. package/dist/src/platforms/telegram/commands/message.js.map +1 -1
  165. package/dist/src/platforms/telegram/commands/shared.js.map +1 -1
  166. package/dist/src/platforms/telegram/credential-manager.d.ts.map +1 -1
  167. package/dist/src/platforms/telegram/tdlib.d.ts.map +1 -1
  168. package/dist/src/platforms/telegram/types.d.ts.map +1 -1
  169. package/dist/src/platforms/telegram/types.js.map +1 -1
  170. package/dist/src/platforms/telegrambot/cli.d.ts.map +1 -1
  171. package/dist/src/platforms/telegrambot/client.d.ts.map +1 -1
  172. package/dist/src/platforms/telegrambot/commands/message.d.ts +1 -1
  173. package/dist/src/platforms/telegrambot/commands/message.d.ts.map +1 -1
  174. package/dist/src/platforms/telegrambot/commands/message.js +3 -3
  175. package/dist/src/platforms/telegrambot/commands/message.js.map +1 -1
  176. package/dist/src/platforms/telegrambot/credential-manager.d.ts.map +1 -1
  177. package/dist/src/platforms/telegrambot/listener.d.ts.map +1 -1
  178. package/dist/src/platforms/telegrambot/types.d.ts.map +1 -1
  179. package/dist/src/platforms/webex/cli.d.ts.map +1 -1
  180. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  181. package/dist/src/platforms/webex/client.js.map +1 -1
  182. package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
  183. package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
  184. package/dist/src/platforms/webex/kms-key-provider.d.ts.map +1 -1
  185. package/dist/src/platforms/webex/listener.d.ts.map +1 -1
  186. package/dist/src/platforms/webex/listener.js.map +1 -1
  187. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  188. package/dist/src/platforms/webex/types.d.ts +4 -4
  189. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  190. package/dist/src/platforms/webexbot/cli.d.ts.map +1 -1
  191. package/dist/src/platforms/webexbot/client.d.ts.map +1 -1
  192. package/dist/src/platforms/webexbot/credential-manager.d.ts.map +1 -1
  193. package/dist/src/platforms/webexbot/listener.d.ts.map +1 -1
  194. package/dist/src/platforms/webexbot/types.d.ts.map +1 -1
  195. package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -1
  196. package/dist/src/platforms/wechatbot/client.d.ts.map +1 -1
  197. package/dist/src/platforms/wechatbot/credential-manager.d.ts.map +1 -1
  198. package/dist/src/platforms/wechatbot/types.d.ts.map +1 -1
  199. package/dist/src/platforms/whatsapp/cli.d.ts.map +1 -1
  200. package/dist/src/platforms/whatsapp/client.d.ts +3 -1
  201. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  202. package/dist/src/platforms/whatsapp/client.js +19 -3
  203. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  204. package/dist/src/platforms/whatsapp/commands/message.d.ts.map +1 -1
  205. package/dist/src/platforms/whatsapp/commands/message.js +18 -0
  206. package/dist/src/platforms/whatsapp/commands/message.js.map +1 -1
  207. package/dist/src/platforms/whatsapp/credential-manager.d.ts.map +1 -1
  208. package/dist/src/platforms/whatsapp/types.d.ts.map +1 -1
  209. package/dist/src/platforms/whatsapp/types.js.map +1 -1
  210. package/dist/src/platforms/whatsappbot/cli.d.ts.map +1 -1
  211. package/dist/src/platforms/whatsappbot/client.d.ts.map +1 -1
  212. package/dist/src/platforms/whatsappbot/credential-manager.d.ts.map +1 -1
  213. package/dist/src/platforms/whatsappbot/types.d.ts.map +1 -1
  214. package/dist/src/shared/chromium/browsers.js.map +1 -1
  215. package/dist/src/shared/chromium/cli-options.js.map +1 -1
  216. package/dist/src/shared/chromium/cookie-reader.d.ts.map +1 -1
  217. package/dist/src/shared/chromium/decryptor.d.ts.map +1 -1
  218. package/dist/src/shared/sqlite.js.map +1 -1
  219. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  220. package/dist/src/tui/adapters/channeltalk-adapter.d.ts.map +1 -1
  221. package/dist/src/tui/adapters/discord-adapter.d.ts.map +1 -1
  222. package/dist/src/tui/adapters/imessage-adapter.d.ts.map +1 -1
  223. package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
  224. package/dist/src/tui/adapters/kakaotalk-adapter.d.ts.map +1 -1
  225. package/dist/src/tui/adapters/line-adapter.d.ts.map +1 -1
  226. package/dist/src/tui/adapters/slack-adapter.d.ts.map +1 -1
  227. package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
  228. package/dist/src/tui/adapters/telegram-adapter.d.ts.map +1 -1
  229. package/dist/src/tui/adapters/telegram-adapter.js.map +1 -1
  230. package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -1
  231. package/dist/src/tui/adapters/whatsapp-adapter.d.ts.map +1 -1
  232. package/dist/src/tui/app.js.map +1 -1
  233. package/dist/src/tui/views/channel-picker.d.ts.map +1 -1
  234. package/dist/src/tui/views/workspace-picker.d.ts.map +1 -1
  235. package/docs/content/docs/cli/discordbot.mdx +2 -2
  236. package/docs/content/docs/cli/kakaotalk.mdx +16 -1
  237. package/docs/content/docs/cli/slack.mdx +2 -2
  238. package/docs/content/docs/cli/slackbot.mdx +2 -2
  239. package/docs/content/docs/cli/telegrambot.mdx +1 -1
  240. package/docs/content/docs/sdk/kakaotalk.mdx +39 -0
  241. package/e2e/README.md +3 -3
  242. package/e2e/discordbot.e2e.test.ts +2 -2
  243. package/e2e/slack.e2e.test.ts +2 -2
  244. package/e2e/slackbot.e2e.test.ts +2 -2
  245. package/e2e/telegrambot.e2e.test.ts +2 -2
  246. package/package.json +2 -2
  247. package/skills/agent-channeltalk/SKILL.md +1 -1
  248. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  249. package/skills/agent-discord/SKILL.md +5 -1
  250. package/skills/agent-discordbot/SKILL.md +3 -3
  251. package/skills/agent-discordbot/references/common-patterns.md +2 -2
  252. package/skills/agent-imessage/SKILL.md +1 -1
  253. package/skills/agent-instagram/SKILL.md +1 -1
  254. package/skills/agent-kakaotalk/SKILL.md +19 -2
  255. package/skills/agent-line/SKILL.md +1 -1
  256. package/skills/agent-slack/SKILL.md +3 -3
  257. package/skills/agent-slack/references/common-patterns.md +2 -2
  258. package/skills/agent-slackbot/SKILL.md +3 -3
  259. package/skills/agent-slackbot/references/common-patterns.md +4 -4
  260. package/skills/agent-teams/SKILL.md +6 -1
  261. package/skills/agent-telegram/SKILL.md +4 -1
  262. package/skills/agent-telegrambot/SKILL.md +2 -2
  263. package/skills/agent-webex/SKILL.md +1 -1
  264. package/skills/agent-webexbot/SKILL.md +1 -1
  265. package/skills/agent-wechatbot/SKILL.md +1 -1
  266. package/skills/agent-whatsapp/SKILL.md +25 -1
  267. package/skills/agent-whatsappbot/SKILL.md +1 -1
  268. package/src/platforms/discord/client.test.ts +143 -0
  269. package/src/platforms/discord/client.ts +49 -22
  270. package/src/platforms/discord/commands/message.test.ts +26 -1
  271. package/src/platforms/discord/commands/message.ts +41 -0
  272. package/src/platforms/discord/types.ts +16 -0
  273. package/src/platforms/discordbot/commands/message.test.ts +6 -6
  274. package/src/platforms/discordbot/commands/message.ts +4 -4
  275. package/src/platforms/kakaotalk/client.test.ts +194 -1
  276. package/src/platforms/kakaotalk/client.ts +58 -0
  277. package/src/platforms/kakaotalk/commands/chat.test.ts +24 -0
  278. package/src/platforms/kakaotalk/commands/chat.ts +23 -0
  279. package/src/platforms/kakaotalk/index.test.ts +10 -0
  280. package/src/platforms/kakaotalk/index.ts +4 -0
  281. package/src/platforms/kakaotalk/protocol/session.test.ts +113 -0
  282. package/src/platforms/kakaotalk/protocol/session.ts +56 -0
  283. package/src/platforms/kakaotalk/types.ts +24 -0
  284. package/src/platforms/slack/commands/message.test.ts +4 -4
  285. package/src/platforms/slack/commands/message.ts +4 -4
  286. package/src/platforms/slackbot/commands/message.ts +4 -4
  287. package/src/platforms/teams/client.test.ts +24 -0
  288. package/src/platforms/teams/client.ts +28 -0
  289. package/src/platforms/teams/commands/chat.test.ts +23 -1
  290. package/src/platforms/teams/commands/chat.ts +44 -0
  291. package/src/platforms/telegram/client.test.ts +60 -0
  292. package/src/platforms/telegram/client.ts +24 -0
  293. package/src/platforms/telegram/commands/message.test.ts +38 -0
  294. package/src/platforms/telegram/commands/message.ts +39 -0
  295. package/src/platforms/telegrambot/commands/message.ts +3 -3
  296. package/src/platforms/whatsapp/client.test.ts +58 -0
  297. package/src/platforms/whatsapp/client.ts +23 -3
  298. package/src/platforms/whatsapp/commands/message.test.ts +43 -0
  299. package/src/platforms/whatsapp/commands/message.ts +25 -0
  300. package/tsconfig.json +8 -8
@@ -246,6 +246,28 @@ describe('DiscordClient', () => {
246
246
  })
247
247
  })
248
248
 
249
+ describe('editMessage', () => {
250
+ it('edits message with PATCH', async () => {
251
+ mockResponse({
252
+ id: 'msg1',
253
+ channel_id: 'ch1',
254
+ author: { id: '123', username: 'bot' },
255
+ content: 'Updated content',
256
+ timestamp: '2024-01-01T00:00:00.000Z',
257
+ edited_timestamp: '2024-01-01T00:05:00.000Z',
258
+ })
259
+
260
+ const client = await new DiscordClient().login({ token: 'test-token' })
261
+ const message = await client.editMessage('ch1', 'msg1', 'Updated content')
262
+
263
+ expect(message.content).toBe('Updated content')
264
+ expect(message.edited_timestamp).toBe('2024-01-01T00:05:00.000Z')
265
+ expect(fetchCalls[0].url).toBe('https://discord.com/api/v10/channels/ch1/messages/msg1')
266
+ expect(fetchCalls[0].options?.method).toBe('PATCH')
267
+ expect(fetchCalls[0].options?.body).toBe(JSON.stringify({ content: 'Updated content' }))
268
+ })
269
+ })
270
+
249
271
  describe('deleteMessage', () => {
250
272
  it('deletes message', async () => {
251
273
  mockResponse(null, 204)
@@ -425,6 +447,127 @@ describe('DiscordClient', () => {
425
447
  })
426
448
  })
427
449
 
450
+ describe('searchMessages', () => {
451
+ const searchResult = {
452
+ id: 'msg1',
453
+ channel_id: 'ch1',
454
+ guild_id: 'guild1',
455
+ author: { id: 'user1', username: 'user1' },
456
+ content: 'matching message',
457
+ timestamp: '2026-01-01T00:00:00.000Z',
458
+ hit: true,
459
+ }
460
+
461
+ it('flattens grouped search results and keeps hits', async () => {
462
+ mockResponse({
463
+ doing_deep_historical_index: false,
464
+ total_results: 1,
465
+ messages: [[searchResult, { ...searchResult, id: 'context', hit: false }]],
466
+ })
467
+
468
+ const client = await new DiscordClient().login({ token: 'test-token' })
469
+ const result = await client.searchMessages('guild1', 'matching', { limit: 25 })
470
+
471
+ expect(result).toEqual({ results: [searchResult], total: 1 })
472
+ expect(fetchCalls[0].url).toBe(
473
+ 'https://discord.com/api/v10/guilds/guild1/messages/search?content=matching&limit=25',
474
+ )
475
+ })
476
+
477
+ it('retries when the search index is not ready', async () => {
478
+ mockResponse(
479
+ {
480
+ message: 'Index not yet available. Try again later',
481
+ code: 110000,
482
+ documents_indexed: 0,
483
+ retry_after: 0,
484
+ },
485
+ 202,
486
+ )
487
+ mockResponse({
488
+ doing_deep_historical_index: false,
489
+ total_results: 1,
490
+ messages: [[searchResult]],
491
+ })
492
+
493
+ const client = await new DiscordClient().login({ token: 'test-token' })
494
+ const startTime = Date.now()
495
+ const result = await client.searchMessages('guild1', 'matching')
496
+ const elapsed = Date.now() - startTime
497
+
498
+ expect(result).toEqual({ results: [searchResult], total: 1 })
499
+ expect(elapsed).toBeGreaterThanOrEqual(50)
500
+ expect(fetchCalls).toHaveLength(2)
501
+ })
502
+
503
+ it('throws a clear error when the search index remains unavailable', async () => {
504
+ for (let attempt = 0; attempt < 4; attempt++) {
505
+ mockResponse(
506
+ {
507
+ message: 'Index not yet available. Try again later',
508
+ code: 110000,
509
+ documents_indexed: 0,
510
+ retry_after: 0.001,
511
+ },
512
+ 202,
513
+ )
514
+ }
515
+
516
+ const client = await new DiscordClient().login({ token: 'test-token' })
517
+
518
+ await expect(client.searchMessages('guild1', 'matching')).rejects.toThrow(
519
+ 'Search index is not ready after retries',
520
+ )
521
+ expect(fetchCalls).toHaveLength(4)
522
+ })
523
+
524
+ it('rejects unexpected successful response payloads', async () => {
525
+ mockResponse(
526
+ {
527
+ message: 'Unexpected response',
528
+ code: 999999,
529
+ retry_after: 0,
530
+ },
531
+ 202,
532
+ )
533
+
534
+ const client = await new DiscordClient().login({ token: 'test-token' })
535
+
536
+ try {
537
+ await client.searchMessages('guild1', 'matching')
538
+ throw new Error('Expected searchMessages to throw')
539
+ } catch (error) {
540
+ expect(error).toBeInstanceOf(DiscordError)
541
+ if (!(error instanceof DiscordError)) throw error
542
+ expect(error.code).toBe('invalid_search_response')
543
+ }
544
+ expect(fetchCalls).toHaveLength(1)
545
+ })
546
+
547
+ it('rejects malformed message and retry payloads', async () => {
548
+ mockResponse({ total_results: 1, messages: null })
549
+ mockResponse(
550
+ {
551
+ message: 'Index not yet available. Try again later',
552
+ code: 110000,
553
+ documents_indexed: 0,
554
+ retry_after: -1,
555
+ },
556
+ 202,
557
+ )
558
+
559
+ const client = await new DiscordClient().login({ token: 'test-token' })
560
+
561
+ await expect(client.searchMessages('guild1', 'matching')).rejects.toThrow(
562
+ 'Discord returned an invalid search response',
563
+ )
564
+ await expect(client.searchMessages('guild1', 'matching')).rejects.toThrow(
565
+ 'Discord returned an invalid search response',
566
+ )
567
+ expect(fetchCalls).toHaveLength(2)
568
+ })
569
+ })
570
+
428
571
  describe('rate limiting', () => {
429
572
  it('waits when bucket is exhausted before making request', async () => {
430
573
  mockResponse({ id: '1', username: 'user1' }, 200, {
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
 
3
3
  import { getDiscordHeaders } from './super-properties'
4
+ import { DiscordSearchIndexNotReadyResponseSchema, DiscordSearchResponseSchema } from './types'
4
5
  import type {
5
6
  DiscordChannel,
6
7
  DiscordDMChannel,
@@ -11,7 +12,6 @@ import type {
11
12
  DiscordMessage,
12
13
  DiscordRelationship,
13
14
  DiscordSearchOptions,
14
- DiscordSearchResponse,
15
15
  DiscordSearchResult,
16
16
  DiscordUser,
17
17
  DiscordUserNote,
@@ -37,6 +37,7 @@ interface RateLimitBucket {
37
37
  const BASE_URL = 'https://discord.com/api/v10'
38
38
  const MAX_RETRIES = 3
39
39
  const BASE_BACKOFF_MS = 100
40
+ const MAX_SEARCH_INDEX_RETRY_MS = 30_000
40
41
 
41
42
  export class DiscordClient {
42
43
  private token: string | null = null
@@ -242,6 +243,10 @@ export class DiscordClient {
242
243
  return this.request<DiscordMessage>('GET', `/channels/${channelId}/messages/${messageId}`)
243
244
  }
244
245
 
246
+ async editMessage(channelId: string, messageId: string, content: string): Promise<DiscordMessage> {
247
+ return this.request<DiscordMessage>('PATCH', `/channels/${channelId}/messages/${messageId}`, { content })
248
+ }
249
+
245
250
  async deleteMessage(channelId: string, messageId: string): Promise<void> {
246
251
  return this.request<void>('DELETE', `/channels/${channelId}/messages/${messageId}`)
247
252
  }
@@ -389,28 +394,50 @@ export class DiscordClient {
389
394
  params.set('offset', options.offset.toString())
390
395
  }
391
396
 
392
- const response = await this.request<DiscordSearchResponse>(
393
- 'GET',
394
- `/guilds/${guildId}/messages/search?${params.toString()}`,
395
- )
396
-
397
- const results = response.messages
398
- .flat()
399
- .filter((msg) => msg.hit)
400
- .map((msg) => ({
401
- id: msg.id,
402
- channel_id: msg.channel_id,
403
- guild_id: msg.guild_id,
404
- content: msg.content,
405
- author: msg.author,
406
- timestamp: msg.timestamp,
407
- hit: msg.hit,
408
- }))
409
-
410
- return {
411
- results,
412
- total: response.total_results,
397
+ const path = `/guilds/${guildId}/messages/search?${params.toString()}`
398
+
399
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
400
+ const response: unknown = await this.request('GET', path)
401
+ const searchResponse = DiscordSearchResponseSchema.safeParse(response)
402
+
403
+ if (searchResponse.success) {
404
+ const results = searchResponse.data.messages
405
+ .flat()
406
+ .filter((msg) => msg.hit)
407
+ .map((msg) => ({
408
+ id: msg.id,
409
+ channel_id: msg.channel_id,
410
+ guild_id: msg.guild_id,
411
+ content: msg.content,
412
+ author: msg.author,
413
+ timestamp: msg.timestamp,
414
+ hit: msg.hit,
415
+ }))
416
+
417
+ return {
418
+ results,
419
+ total: searchResponse.data.total_results,
420
+ }
421
+ }
422
+
423
+ const indexNotReadyResponse = DiscordSearchIndexNotReadyResponseSchema.safeParse(response)
424
+ if (!indexNotReadyResponse.success) {
425
+ throw new DiscordError('Discord returned an invalid search response', 'invalid_search_response')
426
+ }
427
+
428
+ if (attempt === MAX_RETRIES) {
429
+ throw new DiscordError('Search index is not ready after retries', 'search_index_not_ready')
430
+ }
431
+
432
+ const requestedDelayMs = indexNotReadyResponse.data.retry_after * 1000
433
+ const retryDelayMs = Math.min(
434
+ requestedDelayMs === 0 ? BASE_BACKOFF_MS : requestedDelayMs,
435
+ MAX_SEARCH_INDEX_RETRY_MS,
436
+ )
437
+ await this.sleep(retryDelayMs)
413
438
  }
439
+
440
+ throw new DiscordError('Search failed after retries', 'max_retries')
414
441
  }
415
442
 
416
443
  async getUserProfile(userId: string): Promise<DiscordUserProfile> {
@@ -2,11 +2,12 @@ import { afterEach, beforeEach, expect, mock, spyOn, it } from 'bun:test'
2
2
 
3
3
  import { DiscordClient } from '../client'
4
4
  import { DiscordCredentialManager } from '../credential-manager'
5
- import { ackAction, deleteAction, getAction, listAction, searchAction, sendAction } from './message'
5
+ import { ackAction, deleteAction, editAction, getAction, listAction, searchAction, sendAction } from './message'
6
6
 
7
7
  let clientSendMessageSpy: ReturnType<typeof spyOn>
8
8
  let clientGetMessagesSpy: ReturnType<typeof spyOn>
9
9
  let clientGetMessageSpy: ReturnType<typeof spyOn>
10
+ let clientEditMessageSpy: ReturnType<typeof spyOn>
10
11
  let clientDeleteMessageSpy: ReturnType<typeof spyOn>
11
12
  let clientAckMessageSpy: ReturnType<typeof spyOn>
12
13
  let clientSearchMessagesSpy: ReturnType<typeof spyOn>
@@ -47,6 +48,15 @@ beforeEach(() => {
47
48
  timestamp: '2025-01-29T10:00:00Z',
48
49
  })
49
50
 
51
+ clientEditMessageSpy = spyOn(DiscordClient.prototype, 'editMessage').mockResolvedValue({
52
+ id: 'msg_123',
53
+ channel_id: 'ch_456',
54
+ author: { id: 'user_789', username: 'testuser' },
55
+ content: 'Updated content',
56
+ timestamp: '2025-01-29T10:00:00Z',
57
+ edited_timestamp: '2025-01-29T10:05:00Z',
58
+ })
59
+
50
60
  clientDeleteMessageSpy = spyOn(DiscordClient.prototype, 'deleteMessage').mockResolvedValue(undefined)
51
61
 
52
62
  clientAckMessageSpy = spyOn(DiscordClient.prototype, 'ackMessage').mockResolvedValue(undefined)
@@ -87,6 +97,7 @@ afterEach(() => {
87
97
  clientSendMessageSpy?.mockRestore()
88
98
  clientGetMessagesSpy?.mockRestore()
89
99
  clientGetMessageSpy?.mockRestore()
100
+ clientEditMessageSpy?.mockRestore()
90
101
  clientDeleteMessageSpy?.mockRestore()
91
102
  clientAckMessageSpy?.mockRestore()
92
103
  clientSearchMessagesSpy?.mockRestore()
@@ -127,6 +138,20 @@ it('get: returns single message', async () => {
127
138
  expect(output).toContain('msg_123')
128
139
  })
129
140
 
141
+ it('edit: returns updated message', async () => {
142
+ const consoleSpy = mock((_msg: string) => {})
143
+ console.log = consoleSpy
144
+
145
+ await editAction('ch_456', 'msg_123', 'Updated content', { pretty: false })
146
+
147
+ expect(clientEditMessageSpy).toHaveBeenCalledWith('ch_456', 'msg_123', 'Updated content')
148
+ expect(consoleSpy).toHaveBeenCalled()
149
+ const output = consoleSpy.mock.calls[0][0]
150
+ expect(output).toContain('msg_123')
151
+ expect(output).toContain('Updated content')
152
+ expect(output).toContain('edited_timestamp')
153
+ })
154
+
130
155
  it('delete: returns success', async () => {
131
156
  const consoleSpy = mock((_msg: string) => {})
132
157
  console.log = consoleSpy
@@ -121,6 +121,38 @@ export async function deleteAction(
121
121
  }
122
122
  }
123
123
 
124
+ export async function editAction(
125
+ channelId: string,
126
+ messageId: string,
127
+ content: string,
128
+ options: { pretty?: boolean },
129
+ ): Promise<void> {
130
+ try {
131
+ const credManager = new DiscordCredentialManager()
132
+ const config = await credManager.load()
133
+
134
+ if (!config.token) {
135
+ console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
136
+ process.exit(1)
137
+ }
138
+
139
+ const client = await new DiscordClient().login({ token: config.token })
140
+ const message = await client.editMessage(channelId, messageId, content)
141
+
142
+ const output = {
143
+ id: message.id,
144
+ content: message.content,
145
+ author: message.author.username,
146
+ timestamp: message.timestamp,
147
+ edited_timestamp: message.edited_timestamp,
148
+ }
149
+
150
+ console.log(formatOutput(output, options.pretty))
151
+ } catch (error) {
152
+ handleError(error as Error)
153
+ }
154
+ }
155
+
124
156
  export async function ackAction(channelId: string, messageId: string, options: { pretty?: boolean }): Promise<void> {
125
157
  try {
126
158
  const credManager = new DiscordCredentialManager()
@@ -244,6 +276,15 @@ export const messageCommand = new Command('message')
244
276
  .option('--pretty', 'Pretty print JSON output')
245
277
  .action(deleteAction),
246
278
  )
279
+ .addCommand(
280
+ new Command('edit')
281
+ .description('Edit a message')
282
+ .argument('<channel-id>', 'Channel ID')
283
+ .argument('<message-id>', 'Message ID')
284
+ .argument('<content>', 'New message content')
285
+ .option('--pretty', 'Pretty print JSON output')
286
+ .action(editAction),
287
+ )
247
288
  .addCommand(
248
289
  new Command('ack')
249
290
  .description('Mark message as read (acknowledge)')
@@ -109,6 +109,15 @@ export interface DiscordSearchResponse {
109
109
  messages: DiscordSearchResult[][]
110
110
  }
111
111
 
112
+ export interface DiscordSearchIndexNotReadyResponse {
113
+ message: string
114
+ code: 110000
115
+ documents_indexed: number
116
+ retry_after: number
117
+ }
118
+
119
+ export type DiscordSearchApiResponse = DiscordSearchResponse | DiscordSearchIndexNotReadyResponse
120
+
112
121
  export interface DiscordSearchOptions {
113
122
  channelId?: string
114
123
  authorId?: string
@@ -267,6 +276,13 @@ export const DiscordSearchResponseSchema = z.object({
267
276
  messages: z.array(z.array(DiscordSearchResultSchema)),
268
277
  })
269
278
 
279
+ export const DiscordSearchIndexNotReadyResponseSchema = z.object({
280
+ message: z.string(),
281
+ code: z.literal(110000),
282
+ documents_indexed: z.number(),
283
+ retry_after: z.number().nonnegative(),
284
+ })
285
+
270
286
  export const DiscordCredentialsSchema = z.object({
271
287
  token: z.string(),
272
288
  })
@@ -82,7 +82,7 @@ mock.module('../client', () => ({
82
82
  }))
83
83
 
84
84
  import { DiscordBotCredentialManager } from '../credential-manager'
85
- import { deleteAction, getAction, listAction, repliesAction, sendAction, updateAction } from './message'
85
+ import { deleteAction, editAction, getAction, listAction, repliesAction, sendAction } from './message'
86
86
 
87
87
  describe('message commands', () => {
88
88
  let tempDir: string
@@ -243,9 +243,9 @@ describe('message commands', () => {
243
243
  })
244
244
  })
245
245
 
246
- describe('updateAction', () => {
247
- it('updates a message', async () => {
248
- const result = await updateAction('general', 'msg1', 'updated text', { _credManager: manager })
246
+ describe('editAction', () => {
247
+ it('edits a message', async () => {
248
+ const result = await editAction('general', 'msg1', 'updated text', { _credManager: manager })
249
249
 
250
250
  expect(result.id).toBe('msg1')
251
251
  expect(result.content).toBe('updated text')
@@ -254,7 +254,7 @@ describe('message commands', () => {
254
254
  })
255
255
 
256
256
  it('resolves channel name', async () => {
257
- await updateAction('general', 'msg1', 'new', { _credManager: manager })
257
+ await editAction('general', 'msg1', 'new', { _credManager: manager })
258
258
 
259
259
  expect(mockResolveChannel).toHaveBeenCalledWith('guild1', 'general')
260
260
  })
@@ -262,7 +262,7 @@ describe('message commands', () => {
262
262
  it('returns error on failure', async () => {
263
263
  mockEditMessage.mockImplementationOnce(() => Promise.reject(new Error('Cannot edit')))
264
264
 
265
- const result = await updateAction('general', 'msg1', 'new', { _credManager: manager })
265
+ const result = await editAction('general', 'msg1', 'new', { _credManager: manager })
266
266
 
267
267
  expect(result.error).toContain('Cannot edit')
268
268
  })
@@ -95,7 +95,7 @@ export async function getAction(channel: string, messageId: string, options: Bot
95
95
  }
96
96
  }
97
97
 
98
- export async function updateAction(
98
+ export async function editAction(
99
99
  channel: string,
100
100
  messageId: string,
101
101
  text: string,
@@ -207,8 +207,8 @@ export const messageCommand = new Command('message')
207
207
  }),
208
208
  )
209
209
  .addCommand(
210
- new Command('update')
211
- .description('Update a message')
210
+ new Command('edit')
211
+ .description('Edit a message')
212
212
  .argument('<channel>', 'Channel ID or name')
213
213
  .argument('<message-id>', 'Message ID')
214
214
  .argument('<text>', 'New message text')
@@ -216,7 +216,7 @@ export const messageCommand = new Command('message')
216
216
  .option('--server <id>', 'Server ID')
217
217
  .option('--pretty', 'Pretty print JSON output')
218
218
  .action(async (channel: string, messageId: string, text: string, opts: BotOption) => {
219
- cliOutput(await updateAction(channel, messageId, text, opts), opts.pretty)
219
+ cliOutput(await editAction(channel, messageId, text, opts), opts.pretty)
220
220
  }),
221
221
  )
222
222
  .addCommand(