agent-messenger 2.30.0 → 2.32.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 (258) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +14 -1
  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 +11 -2
  31. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  32. package/dist/src/platforms/discord/client.js +145 -21
  33. package/dist/src/platforms/discord/client.js.map +1 -1
  34. package/dist/src/platforms/discord/commands/mention.d.ts +5 -0
  35. package/dist/src/platforms/discord/commands/mention.d.ts.map +1 -1
  36. package/dist/src/platforms/discord/commands/mention.js +60 -4
  37. package/dist/src/platforms/discord/commands/mention.js.map +1 -1
  38. package/dist/src/platforms/discord/commands/reaction.js +1 -1
  39. package/dist/src/platforms/discord/commands/reaction.js.map +1 -1
  40. package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
  41. package/dist/src/platforms/discord/index.d.ts +1 -1
  42. package/dist/src/platforms/discord/index.d.ts.map +1 -1
  43. package/dist/src/platforms/discord/index.js.map +1 -1
  44. package/dist/src/platforms/discord/listener.d.ts +4 -1
  45. package/dist/src/platforms/discord/listener.d.ts.map +1 -1
  46. package/dist/src/platforms/discord/listener.js +28 -1
  47. package/dist/src/platforms/discord/listener.js.map +1 -1
  48. package/dist/src/platforms/discord/remote-auth.js.map +1 -1
  49. package/dist/src/platforms/discord/super-properties.js.map +1 -1
  50. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  51. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  52. package/dist/src/platforms/discord/types.d.ts +56 -9
  53. package/dist/src/platforms/discord/types.d.ts.map +1 -1
  54. package/dist/src/platforms/discord/types.js +28 -7
  55. package/dist/src/platforms/discord/types.js.map +1 -1
  56. package/dist/src/platforms/discordbot/cli.d.ts.map +1 -1
  57. package/dist/src/platforms/discordbot/client.d.ts.map +1 -1
  58. package/dist/src/platforms/discordbot/client.js.map +1 -1
  59. package/dist/src/platforms/discordbot/credential-manager.d.ts.map +1 -1
  60. package/dist/src/platforms/discordbot/listener.d.ts.map +1 -1
  61. package/dist/src/platforms/discordbot/listener.js.map +1 -1
  62. package/dist/src/platforms/discordbot/types.d.ts.map +1 -1
  63. package/dist/src/platforms/imessage/cli.d.ts.map +1 -1
  64. package/dist/src/platforms/imessage/client.d.ts.map +1 -1
  65. package/dist/src/platforms/imessage/client.js.map +1 -1
  66. package/dist/src/platforms/imessage/credential-manager.d.ts.map +1 -1
  67. package/dist/src/platforms/imessage/errors.js.map +1 -1
  68. package/dist/src/platforms/imessage/rpc.d.ts.map +1 -1
  69. package/dist/src/platforms/imessage/rpc.js.map +1 -1
  70. package/dist/src/platforms/imessage/types.d.ts.map +1 -1
  71. package/dist/src/platforms/imessage/types.js.map +1 -1
  72. package/dist/src/platforms/instagram/cli.d.ts.map +1 -1
  73. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  74. package/dist/src/platforms/instagram/client.js.map +1 -1
  75. package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
  76. package/dist/src/platforms/instagram/hybrid-listener.d.ts.map +1 -1
  77. package/dist/src/platforms/instagram/hybrid-listener.js.map +1 -1
  78. package/dist/src/platforms/instagram/listener.d.ts.map +1 -1
  79. package/dist/src/platforms/instagram/mqtt/thrift.d.ts.map +1 -1
  80. package/dist/src/platforms/instagram/mqtt/transport.d.ts.map +1 -1
  81. package/dist/src/platforms/instagram/mqtt/transport.js.map +1 -1
  82. package/dist/src/platforms/instagram/realtime-listener.d.ts.map +1 -1
  83. package/dist/src/platforms/instagram/realtime-listener.js.map +1 -1
  84. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -1
  85. package/dist/src/platforms/instagram/types.d.ts.map +1 -1
  86. package/dist/src/platforms/instagram/types.js.map +1 -1
  87. package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
  88. package/dist/src/platforms/kakaotalk/client.d.ts +5 -1
  89. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  90. package/dist/src/platforms/kakaotalk/client.js +55 -0
  91. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  92. package/dist/src/platforms/kakaotalk/commands/chat.d.ts.map +1 -1
  93. package/dist/src/platforms/kakaotalk/commands/chat.js +16 -1
  94. package/dist/src/platforms/kakaotalk/commands/chat.js.map +1 -1
  95. package/dist/src/platforms/kakaotalk/credential-manager.d.ts.map +1 -1
  96. package/dist/src/platforms/kakaotalk/index.d.ts +2 -2
  97. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  98. package/dist/src/platforms/kakaotalk/index.js +1 -1
  99. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  100. package/dist/src/platforms/kakaotalk/listener.d.ts.map +1 -1
  101. package/dist/src/platforms/kakaotalk/listener.js.map +1 -1
  102. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
  103. package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
  104. package/dist/src/platforms/kakaotalk/protocol/crypto.d.ts.map +1 -1
  105. package/dist/src/platforms/kakaotalk/protocol/session.d.ts +23 -0
  106. package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
  107. package/dist/src/platforms/kakaotalk/protocol/session.js +50 -0
  108. package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
  109. package/dist/src/platforms/kakaotalk/token-extractor.d.ts.map +1 -1
  110. package/dist/src/platforms/kakaotalk/types.d.ts +30 -10
  111. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  112. package/dist/src/platforms/kakaotalk/types.js +10 -0
  113. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  114. package/dist/src/platforms/line/cli.d.ts.map +1 -1
  115. package/dist/src/platforms/line/client.d.ts.map +1 -1
  116. package/dist/src/platforms/line/credential-manager.d.ts.map +1 -1
  117. package/dist/src/platforms/line/listener.d.ts.map +1 -1
  118. package/dist/src/platforms/line/types.d.ts +6 -6
  119. package/dist/src/platforms/line/types.d.ts.map +1 -1
  120. package/dist/src/platforms/slack/cli.d.ts.map +1 -1
  121. package/dist/src/platforms/slack/client.d.ts.map +1 -1
  122. package/dist/src/platforms/slack/client.js.map +1 -1
  123. package/dist/src/platforms/slack/credential-manager.d.ts.map +1 -1
  124. package/dist/src/platforms/slack/ensure-auth.js.map +1 -1
  125. package/dist/src/platforms/slack/listener.d.ts.map +1 -1
  126. package/dist/src/platforms/slack/qr-http-login.js.map +1 -1
  127. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  128. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  129. package/dist/src/platforms/slackbot/cli.d.ts.map +1 -1
  130. package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
  131. package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -1
  132. package/dist/src/platforms/slackbot/listener.d.ts.map +1 -1
  133. package/dist/src/platforms/slackbot/listener.js.map +1 -1
  134. package/dist/src/platforms/slackbot/types.d.ts.map +1 -1
  135. package/dist/src/platforms/teams/app-config.js.map +1 -1
  136. package/dist/src/platforms/teams/cli.d.ts.map +1 -1
  137. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  138. package/dist/src/platforms/teams/client.js.map +1 -1
  139. package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
  140. package/dist/src/platforms/teams/device-code.js.map +1 -1
  141. package/dist/src/platforms/teams/device-login.d.ts.map +1 -1
  142. package/dist/src/platforms/teams/device-login.js.map +1 -1
  143. package/dist/src/platforms/teams/listener.d.ts.map +1 -1
  144. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  145. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  146. package/dist/src/platforms/teams/token-provider.d.ts.map +1 -1
  147. package/dist/src/platforms/teams/token-provider.js.map +1 -1
  148. package/dist/src/platforms/teams/types.d.ts +14 -14
  149. package/dist/src/platforms/teams/types.d.ts.map +1 -1
  150. package/dist/src/platforms/telegram/cli.d.ts.map +1 -1
  151. package/dist/src/platforms/telegram/client.d.ts.map +1 -1
  152. package/dist/src/platforms/telegram/client.js.map +1 -1
  153. package/dist/src/platforms/telegram/commands/shared.js.map +1 -1
  154. package/dist/src/platforms/telegram/credential-manager.d.ts.map +1 -1
  155. package/dist/src/platforms/telegram/tdlib.d.ts.map +1 -1
  156. package/dist/src/platforms/telegram/types.d.ts.map +1 -1
  157. package/dist/src/platforms/telegram/types.js.map +1 -1
  158. package/dist/src/platforms/telegrambot/cli.d.ts.map +1 -1
  159. package/dist/src/platforms/telegrambot/client.d.ts.map +1 -1
  160. package/dist/src/platforms/telegrambot/credential-manager.d.ts.map +1 -1
  161. package/dist/src/platforms/telegrambot/listener.d.ts.map +1 -1
  162. package/dist/src/platforms/telegrambot/types.d.ts.map +1 -1
  163. package/dist/src/platforms/webex/cli.d.ts.map +1 -1
  164. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  165. package/dist/src/platforms/webex/client.js.map +1 -1
  166. package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -1
  167. package/dist/src/platforms/webex/encryption.d.ts.map +1 -1
  168. package/dist/src/platforms/webex/kms-key-provider.d.ts.map +1 -1
  169. package/dist/src/platforms/webex/listener.d.ts.map +1 -1
  170. package/dist/src/platforms/webex/listener.js.map +1 -1
  171. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  172. package/dist/src/platforms/webex/types.d.ts +4 -4
  173. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  174. package/dist/src/platforms/webexbot/cli.d.ts.map +1 -1
  175. package/dist/src/platforms/webexbot/client.d.ts.map +1 -1
  176. package/dist/src/platforms/webexbot/credential-manager.d.ts.map +1 -1
  177. package/dist/src/platforms/webexbot/listener.d.ts.map +1 -1
  178. package/dist/src/platforms/webexbot/types.d.ts.map +1 -1
  179. package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -1
  180. package/dist/src/platforms/wechatbot/client.d.ts.map +1 -1
  181. package/dist/src/platforms/wechatbot/credential-manager.d.ts.map +1 -1
  182. package/dist/src/platforms/wechatbot/types.d.ts.map +1 -1
  183. package/dist/src/platforms/whatsapp/cli.d.ts.map +1 -1
  184. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  185. package/dist/src/platforms/whatsapp/credential-manager.d.ts.map +1 -1
  186. package/dist/src/platforms/whatsapp/types.d.ts.map +1 -1
  187. package/dist/src/platforms/whatsapp/types.js.map +1 -1
  188. package/dist/src/platforms/whatsappbot/cli.d.ts.map +1 -1
  189. package/dist/src/platforms/whatsappbot/client.d.ts.map +1 -1
  190. package/dist/src/platforms/whatsappbot/credential-manager.d.ts.map +1 -1
  191. package/dist/src/platforms/whatsappbot/types.d.ts.map +1 -1
  192. package/dist/src/shared/chromium/browsers.js.map +1 -1
  193. package/dist/src/shared/chromium/cli-options.js.map +1 -1
  194. package/dist/src/shared/chromium/cookie-reader.d.ts.map +1 -1
  195. package/dist/src/shared/chromium/decryptor.d.ts.map +1 -1
  196. package/dist/src/shared/sqlite.js.map +1 -1
  197. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  198. package/dist/src/tui/adapters/channeltalk-adapter.d.ts.map +1 -1
  199. package/dist/src/tui/adapters/discord-adapter.d.ts.map +1 -1
  200. package/dist/src/tui/adapters/imessage-adapter.d.ts.map +1 -1
  201. package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
  202. package/dist/src/tui/adapters/kakaotalk-adapter.d.ts.map +1 -1
  203. package/dist/src/tui/adapters/line-adapter.d.ts.map +1 -1
  204. package/dist/src/tui/adapters/slack-adapter.d.ts.map +1 -1
  205. package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
  206. package/dist/src/tui/adapters/telegram-adapter.d.ts.map +1 -1
  207. package/dist/src/tui/adapters/telegram-adapter.js.map +1 -1
  208. package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -1
  209. package/dist/src/tui/adapters/whatsapp-adapter.d.ts.map +1 -1
  210. package/dist/src/tui/app.js.map +1 -1
  211. package/dist/src/tui/views/channel-picker.d.ts.map +1 -1
  212. package/dist/src/tui/views/workspace-picker.d.ts.map +1 -1
  213. package/docs/content/docs/cli/discord.mdx +7 -0
  214. package/docs/content/docs/cli/kakaotalk.mdx +16 -1
  215. package/docs/content/docs/sdk/discord.mdx +38 -0
  216. package/docs/content/docs/sdk/kakaotalk.mdx +39 -0
  217. package/e2e/README.md +2 -2
  218. package/e2e/discord.e2e.test.ts +16 -2
  219. package/package.json +2 -2
  220. package/skills/agent-channeltalk/SKILL.md +1 -1
  221. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  222. package/skills/agent-discord/SKILL.md +19 -1
  223. package/skills/agent-discordbot/SKILL.md +1 -1
  224. package/skills/agent-imessage/SKILL.md +1 -1
  225. package/skills/agent-instagram/SKILL.md +1 -1
  226. package/skills/agent-kakaotalk/SKILL.md +19 -2
  227. package/skills/agent-line/SKILL.md +1 -1
  228. package/skills/agent-slack/SKILL.md +1 -1
  229. package/skills/agent-slackbot/SKILL.md +1 -1
  230. package/skills/agent-teams/SKILL.md +1 -1
  231. package/skills/agent-telegram/SKILL.md +1 -1
  232. package/skills/agent-telegrambot/SKILL.md +1 -1
  233. package/skills/agent-webex/SKILL.md +1 -1
  234. package/skills/agent-webexbot/SKILL.md +1 -1
  235. package/skills/agent-wechatbot/SKILL.md +1 -1
  236. package/skills/agent-whatsapp/SKILL.md +1 -1
  237. package/skills/agent-whatsappbot/SKILL.md +1 -1
  238. package/src/platforms/discord/client.test.ts +351 -9
  239. package/src/platforms/discord/client.ts +166 -26
  240. package/src/platforms/discord/commands/mention.test.ts +84 -0
  241. package/src/platforms/discord/commands/mention.ts +78 -15
  242. package/src/platforms/discord/commands/reaction.test.ts +1 -1
  243. package/src/platforms/discord/commands/reaction.ts +1 -1
  244. package/src/platforms/discord/index.ts +3 -0
  245. package/src/platforms/discord/listener.test.ts +69 -2
  246. package/src/platforms/discord/listener.ts +31 -2
  247. package/src/platforms/discord/types.test.ts +11 -0
  248. package/src/platforms/discord/types.ts +66 -10
  249. package/src/platforms/kakaotalk/client.test.ts +194 -1
  250. package/src/platforms/kakaotalk/client.ts +58 -0
  251. package/src/platforms/kakaotalk/commands/chat.test.ts +24 -0
  252. package/src/platforms/kakaotalk/commands/chat.ts +23 -0
  253. package/src/platforms/kakaotalk/index.test.ts +10 -0
  254. package/src/platforms/kakaotalk/index.ts +4 -0
  255. package/src/platforms/kakaotalk/protocol/session.test.ts +113 -0
  256. package/src/platforms/kakaotalk/protocol/session.ts +56 -0
  257. package/src/platforms/kakaotalk/types.ts +24 -0
  258. package/tsconfig.json +8 -8
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
2
2
 
