agent-messenger 2.2.0 → 2.4.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 (229) hide show
  1. package/.claude-plugin/README.md +16 -16
  2. package/.claude-plugin/marketplace.json +29 -29
  3. package/.claude-plugin/plugin.json +5 -5
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +9 -6
  6. package/bun.lock +89 -105
  7. package/bunfig.toml +3 -0
  8. package/dist/package.json +13 -3
  9. package/dist/src/platforms/discordbot/client.js +2 -2
  10. package/dist/src/platforms/discordbot/client.js.map +1 -1
  11. package/dist/src/platforms/kakaotalk/cli.d.ts.map +1 -1
  12. package/dist/src/platforms/kakaotalk/cli.js +2 -1
  13. package/dist/src/platforms/kakaotalk/cli.js.map +1 -1
  14. package/dist/src/platforms/kakaotalk/client.d.ts +2 -1
  15. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  16. package/dist/src/platforms/kakaotalk/client.js +52 -2
  17. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  18. package/dist/src/platforms/kakaotalk/commands/index.d.ts +1 -0
  19. package/dist/src/platforms/kakaotalk/commands/index.d.ts.map +1 -1
  20. package/dist/src/platforms/kakaotalk/commands/index.js +1 -0
  21. package/dist/src/platforms/kakaotalk/commands/index.js.map +1 -1
  22. package/dist/src/platforms/kakaotalk/commands/profile.d.ts +3 -0
  23. package/dist/src/platforms/kakaotalk/commands/profile.d.ts.map +1 -0
  24. package/dist/src/platforms/kakaotalk/commands/profile.js +19 -0
  25. package/dist/src/platforms/kakaotalk/commands/profile.js.map +1 -0
  26. package/dist/src/platforms/kakaotalk/index.d.ts +2 -2
  27. package/dist/src/platforms/kakaotalk/index.d.ts.map +1 -1
  28. package/dist/src/platforms/kakaotalk/index.js +1 -1
  29. package/dist/src/platforms/kakaotalk/index.js.map +1 -1
  30. package/dist/src/platforms/kakaotalk/protocol/session.d.ts.map +1 -1
  31. package/dist/src/platforms/kakaotalk/protocol/session.js +2 -1
  32. package/dist/src/platforms/kakaotalk/protocol/session.js.map +1 -1
  33. package/dist/src/platforms/kakaotalk/types.d.ts +16 -0
  34. package/dist/src/platforms/kakaotalk/types.d.ts.map +1 -1
  35. package/dist/src/platforms/kakaotalk/types.js +8 -0
  36. package/dist/src/platforms/kakaotalk/types.js.map +1 -1
  37. package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
  38. package/dist/src/platforms/line/commands/auth.js +32 -20
  39. package/dist/src/platforms/line/commands/auth.js.map +1 -1
  40. package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
  41. package/dist/src/platforms/teams/commands/reaction.js +2 -0
  42. package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
  43. package/dist/src/platforms/webex/client.d.ts +2 -0
  44. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  45. package/dist/src/platforms/webex/client.js +66 -23
  46. package/dist/src/platforms/webex/client.js.map +1 -1
  47. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  48. package/dist/src/platforms/webex/commands/auth.js +4 -0
  49. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  50. package/dist/src/platforms/webex/encryption.d.ts +10 -0
  51. package/dist/src/platforms/webex/encryption.d.ts.map +1 -0
  52. package/dist/src/platforms/webex/encryption.js +49 -0
  53. package/dist/src/platforms/webex/encryption.js.map +1 -0
  54. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  55. package/dist/src/platforms/webex/ensure-auth.js +4 -0
  56. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  57. package/dist/src/platforms/webex/token-extractor.d.ts +6 -5
  58. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -1
  59. package/dist/src/platforms/webex/token-extractor.js +92 -43
  60. package/dist/src/platforms/webex/token-extractor.js.map +1 -1
  61. package/dist/src/platforms/webex/types.d.ts +4 -0
  62. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  63. package/dist/src/platforms/webex/types.js +2 -0
  64. package/dist/src/platforms/webex/types.js.map +1 -1
  65. package/dist/src/platforms/wechatbot/cli.d.ts +5 -0
  66. package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -0
  67. package/dist/src/platforms/wechatbot/cli.js +18 -0
  68. package/dist/src/platforms/wechatbot/cli.js.map +1 -0
  69. package/dist/src/platforms/wechatbot/client.d.ts +36 -0
  70. package/dist/src/platforms/wechatbot/client.d.ts.map +1 -0
  71. package/dist/src/platforms/wechatbot/client.js +208 -0
  72. package/dist/src/platforms/wechatbot/client.js.map +1 -0
  73. package/dist/src/platforms/wechatbot/commands/auth.d.ts +28 -0
  74. package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -0
  75. package/dist/src/platforms/wechatbot/commands/auth.js +164 -0
  76. package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -0
  77. package/dist/src/platforms/wechatbot/commands/index.d.ts +5 -0
  78. package/dist/src/platforms/wechatbot/commands/index.d.ts.map +1 -0
  79. package/dist/src/platforms/wechatbot/commands/index.js +5 -0
  80. package/dist/src/platforms/wechatbot/commands/index.js.map +1 -0
  81. package/dist/src/platforms/wechatbot/commands/message.d.ts +18 -0
  82. package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -0
  83. package/dist/src/platforms/wechatbot/commands/message.js +80 -0
  84. package/dist/src/platforms/wechatbot/commands/message.js.map +1 -0
  85. package/dist/src/platforms/wechatbot/commands/shared.d.ts +9 -0
  86. package/dist/src/platforms/wechatbot/commands/shared.d.ts.map +1 -0
  87. package/dist/src/platforms/wechatbot/commands/shared.js +13 -0
  88. package/dist/src/platforms/wechatbot/commands/shared.js.map +1 -0
  89. package/dist/src/platforms/wechatbot/commands/template.d.ts +19 -0
  90. package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -0
  91. package/dist/src/platforms/wechatbot/commands/template.js +76 -0
  92. package/dist/src/platforms/wechatbot/commands/template.js.map +1 -0
  93. package/dist/src/platforms/wechatbot/commands/user.d.ts +20 -0
  94. package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -0
  95. package/dist/src/platforms/wechatbot/commands/user.js +53 -0
  96. package/dist/src/platforms/wechatbot/commands/user.js.map +1 -0
  97. package/dist/src/platforms/wechatbot/credential-manager.d.ts +17 -0
  98. package/dist/src/platforms/wechatbot/credential-manager.d.ts.map +1 -0
  99. package/dist/src/platforms/wechatbot/credential-manager.js +121 -0
  100. package/dist/src/platforms/wechatbot/credential-manager.js.map +1 -0
  101. package/dist/src/platforms/wechatbot/index.d.ts +5 -0
  102. package/dist/src/platforms/wechatbot/index.d.ts.map +1 -0
  103. package/dist/src/platforms/wechatbot/index.js +4 -0
  104. package/dist/src/platforms/wechatbot/index.js.map +1 -0
  105. package/dist/src/platforms/wechatbot/types.d.ts +94 -0
  106. package/dist/src/platforms/wechatbot/types.d.ts.map +1 -0
  107. package/dist/src/platforms/wechatbot/types.js +54 -0
  108. package/dist/src/platforms/wechatbot/types.js.map +1 -0
  109. package/dist/src/platforms/whatsapp/client.d.ts +1 -0
  110. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  111. package/dist/src/platforms/whatsapp/client.js +27 -13
  112. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  113. package/dist/src/platforms/whatsapp/commands/auth.d.ts.map +1 -1
  114. package/dist/src/platforms/whatsapp/commands/auth.js +21 -18
  115. package/dist/src/platforms/whatsapp/commands/auth.js.map +1 -1
  116. package/dist/src/platforms/whatsapp/credential-manager.d.ts.map +1 -1
  117. package/dist/src/platforms/whatsapp/credential-manager.js +14 -8
  118. package/dist/src/platforms/whatsapp/credential-manager.js.map +1 -1
  119. package/docs/content/docs/agent-skills.mdx +4 -4
  120. package/docs/content/docs/cli/channeltalk.mdx +1 -1
  121. package/docs/content/docs/cli/channeltalkbot.mdx +1 -1
  122. package/docs/content/docs/cli/discord.mdx +1 -1
  123. package/docs/content/docs/cli/discordbot.mdx +1 -1
  124. package/docs/content/docs/cli/instagram.mdx +1 -1
  125. package/docs/content/docs/cli/kakaotalk.mdx +1 -1
  126. package/docs/content/docs/cli/line.mdx +1 -1
  127. package/docs/content/docs/cli/meta.json +1 -0
  128. package/docs/content/docs/cli/slack.mdx +1 -1
  129. package/docs/content/docs/cli/slackbot.mdx +1 -1
  130. package/docs/content/docs/cli/teams.mdx +1 -1
  131. package/docs/content/docs/cli/webex.mdx +5 -3
  132. package/docs/content/docs/cli/wechatbot.mdx +179 -0
  133. package/docs/content/docs/cli/whatsapp.mdx +1 -1
  134. package/docs/content/docs/cli/whatsappbot.mdx +1 -1
  135. package/docs/content/docs/sdk/meta.json +1 -1
  136. package/docs/content/docs/sdk/wechatbot.mdx +282 -0
  137. package/docs/content/docs/tui.mdx +1 -1
  138. package/docs/src/app/page.tsx +5 -5
  139. package/package.json +13 -3
  140. package/skills/agent-channeltalk/SKILL.md +1 -1
  141. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  142. package/skills/agent-discord/SKILL.md +1 -1
  143. package/skills/agent-discordbot/SKILL.md +1 -1
  144. package/skills/agent-instagram/SKILL.md +1 -1
  145. package/skills/agent-kakaotalk/SKILL.md +24 -1
  146. package/skills/agent-line/SKILL.md +7 -11
  147. package/skills/agent-line/references/authentication.md +13 -4
  148. package/skills/agent-slack/SKILL.md +1 -1
  149. package/skills/agent-slackbot/SKILL.md +1 -1
  150. package/skills/agent-teams/SKILL.md +1 -1
  151. package/skills/agent-telegram/SKILL.md +1 -1
  152. package/skills/agent-webex/SKILL.md +1 -1
  153. package/skills/agent-webex/references/authentication.md +4 -3
  154. package/skills/agent-webex/references/common-patterns.md +1 -1
  155. package/skills/agent-wechatbot/SKILL.md +385 -0
  156. package/skills/agent-whatsapp/SKILL.md +12 -1
  157. package/skills/agent-whatsappbot/SKILL.md +1 -1
  158. package/src/platforms/discord/credential-manager.test.ts +18 -1
  159. package/src/platforms/discordbot/client.ts +2 -2
  160. package/src/platforms/instagram/commands/auth.test.ts +216 -0
  161. package/src/platforms/instagram/commands/chat.test.ts +127 -0
  162. package/src/platforms/instagram/commands/message.test.ts +178 -0
  163. package/src/platforms/kakaotalk/cli.ts +2 -1
  164. package/src/platforms/kakaotalk/client.test.ts +157 -0
  165. package/src/platforms/kakaotalk/client.ts +57 -3
  166. package/src/platforms/kakaotalk/commands/auth.test.ts +299 -0
  167. package/src/platforms/kakaotalk/commands/chat.test.ts +97 -0
  168. package/src/platforms/kakaotalk/commands/index.ts +1 -0
  169. package/src/platforms/kakaotalk/commands/message.test.ts +113 -0
  170. package/src/platforms/kakaotalk/commands/profile.test.ts +84 -0
  171. package/src/platforms/kakaotalk/commands/profile.ts +21 -0
  172. package/src/platforms/kakaotalk/index.test.ts +5 -0
  173. package/src/platforms/kakaotalk/index.ts +2 -0
  174. package/src/platforms/kakaotalk/protocol/session.ts +2 -0
  175. package/src/platforms/kakaotalk/types.ts +18 -0
  176. package/src/platforms/line/commands/auth.test.ts +141 -0
  177. package/src/platforms/line/commands/auth.ts +28 -19
  178. package/src/platforms/line/commands/chat.test.ts +110 -0
  179. package/src/platforms/line/commands/friend.test.ts +98 -0
  180. package/src/platforms/line/commands/message.test.ts +119 -0
  181. package/src/platforms/line/commands/profile.test.ts +85 -0
  182. package/src/platforms/slackbot/commands/channel.test.ts +139 -0
  183. package/src/platforms/slackbot/commands/message.test.ts +226 -0
  184. package/src/platforms/slackbot/commands/reaction.test.ts +90 -0
  185. package/src/platforms/slackbot/commands/user.test.ts +143 -0
  186. package/src/platforms/teams/commands/reaction.test.ts +45 -61
  187. package/src/platforms/teams/commands/reaction.ts +2 -0
  188. package/src/platforms/telegram/commands/chat.test.ts +125 -0
  189. package/src/platforms/telegram/commands/message.test.ts +92 -0
  190. package/src/platforms/webex/client.ts +98 -26
  191. package/src/platforms/webex/commands/auth.ts +4 -0
  192. package/src/platforms/webex/commands/member.test.ts +65 -58
  193. package/src/platforms/webex/commands/message.test.ts +78 -121
  194. package/src/platforms/webex/commands/snapshot.test.ts +59 -46
  195. package/src/platforms/webex/commands/space.test.ts +49 -48
  196. package/src/platforms/webex/encryption.ts +53 -0
  197. package/src/platforms/webex/ensure-auth.ts +4 -0
  198. package/src/platforms/webex/token-extractor.ts +107 -40
  199. package/src/platforms/webex/types.ts +4 -0
  200. package/src/platforms/webex/typings/node-jose.d.ts +27 -0
  201. package/src/platforms/wechatbot/cli.ts +24 -0
  202. package/src/platforms/wechatbot/client.test.ts +497 -0
  203. package/src/platforms/wechatbot/client.ts +268 -0
  204. package/src/platforms/wechatbot/commands/auth.test.ts +211 -0
  205. package/src/platforms/wechatbot/commands/auth.ts +203 -0
  206. package/src/platforms/wechatbot/commands/index.ts +4 -0
  207. package/src/platforms/wechatbot/commands/message.test.ts +155 -0
  208. package/src/platforms/wechatbot/commands/message.ts +104 -0
  209. package/src/platforms/wechatbot/commands/shared.ts +22 -0
  210. package/src/platforms/wechatbot/commands/template.test.ts +199 -0
  211. package/src/platforms/wechatbot/commands/template.ts +102 -0
  212. package/src/platforms/wechatbot/commands/user.test.ts +165 -0
  213. package/src/platforms/wechatbot/commands/user.ts +75 -0
  214. package/src/platforms/wechatbot/credential-manager.test.ts +255 -0
  215. package/src/platforms/wechatbot/credential-manager.ts +148 -0
  216. package/src/platforms/wechatbot/index.test.ts +49 -0
  217. package/src/platforms/wechatbot/index.ts +19 -0
  218. package/src/platforms/wechatbot/types.test.ts +223 -0
  219. package/src/platforms/wechatbot/types.ts +107 -0
  220. package/src/platforms/whatsapp/client.ts +24 -13
  221. package/src/platforms/whatsapp/commands/auth.test.ts +311 -0
  222. package/src/platforms/whatsapp/commands/auth.ts +21 -17
  223. package/src/platforms/whatsapp/commands/chat.test.ts +198 -0
  224. package/src/platforms/whatsapp/commands/message.test.ts +231 -0
  225. package/src/platforms/whatsapp/credential-manager.test.ts +20 -0
  226. package/src/platforms/whatsapp/credential-manager.ts +17 -8
  227. package/src/platforms/whatsappbot/commands/auth.test.ts +217 -0
  228. package/src/platforms/whatsappbot/commands/message.test.ts +198 -0
  229. package/src/platforms/whatsappbot/commands/template.test.ts +112 -0
@@ -0,0 +1,231 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ mock.module('@/shared/utils/error-handler', () => ({
6
+ handleError: (err: Error) => { throw err },
7
+ }))
8
+
9
+ const mockGetAccount = mock(() =>
10
+ Promise.resolve({
11
+ account_id: 'plus-12025551234',
12
+ phone_number: '+12025551234',
13
+ name: 'Test User',
14
+ created_at: '2024-01-01T00:00:00.000Z',
15
+ updated_at: '2024-01-01T00:00:00.000Z',
16
+ }),
17
+ )
18
+ const mockEnsureAccountPaths = mock(() =>
19
+ Promise.resolve({ account_dir: '/tmp/test', auth_dir: '/tmp/test/auth' }),
20
+ )
21
+
22
+ mock.module('../credential-manager', () => ({
23
+ WhatsAppCredentialManager: class {
24
+ getAccount = mockGetAccount
25
+ ensureAccountPaths = mockEnsureAccountPaths
26
+ },
27
+ }))
28
+
29
+ const mockGetMessages = mock(() =>
30
+ Promise.resolve([
31
+ {
32
+ id: 'msg-1',
33
+ text: 'Hello',
34
+ from: '12025551234@s.whatsapp.net',
35
+ timestamp: 1000,
36
+ fromMe: false,
37
+ },
38
+ ]),
39
+ )
40
+
41
+ const mockSendMessage = mock(() =>
42
+ Promise.resolve({
43
+ id: 'msg-2',
44
+ text: 'Hi there',
45
+ from: 'me@s.whatsapp.net',
46
+ timestamp: 2000,
47
+ fromMe: true,
48
+ }),
49
+ )
50
+
51
+ const mockSendReaction = mock(() => Promise.resolve())
52
+ const mockConnect = mock(() => Promise.resolve())
53
+ const mockClose = mock(() => Promise.resolve())
54
+
55
+ mock.module('../client', () => ({
56
+ WhatsAppClient: class {
57
+ login = mock(function (this: unknown) { return Promise.resolve(this) })
58
+ connect = mockConnect
59
+ close = mockClose
60
+ getMessages = mockGetMessages
61
+ sendMessage = mockSendMessage
62
+ sendReaction = mockSendReaction
63
+ },
64
+ }))
65
+
66
+ import { messageCommand } from './message'
67
+
68
+ describe('message commands', () => {
69
+ let consoleLogSpy: ReturnType<typeof mock>
70
+ let processExitSpy: ReturnType<typeof spyOn>
71
+
72
+ beforeEach(() => {
73
+ mockGetAccount.mockReset()
74
+ mockEnsureAccountPaths.mockReset()
75
+ mockGetMessages.mockReset()
76
+ mockSendMessage.mockReset()
77
+ mockSendReaction.mockReset()
78
+ mockConnect.mockReset()
79
+ mockClose.mockReset()
80
+
81
+ mockGetAccount.mockImplementation(() =>
82
+ Promise.resolve({
83
+ account_id: 'plus-12025551234',
84
+ phone_number: '+12025551234',
85
+ name: 'Test User',
86
+ created_at: '2024-01-01T00:00:00.000Z',
87
+ updated_at: '2024-01-01T00:00:00.000Z',
88
+ }),
89
+ )
90
+ mockEnsureAccountPaths.mockImplementation(() =>
91
+ Promise.resolve({ account_dir: '/tmp/test', auth_dir: '/tmp/test/auth' }),
92
+ )
93
+ mockGetMessages.mockImplementation(() =>
94
+ Promise.resolve([
95
+ {
96
+ id: 'msg-1',
97
+ text: 'Hello',
98
+ from: '12025551234@s.whatsapp.net',
99
+ timestamp: 1000,
100
+ fromMe: false,
101
+ },
102
+ ]),
103
+ )
104
+ mockSendMessage.mockImplementation(() =>
105
+ Promise.resolve({
106
+ id: 'msg-2',
107
+ text: 'Hi there',
108
+ from: 'me@s.whatsapp.net',
109
+ timestamp: 2000,
110
+ fromMe: true,
111
+ }),
112
+ )
113
+ mockSendReaction.mockImplementation(() => Promise.resolve())
114
+ mockConnect.mockImplementation(() => Promise.resolve())
115
+ mockClose.mockImplementation(() => Promise.resolve())
116
+
117
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
118
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
119
+ throw new Error(`process.exit(${_code})`)
120
+ })
121
+ processExitSpy.mockClear()
122
+ })
123
+
124
+ afterEach(() => {
125
+ console.log = originalConsoleLog
126
+ processExitSpy.mockRestore()
127
+ })
128
+
129
+ describe('list', () => {
130
+ test('fetches messages for a chat', async () => {
131
+ await expect(
132
+ messageCommand.parseAsync(['list', '12025551234@s.whatsapp.net'], { from: 'user' }),
133
+ ).rejects.toThrow('process.exit(0)')
134
+
135
+ expect(mockGetMessages).toHaveBeenCalledWith('12025551234@s.whatsapp.net', 25)
136
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
137
+ expect(output).toHaveLength(1)
138
+ expect(output[0].id).toBe('msg-1')
139
+ expect(output[0].text).toBe('Hello')
140
+ })
141
+
142
+ test('respects --limit option', async () => {
143
+ await expect(
144
+ messageCommand.parseAsync(['list', '12025551234@s.whatsapp.net', '--limit', '10'], { from: 'user' }),
145
+ ).rejects.toThrow('process.exit(0)')
146
+
147
+ expect(mockGetMessages).toHaveBeenCalledWith('12025551234@s.whatsapp.net', 10)
148
+ })
149
+
150
+ test('passes account option to credential manager', async () => {
151
+ await expect(
152
+ messageCommand.parseAsync(['list', '12025551234@s.whatsapp.net', '--account', 'my-account'], { from: 'user' }),
153
+ ).rejects.toThrow('process.exit(0)')
154
+
155
+ expect(mockGetAccount).toHaveBeenCalledWith('my-account')
156
+ })
157
+
158
+ test('exits with error when no account configured', async () => {
159
+ mockGetAccount.mockImplementation(() => Promise.resolve(null))
160
+
161
+ await expect(
162
+ messageCommand.parseAsync(['list', '12025551234@s.whatsapp.net'], { from: 'user' }),
163
+ ).rejects.toThrow('process.exit(1)')
164
+
165
+ expect(processExitSpy).toHaveBeenCalledWith(1)
166
+ })
167
+ })
168
+
169
+ describe('send', () => {
170
+ test('sends a message to a chat', async () => {
171
+ await expect(
172
+ messageCommand.parseAsync(['send', '12025551234@s.whatsapp.net', 'Hello world'], { from: 'user' }),
173
+ ).rejects.toThrow('process.exit(0)')
174
+
175
+ expect(mockSendMessage).toHaveBeenCalledWith('12025551234@s.whatsapp.net', 'Hello world')
176
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
177
+ expect(output.id).toBe('msg-2')
178
+ expect(output.text).toBe('Hi there')
179
+ })
180
+
181
+ test('passes account option to credential manager', async () => {
182
+ await expect(
183
+ messageCommand.parseAsync(['send', '12025551234@s.whatsapp.net', 'Hi', '--account', 'my-account'], {
184
+ from: 'user',
185
+ }),
186
+ ).rejects.toThrow('process.exit(0)')
187
+
188
+ expect(mockGetAccount).toHaveBeenCalledWith('my-account')
189
+ })
190
+ })
191
+
192
+ describe('react', () => {
193
+ test('sends a reaction to a message', async () => {
194
+ await expect(
195
+ messageCommand.parseAsync(
196
+ ['react', '12025551234@s.whatsapp.net', 'msg-1', '👍'],
197
+ { from: 'user' },
198
+ ),
199
+ ).rejects.toThrow('process.exit(0)')
200
+
201
+ expect(mockSendReaction).toHaveBeenCalledWith('12025551234@s.whatsapp.net', 'msg-1', '👍', undefined)
202
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
203
+ expect(output.success).toBe(true)
204
+ expect(output.chat).toBe('12025551234@s.whatsapp.net')
205
+ expect(output.message_id).toBe('msg-1')
206
+ expect(output.emoji).toBe('👍')
207
+ })
208
+
209
+ test('passes --from-me flag to sendReaction', async () => {
210
+ await expect(
211
+ messageCommand.parseAsync(
212
+ ['react', '12025551234@s.whatsapp.net', 'msg-1', '❤️', '--from-me'],
213
+ { from: 'user' },
214
+ ),
215
+ ).rejects.toThrow('process.exit(0)')
216
+
217
+ expect(mockSendReaction).toHaveBeenCalledWith('12025551234@s.whatsapp.net', 'msg-1', '❤️', true)
218
+ })
219
+
220
+ test('passes account option to credential manager', async () => {
221
+ await expect(
222
+ messageCommand.parseAsync(
223
+ ['react', '12025551234@s.whatsapp.net', 'msg-1', '👍', '--account', 'my-account'],
224
+ { from: 'user' },
225
+ ),
226
+ ).rejects.toThrow('process.exit(0)')
227
+
228
+ expect(mockGetAccount).toHaveBeenCalledWith('my-account')
229
+ })
230
+ })
231
+ })
@@ -198,6 +198,26 @@ describe('WhatsAppCredentialManager', () => {
198
198
 
199
199
  expect(result).toBe(false)
200
200
  })