3
3
  import { DiscordClient } from '../client'
4
4
  import { DiscordCredentialManager } from '../credential-manager'
5
+ import { unreadAction } from './mention'
5
6
 
6
7
  let clientGetMentionsSpy: ReturnType<typeof spyOn>
7
8
  let credManagerLoadSpy: ReturnType<typeof spyOn>
@@ -128,3 +129,86 @@ it('getMentions: includes mention metadata', async () => {
128
129
  expect(mention.mention_everyone).toBeDefined()
129
130
  expect(mention.mentions).toBeDefined()
130
131
  })
132
+
133
+ it('unread command: emits snake_case JSON and forwards guild/limit options', async () => {
134
+ // given: an authenticated client whose getUnreadMentions returns one unread mention
135
+ credManagerLoadSpy.mockResolvedValue({
136
+ token: 'test-token',
137
+ current_server: null,
138
+ servers: {},
139
+ })
140
+ const loginSpy = spyOn(DiscordClient.prototype, 'login').mockImplementation(async function (this: DiscordClient) {
141
+ return this
142
+ })
143
+ const getUnreadSpy = spyOn(DiscordClient.prototype, 'getUnreadMentions').mockResolvedValue({
144
+ mentions: [
145
+ {
146
+ id: 'msg-1',
147
+ channel_id: 'ch-1',
148
+ author: { id: 'user-1', username: 'alice' },
149
+ content: 'Hey @testuser',
150
+ timestamp: '2024-01-29T10:00:00Z',
151
+ mention_everyone: false,
152
+ mentions: [{ id: 'user-me', username: 'testuser' }],
153
+ guild_id: 'guild-1',
154
+ mention_count: 2,
155
+ },
156
+ ],
157
+ count: 1,
158
+ badgeCount: 2,
159
+ complete: true,
160
+ windowDays: 7,
161
+ })
162
+ const consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
163
+
164
+ // when: running the unread action with guild and limit
165
+ await unreadAction({ guild: 'guild-1', limit: 200 })
166
+
167
+ // then: options are forwarded and the emitted JSON uses documented snake_case keys
168
+ expect(getUnreadSpy).toHaveBeenCalledWith({ guildId: 'guild-1', limit: 200 })
169
+ const lastCall = consoleLogSpy.mock.calls.at(-1) as [string]
170
+ const emitted = JSON.parse(lastCall[0])
171
+ expect(emitted).toEqual({
172
+ mentions: [
173
+ {
174
+ id: 'msg-1',
175
+ channel_id: 'ch-1',
176
+ author: 'alice',
177
+ content: 'Hey @testuser',
178
+ timestamp: '2024-01-29T10:00:00Z',
179
+ mention_everyone: false,
180
+ mentioned_users: ['testuser'],
181
+ guild_id: 'guild-1',
182
+ },
183
+ ],
184
+ count: 1,
185
+ badge_count: 2,
186
+ complete: true,
187
+ window_days: 7,
188
+ })
189
+
190
+ loginSpy.mockRestore()
191
+ getUnreadSpy.mockRestore()
192
+ consoleLogSpy.mockRestore()
193
+ })
194
+
195
+ it('unread command: exits with error when not authenticated', async () => {
196
+ // given: no token in credentials
197
+ credManagerLoadSpy.mockResolvedValue({
198
+ token: undefined,
199
+ current_server: null,
200
+ servers: {},
201
+ })
202
+ const consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
203
+ const processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never)
204
+
205
+ // when: running the unread action
206
+ await unreadAction({})
207
+
208
+ // then: it reports the auth error and exits
209
+ expect(consoleLogSpy).toHaveBeenCalledWith(JSON.stringify({ error: 'Not authenticated. Run "auth extract" first.' }))
210
+ expect(processExitSpy).toHaveBeenCalledWith(1)
211
+
212
+ consoleLogSpy.mockRestore()
213
+ processExitSpy.mockRestore()
214
+ })
@@ -1,4 +1,4 @@
1
- import { Command } from 'commander'
1
+ import { Command, InvalidArgumentError } from 'commander'
2
2
 
3
3
  import { handleError } from '@/shared/utils/error-handler'
4
4
  import { formatOutput } from '@/shared/utils/output'
@@ -6,6 +6,14 @@ import { formatOutput } from '@/shared/utils/output'
6
6
  import { DiscordClient } from '../client'
7
7
  import { DiscordCredentialManager } from '../credential-manager'
8
8
 
9
+ function parseLimit(value: string): number {
10
+ const parsed = Number(value)
11
+ if (!Number.isInteger(parsed) || parsed < 1) {
12
+ throw new InvalidArgumentError('Must be a positive integer.')
13
+ }
14
+ return parsed
15
+ }
16
+
9
17
  export async function listAction(options: { limit?: number; guild?: string; pretty?: boolean }): Promise<void> {
10
18
  try {
11
19
  const credManager = new DiscordCredentialManager()
@@ -39,17 +47,72 @@ export async function listAction(options: { limit?: number; guild?: string; pret
39
47
  }
40
48
  }
41
49
 
42
- export const mentionCommand = new Command('mention').description('Mention commands').addCommand(
43
- new Command('list')
44
- .description('List mentions')
45
- .option('--limit <n>', 'Number of mentions to fetch', '25')
46
- .option('--guild <guild-id>', 'Filter by guild ID')
47
- .option('--pretty', 'Pretty print JSON output')
48
- .action((options) => {
49
- listAction({
50
- limit: options.limit ? parseInt(options.limit, 10) : undefined,
51
- guild: options.guild,
52
- pretty: options.pretty,
53
- })
54
- }),
55
- )
50
+ export async function unreadAction(options: { guild?: string; limit?: number; pretty?: boolean }): Promise<void> {
51
+ try {
52
+ const credManager = new DiscordCredentialManager()
53
+ const config = await credManager.load()
54
+
55
+ if (!config.token) {
56
+ console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
57
+ process.exit(1)
58
+ }
59
+
60
+ const client = await new DiscordClient().login({ token: config.token })
61
+ const result = await client.getUnreadMentions({
62
+ guildId: options.guild,
63
+ limit: options.limit,
64
+ })
65
+
66
+ const output = {
67
+ mentions: result.mentions.map((mention) => ({
68
+ id: mention.id,
69
+ channel_id: mention.channel_id,
70
+ author: mention.author.username,
71
+ content: mention.content,
72
+ timestamp: mention.timestamp,
73
+ mention_everyone: mention.mention_everyone,
74
+ mentioned_users: mention.mentions.map((u) => u.username),
75
+ guild_id: mention.guild_id || null,
76
+ })),
77
+ count: result.count,
78
+ badge_count: result.badgeCount,
79
+ complete: result.complete,
80
+ window_days: result.windowDays,
81
+ }
82
+
83
+ console.log(formatOutput(output, options.pretty))
84
+ } catch (error) {
85
+ handleError(error as Error)
86
+ }
87
+ }
88
+
89
+ export const mentionCommand = new Command('mention')
90
+ .description('Mention commands')
91
+ .addCommand(
92
+ new Command('list')
93
+ .description('List mentions')
94
+ .option('--limit <n>', 'Number of mentions to fetch', parseLimit, 25)
95
+ .option('--guild <guild-id>', 'Filter by guild ID')
96
+ .option('--pretty', 'Pretty print JSON output')
97
+ .action((options) => {
98
+ listAction({
99
+ limit: options.limit,
100
+ guild: options.guild,
101
+ pretty: options.pretty,
102
+ })
103
+ }),
104
+ )
105
+ .addCommand(
106
+ new Command('unread')
107
+ .description('List unread mentions (correlates mention history with per-channel read state)')
108
+ .option('--limit <n>', 'Max number of mentions to scan', parseLimit)
109
+ .option('--guild <guild-id>', 'Filter by guild ID')
110
+ .option('--pretty', 'Pretty print JSON output')
111
+ .action((options) => {
112
+ unreadAction({
113
+ limit: options.limit,
114
+ guild: options.guild,
115
+ pretty: options.pretty,
116
+ })
117
+ }),
118
+ )
@@ -37,7 +37,7 @@ beforeEach(() => {
37
37
  count: 1,
38
38
  },
39
39
  ],
40
- } as any)
40
+ })
41
41
 