201
+
202
+ test('deletes session directory even when account is not in config', async () => {
203
+ const manager = setup()
204
+
205
+ const paths = await manager.ensureAccountPaths('orphan-account')
206
+ expect(existsSync(paths.account_dir)).toBe(true)
207
+
208
+ const result = await manager.removeAccount('orphan-account')
209
+
210
+ expect(result).toBe(true)
211
+ expect(existsSync(paths.account_dir)).toBe(false)
212
+ })
213
+
214
+ test('returns false when neither config entry nor directory exists', async () => {
215
+ const manager = setup()
216
+
217
+ const result = await manager.removeAccount('ghost-account')
218
+
219
+ expect(result).toBe(false)
220
+ })
201
221
  })
202
222
 
203
223
  describe('clearCredentials', () => {
@@ -91,19 +91,28 @@ export class WhatsAppCredentialManager {
91
91
  const config = await this.loadConfig()
92
92
  const account = config.accounts[accountId] ?? config.accounts[createAccountId(accountId)]
93
93
 
94
- if (!account) {
95
- return false
94
+ let removedFromConfig = false
95
+
96
+ if (account) {
97
+ delete config.accounts[account.account_id]
98
+
99
+ if (config.current === account.account_id) {
100
+ config.current = Object.keys(config.accounts)[0] ?? null
101
+ }
102
+
103
+ await this.saveConfig(config)
104
+ removedFromConfig = true
96
105
  }
97
106
 
98
- delete config.accounts[account.account_id]
107
+ const resolvedId = account?.account_id ?? createAccountId(accountId)
108
+ const accountDir = this.getAccountPaths(resolvedId).account_dir
109
+ const dirExisted = existsSync(accountDir)
99
110
 
100
- if (config.current === account.account_id) {
101
- config.current = Object.keys(config.accounts)[0] ?? null
111
+ if (dirExisted) {
112
+ await rm(accountDir, { recursive: true, force: true })
102
113
  }
103
114
 
104
- await this.saveConfig(config)
105
- await rm(this.getAccountPaths(account.account_id).account_dir, { recursive: true, force: true })
106
- return true
115
+ return removedFromConfig || dirExisted
107
116
  }
108
117
 
109
118
  async clearCredentials(): Promise<void> {
@@ -0,0 +1,217 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
2
+ import { existsSync, rmSync } from 'node:fs'
3
+ import { mkdir } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ const mockVerifyToken = mock(() =>
8
+ Promise.resolve({
9
+ verified_name: 'Test Business',
10
+ }),
11
+ )
12
+
13
+ mock.module('../client', () => ({
14
+ WhatsAppBotClient: class MockWhatsAppBotClient {
15
+ async login(_credentials?: { phoneNumberId: string; accessToken: string }) {
16
+ return this
17
+ }
18
+ verifyToken = mockVerifyToken
19
+ },
20
+ }))
21
+
22
+ import { WhatsAppBotCredentialManager } from '../credential-manager'
23
+ import { clearAction, listAction, removeAction, setAction, statusAction, useAction } from './auth'
24
+
25
+ describe('auth commands', () => {
26
+ let tempDir: string
27
+
28
+ beforeEach(async () => {
29
+ tempDir = join(tmpdir(), `whatsappbot-auth-test-${Date.now()}`)
30
+ await mkdir(tempDir, { recursive: true })
31
+ mockVerifyToken.mockClear()
32
+ })
33
+
34
+ afterEach(() => {
35
+ if (existsSync(tempDir)) {
36
+ rmSync(tempDir, { recursive: true })
37
+ }
38
+ })
39
+
40
+ describe('setAction', () => {
41
+ test('validates token and stores credentials', async () => {
42
+ const manager = new WhatsAppBotCredentialManager(tempDir)
43
+
44
+ const result = await setAction('12345678901', 'EAAtest-token', { _credManager: manager })
45
+
46
+ expect(result.success).toBe(true)
47
+ expect(result.phone_number_id).toBe('12345678901')
48
+ expect(result.account_name).toBe('Test Business')
49
+
50
+ const creds = await manager.getCredentials()
51
+ expect(creds?.phone_number_id).toBe('12345678901')
52
+ expect(creds?.access_token).toBe('EAAtest-token')
53
+ })
54
+
55
+ test('returns error when client throws', async () => {
56
+ mockVerifyToken.mockImplementationOnce(() => Promise.reject(new Error('Invalid token')))
57
+ const manager = new WhatsAppBotCredentialManager(tempDir)
58
+
59
+ const result = await setAction('12345678901', 'bad-token', { _credManager: manager })
60
+
61
+ expect(result.error).toBeDefined()
62
+ expect(result.error).toContain('Invalid token')
63
+ })
64
+ })
65
+
66
+ describe('statusAction', () => {
67
+ test('returns invalid status when no credentials set', async () => {
68
+ const manager = new WhatsAppBotCredentialManager(tempDir)
69
+
70
+ const result = await statusAction({ _credManager: manager })
71
+
72
+ expect(result.valid).toBe(false)
73
+ expect(result.error).toBeDefined()
74
+ })
75
+
76
+ test('returns valid status when credentials are set', async () => {
77
+ const manager = new WhatsAppBotCredentialManager(tempDir)
78
+ await manager.setCredentials({
79
+ phone_number_id: '12345678901',
80
+ account_name: 'Test Business',
81
+ access_token: 'EAAtest-token',
82
+ })
83
+
84
+ const result = await statusAction({ _credManager: manager })
85
+
86
+ expect(result.valid).toBe(true)
87
+ expect(result.phone_number_id).toBe('12345678901')
88
+ expect(result.account_name).toBe('Test Business')
89
+ })
90
+
91
+ test('returns invalid status when verifyToken fails', async () => {
92
+ mockVerifyToken.mockImplementationOnce(() => Promise.reject(new Error('Token expired')))
93
+ const manager = new WhatsAppBotCredentialManager(tempDir)
94
+ await manager.setCredentials({
95
+ phone_number_id: '12345678901',
96
+ account_name: 'Test Business',
97
+ access_token: 'expired-token',
98
+ })
99
+
100
+ const result = await statusAction({ _credManager: manager })
101
+
102
+ expect(result.valid).toBe(false)
103
+ expect(result.phone_number_id).toBe('12345678901')
104
+ })
105
+
106
+ test('returns error for unknown account', async () => {
107
+ const manager = new WhatsAppBotCredentialManager(tempDir)
108
+
109
+ const result = await statusAction({ account: 'nonexistent', _credManager: manager })
110
+
111
+ expect(result.valid).toBe(false)
112
+ expect(result.error).toContain('nonexistent')
113
+ })
114
+ })
115
+
116
+ describe('clearAction', () => {
117
+ test('removes all stored credentials', async () => {
118
+ const manager = new WhatsAppBotCredentialManager(tempDir)
119
+ await manager.setCredentials({
120
+ phone_number_id: '12345678901',
121
+ account_name: 'Test Business',
122
+ access_token: 'EAAtest-token',
123
+ })
124
+
125
+ const result = await clearAction({ _credManager: manager })
126
+
127
+ expect(result.success).toBe(true)
128
+ expect(await manager.getCredentials()).toBeNull()
129
+ })
130
+ })
131
+
132
+ describe('listAction', () => {
133
+ test('returns empty accounts when none set', async () => {
134
+ const manager = new WhatsAppBotCredentialManager(tempDir)
135
+
136
+ const result = await listAction({ _credManager: manager })
137
+
138
+ expect(result.accounts).toHaveLength(0)
139
+ })
140
+
141
+ test('returns all stored accounts', async () => {
142
+ const manager = new WhatsAppBotCredentialManager(tempDir)
143
+ await manager.setCredentials({
144
+ phone_number_id: '11111111111',
145
+ account_name: 'Account A',
146
+ access_token: 'token-a',
147
+ })
148
+ await manager.setCredentials({
149
+ phone_number_id: '22222222222',
150
+ account_name: 'Account B',
151
+ access_token: 'token-b',
152
+ })
153
+
154
+ const result = await listAction({ _credManager: manager })
155
+
156
+ expect(result.accounts).toHaveLength(2)
157
+ expect(result.accounts?.find((a) => a.phone_number_id === '11111111111')?.account_name).toBe('Account A')
158
+ expect(result.accounts?.find((a) => a.phone_number_id === '22222222222')?.is_current).toBe(true)
159
+ })
160
+ })
161
+
162
+ describe('useAction', () => {
163
+ test('switches current account', async () => {
164
+ const manager = new WhatsAppBotCredentialManager(tempDir)
165
+ await manager.setCredentials({
166
+ phone_number_id: '11111111111',
167
+ account_name: 'Account A',
168
+ access_token: 'token-a',
169
+ })
170
+ await manager.setCredentials({
171
+ phone_number_id: '22222222222',
172
+ account_name: 'Account B',
173
+ access_token: 'token-b',
174
+ })
175
+
176
+ const result = await useAction('11111111111', { _credManager: manager })
177
+
178
+ expect(result.success).toBe(true)
179
+ expect(result.phone_number_id).toBe('11111111111')
180
+ expect(result.account_name).toBe('Account A')
181
+ })
182
+
183
+ test('returns error for unknown account', async () => {
184
+ const manager = new WhatsAppBotCredentialManager(tempDir)
185
+
186
+ const result = await useAction('nonexistent', { _credManager: manager })
187
+
188
+ expect(result.error).toBeDefined()
189
+ expect(result.error).toContain('nonexistent')
190
+ })
191
+ })
192
+
193
+ describe('removeAction', () => {
194
+ test('removes a stored account', async () => {
195
+ const manager = new WhatsAppBotCredentialManager(tempDir)
196
+ await manager.setCredentials({
197
+ phone_number_id: '12345678901',
198
+ account_name: 'Test Business',
199
+ access_token: 'EAAtest-token',
200
+ })
201
+
202
+ const result = await removeAction('12345678901', { _credManager: manager })
203
+
204
+ expect(result.success).toBe(true)
205
+ expect(await manager.getCredentials('12345678901')).toBeNull()
206
+ })
207
+
208
+ test('returns error for unknown account', async () => {
209
+ const manager = new WhatsAppBotCredentialManager(tempDir)
210
+
211
+ const result = await removeAction('nonexistent', { _credManager: manager })
212
+
213
+ expect(result.error).toBeDefined()
214
+ expect(result.error).toContain('nonexistent')
215
+ })
216
+ })
217
+ })