42
42
  // Spy on DiscordCredentialManager.prototype methods
43
43
  credManagerLoadSpy = spyOn(DiscordCredentialManager.prototype, 'load').mockResolvedValue({
@@ -101,7 +101,7 @@ export async function listAction(channelId: string, messageId: string, options:
101
101
  process.exit(1)
102
102
  }
103
103
 
104
- const reactions = (message as any).reactions || []
104
+ const reactions = message.reactions ?? []
105
105
 
106
106
  console.log(
107
107
  formatOutput(
@@ -25,10 +25,13 @@ export type {
25
25
  DiscordMention,
26
26
  DiscordMessage,
27
27
  DiscordReaction,
28
+ DiscordReadState,
28
29
  DiscordRelationship,
29
30
  DiscordSearchOptions,
30
31
  DiscordSearchResponse,
31
32
  DiscordSearchResult,
33
+ DiscordUnreadMention,
34
+ DiscordUnreadMentionsResult,
32
35
  DiscordUser,
33
36
  DiscordUserNote,
34
37
  DiscordUserProfile,
@@ -1,6 +1,6 @@
1
1
  import { afterEach, describe, expect, mock, it } from 'bun:test'
2
2
 
3
- import { DiscordListener } from '@/platforms/discord/listener'
3
+ import { DiscordListener, parseReadStates } from '@/platforms/discord/listener'
4
4
  import type {
5
5
  DiscordGatewayGenericEvent,
6
6
  DiscordGatewayMessageCreateEvent,
@@ -68,7 +68,7 @@ class MockWs {
68
68
  this.simulateMessage({ op: 10, d: { heartbeat_interval: interval } })
69
69
  }
70
70
 
71
- simulateReady(sessionId = 'session_123') {
71
+ simulateReady(sessionId = 'session_123', readState?: unknown) {
72
72
  this.simulateMessage({
73
73
  op: 0,
74
74
  t: 'READY',
@@ -77,6 +77,7 @@ class MockWs {
77
77
  session_id: sessionId,
78
78
  resume_gateway_url: 'wss://resume.discord.gg',
79
79
  user: { id: 'U_SELF', username: 'testbot' },
80
+ ...(readState === undefined ? {} : { read_state: readState }),
80
81
  },
81
82
  })
82
83
  }
@@ -135,6 +136,33 @@ async function startConnecting(listener: DiscordListener): Promise<MockWs> {
135
136
  return ws
136
137
  }
137
138
 
139
+ describe('parseReadStates', () => {
140
+ it('filters out non-channel read state types', () => {
141
+ const result = parseReadStates([
142
+ { id: 'c1', last_message_id: '100', mention_count: 1 },
143
+ { id: 'c2', read_state_type: 2, mention_count: 5 },
144
+ ])
145
+
146
+ expect(result).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 1 }])
147
+ })
148
+
149
+ it('normalizes missing last_message_id and mention_count', () => {
150
+ const result = parseReadStates([{ id: 'c1' }])
151
+
152
+ expect(result).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 0 }])
153
+ })
154
+
155
+ it('skips malformed entries but keeps valid ones', () => {
156
+ const result = parseReadStates([{ id: 'c1', mention_count: 1 }, { mention_count: 3 }])
157
+
158
+ expect(result).toEqual([{ channelId: 'c1', lastMessageId: null, mentionCount: 1 }])
159
+ })
160
+
161
+ it('throws on a non-array, non-versioned payload', () => {
162
+ expect(() => parseReadStates({ foo: 'bar' })).toThrow(/invalid read_state/)
163
+ })
164
+ })
165
+
138
166
  describe('DiscordListener', () => {
139
167
  let listener: DiscordListener
140
168
 
@@ -235,6 +263,45 @@ describe('DiscordListener', () => {
235
263
  })
236
264
  })
237
265
 
266
+ describe('read state', () => {
267
+ it('captures raw-array read_state from READY', async () => {
268
+ const client = createMockClient()
269
+ listener = new DiscordListener(client)
270
+
271
+ const started = listener.start()
272
+ const ws = await waitForSocket()
273
+ ws.simulateOpen()
274
+ ws.simulateHello()
275
+ ws.simulateReady('session_123', [{ id: 'c1', last_message_id: '100', mention_count: 2 }])
276
+ await started
277
+
278
+ expect(listener.getReadState()).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 2 }])
279
+ })
280
+
281
+ it('captures versioned { entries } read_state from READY', async () => {
282
+ const client = createMockClient()
283
+ listener = new DiscordListener(client)
284
+
285
+ const started = listener.start()
286
+ const ws = await waitForSocket()
287
+ ws.simulateOpen()
288
+ ws.simulateHello()
289
+ ws.simulateReady('session_123', { version: 7, entries: [{ id: 'c1', last_message_id: '100', mention_count: 1 }] })
290
+ await started
291
+
292
+ expect(listener.getReadState()).toEqual([{ channelId: 'c1', lastMessageId: '100', mentionCount: 1 }])
293
+ })
294
+
295
+ it('defaults to an empty read state when READY omits read_state', async () => {
296
+ const client = createMockClient()
297
+ listener = new DiscordListener(client)
298
+
299
+ await startAndReady(listener)
300
+
301
+ expect(listener.getReadState()).toEqual([])
302
+ })
303
+ })
304
+
238
305
  describe('identify', () => {
239
306
  it('sends Identify after Hello', async () => {
240
307
  const client = createMockClient()
@@ -3,8 +3,30 @@ import { EventEmitter } from 'events'
3
3
  import WebSocket from 'ws'
4
4
 
5
5
  import type { DiscordClient } from './client'
6
- import type { DiscordListenerEventMap, DiscordGatewayGenericEvent } from './types'
7
- import { DiscordGatewayOpcode } from './types'
6
+ import type { DiscordListenerEventMap, DiscordGatewayGenericEvent, DiscordReadState } from './types'
7
+ import { DiscordGatewayOpcode, DiscordRawReadStateSchema, DiscordReadyReadStateSchema } from './types'
8
+
9
+ export function parseReadStates(value: unknown): DiscordReadState[] {
10
+ const parsed = DiscordReadyReadStateSchema.safeParse(value)
11
+ if (!parsed.success) {
12
+ throw new Error('Discord READY contained an invalid read_state payload')
13
+ }
14
+
15
+ const entries = Array.isArray(parsed.data) ? parsed.data : parsed.data.entries
16
+
17
+ const readStates: DiscordReadState[] = []
18
+ for (const entry of entries) {
19
+ const state = DiscordRawReadStateSchema.safeParse(entry)
20
+ if (!state.success) continue
21
+ if ((state.data.read_state_type ?? 0) !== 0) continue
22
+ readStates.push({
23
+ channelId: state.data.id,
24
+ lastMessageId: state.data.last_message_id ?? null,
25
+ mentionCount: state.data.mention_count ?? 0,
26
+ })
27
+ }
28
+ return readStates
29
+ }
8
30
 
9
31
  const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json'
10
32
  const GATEWAY_QUERY = '?v=10&encoding=json'
@@ -57,6 +79,7 @@ export class DiscordListener {
57
79
  private resumeGatewayUrl: string | null = null
58
80
  private token: string | null = null
59
81
  private cachedUser: { id: string; username: string } | null = null
82
+ private readState: DiscordReadState[] | null = null
60
83
  private generation = 0
61
84
  private startPromise: Promise<void> | null = null
62
85
  private pendingStartReject: ((error: Error) => void) | null = null
@@ -151,6 +174,7 @@ export class DiscordListener {
151
174
  this.resumeGatewayUrl = null
152
175
  this.token = null
153
176
  this.cachedUser = null
177
+ this.readState = null
154
178
  rejectStart?.(new Error('Discord gateway start was stopped before becoming ready'))
155
179
  }
156
180
 
@@ -174,6 +198,10 @@ export class DiscordListener {
174
198
  return this
175
199
  }
176
200
 
201
+ getReadState(): DiscordReadState[] | null {
202
+ return this.readState
203
+ }
204
+
177
205
  private isCurrent(generation: number, ws?: WebSocket): boolean {
178
206
  if (generation !== this.generation || !this.running) return false
179
207
  if (ws !== undefined && this.ws !== ws) return false
@@ -298,6 +326,7 @@ export class DiscordListener {
298
326
  this.sessionId = d.session_id
299
327
  this.resumeGatewayUrl = d.resume_gateway_url
300
328
  this.cachedUser = d.user
329
+ this.readState = d.read_state === undefined ? [] : parseReadStates(d.read_state)
301
330
  this.reconnectAttempts = 0
302
331
  this.emitter.emit('connected', { user: d.user, sessionId: d.session_id })
303
332
  return
@@ -150,6 +150,17 @@ it('DiscordReactionSchema validates reaction with custom emoji', () => {
150
150
  expect(result.success).toBe(true)
151
151
  })
152
152
 
153
+ it('DiscordReactionSchema validates nullable emoji fields', () => {
154
+ const result = DiscordReactionSchema.safeParse({
155
+ emoji: {
156
+ id: null,
157
+ name: null,
158
+ },
159
+ count: 1,
160
+ })
161
+ expect(result.success).toBe(true)
162
+ })
163
+
153
164
  it('DiscordReactionSchema rejects missing required fields', () => {
154
165
  const result = DiscordReactionSchema.safeParse({
155
166
  emoji: {
@@ -38,6 +38,7 @@ export interface DiscordMessage {
38
38
  timestamp: string
39
39
  edited_timestamp?: string
40
40
  thread_id?: string
41
+ reactions?: DiscordReaction[]
41
42
  }
42
43
 
43
44
  export interface DiscordUser {
@@ -58,8 +59,8 @@ export interface DiscordDMChannel {
58
59
 
59
60
  export interface DiscordReaction {
60
61
  emoji: {
61
- id?: string
62
- name: string
62
+ id?: string | null
63
+ name: string | null
63
64
  }
64
65
  count: number
65
66
  }
@@ -91,6 +92,26 @@ export interface DiscordUserNote {
91
92
  note: string
92
93
  }
93
94
 
95
+ export interface DiscordReadState {
96
+ channelId: string
97
+ lastMessageId: string | null
98
+ mentionCount: number
99
+ }
100
+
101
+ export interface DiscordUnreadMention extends DiscordMention {
102
+ mention_count: number
103
+ }
104
+
105
+ export interface DiscordUnreadMentionsResult {
106
+ mentions: DiscordUnreadMention[]
107
+ count: number
108
+ // Account-wide unread-mention badge total; not narrowed by guildId because
109
+ // READY read states carry no guild association.
110
+ badgeCount: number
111
+ complete: boolean
112
+ windowDays: number
113
+ }
114
+
94
115
  export interface DiscordSearchResult {
95
116
  id: string
96
117
  channel_id: string
@@ -109,6 +130,15 @@ export interface DiscordSearchResponse {
109
130
  messages: DiscordSearchResult[][]
110
131
  }
111
132
 
133
+ export interface DiscordSearchIndexNotReadyResponse {
134
+ message: string
135
+ code: 110000
136
+ documents_indexed: number
137
+ retry_after: number
138
+ }
139
+
140
+ export type DiscordSearchApiResponse = DiscordSearchResponse | DiscordSearchIndexNotReadyResponse
141
+
112
142
  export interface DiscordSearchOptions {
113
143
  channelId?: string
114
144
  authorId?: string
@@ -181,6 +211,14 @@ export const DiscordChannelSchema = z.object({
181
211
  position: z.number().optional(),
182
212
  })
183
213
 
214
+ export const DiscordReactionSchema = z.object({
215
+ emoji: z.object({
216
+ id: z.string().nullish(),
217
+ name: z.string().nullable(),
218
+ }),
219
+ count: z.number(),
220
+ })
221
+
184
222
  export const DiscordMessageSchema = z.object({
185
223
  id: z.string(),
186
224
  channel_id: z.string(),
@@ -192,6 +230,7 @@ export const DiscordMessageSchema = z.object({
192
230
  timestamp: z.string(),
193
231
  edited_timestamp: z.string().optional(),
194
232
  thread_id: z.string().optional(),
233
+ reactions: z.array(DiscordReactionSchema).optional(),
195
234
  })
196
235
 
197
236
  export const DiscordUserSchema = z.object({
@@ -210,14 +249,6 @@ export const DiscordDMChannelSchema = z.object({
210
249
  name: z.string().optional(),
211
250
  })
212
251
 
213
- export const DiscordReactionSchema = z.object({
214
- emoji: z.object({
215
- id: z.string().optional(),
216
- name: z.string(),
217
- }),
218
- count: z.number(),
219
- })
220
-
221
252
  export const DiscordFileSchema = z.object({
222
253
  id: z.string(),
223
254
  filename: z.string(),
@@ -267,6 +298,13 @@ export const DiscordSearchResponseSchema = z.object({
267
298
  messages: z.array(z.array(DiscordSearchResultSchema)),
268
299
  })
269
300
 
301
+ export const DiscordSearchIndexNotReadyResponseSchema = z.object({
302
+ message: z.string(),
303
+ code: z.literal(110000),
304
+ documents_indexed: z.number(),
305
+ retry_after: z.number().nonnegative(),
306
+ })
307
+
270
308
  export const DiscordCredentialsSchema = z.object({
271
309
  token: z.string(),
272
310
  })
@@ -386,6 +424,24 @@ export interface DiscordGatewayGenericEvent {
386
424
  [key: string]: unknown
387
425
  }
388
426
 
427
+ // read_state_type 0 = channel read state (carries last_message_id); non-zero types
428
+ // carry last_acked_id instead and must be filtered out for mention correlation.
429
+ export const DiscordRawReadStateSchema = z.object({
430
+ id: z.string(),
431
+ read_state_type: z.number().optional(),
432
+ last_message_id: z.string().nullish(),
433
+ mention_count: z.number().optional(),
434
+ })
435
+
436
+ export type DiscordRawReadState = z.infer<typeof DiscordRawReadStateSchema>
437
+
438
+ // READY sends read_state as a raw array, or as { entries, version } when the
439
+ // VERSIONED_READ_STATES gateway capability is negotiated.
440
+ export const DiscordReadyReadStateSchema = z.union([
441
+ z.array(z.unknown()),
442
+ z.object({ entries: z.array(z.unknown()), version: z.number().optional() }),
443
+ ])
444
+
389
445
  export type DiscordGatewayEvent =
390
446
  | DiscordGatewayMessageCreateEvent
391
447
  | DiscordGatewayMessageUpdateEvent