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,311 @@
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(() => Promise.resolve(null))
10
+ const mockListAccounts = mock(() => Promise.resolve([]))
11
+ const mockSetCurrent = mock(() => Promise.resolve(false))
12
+ const mockRemoveAccount = mock(() => Promise.resolve(false))
13
+ const mockGetAccountPaths = mock(() => ({ account_dir: '/tmp/test', auth_dir: '/tmp/test/auth' }))
14
+
15
+ mock.module('../credential-manager', () => ({
16
+ WhatsAppCredentialManager: class {
17
+ getAccount = mockGetAccount
18
+ listAccounts = mockListAccounts
19
+ setCurrent = mockSetCurrent
20
+ removeAccount = mockRemoveAccount
21
+ getAccountPaths = mockGetAccountPaths
22
+ ensureAccountPaths = mock(() => Promise.resolve({ account_dir: '/tmp/test', auth_dir: '/tmp/test/auth' }))
23
+ setAccount = mock(() => Promise.resolve())
24
+ },
25
+ }))
26
+
27
+ const mockConnect = mock(() => Promise.resolve())
28
+ const mockClose = mock(() => Promise.resolve())
29
+ const mockGetSocket = mock(() => null)
30
+ const mockLogin = mock(function (this: unknown) { return Promise.resolve(this) })
31
+
32
+ mock.module('../client', () => ({
33
+ WhatsAppClient: class {
34
+ login = mockLogin
35
+ connect = mockConnect
36
+ close = mockClose
37
+ getSocket = mockGetSocket
38
+ },
39
+ }))
40
+
41
+ import { authCommand } from './auth'
42
+
43
+ describe('auth commands', () => {
44
+ let consoleLogSpy: ReturnType<typeof mock>
45
+ let processExitSpy: ReturnType<typeof spyOn>
46
+
47
+ beforeEach(() => {
48
+ mockGetAccount.mockReset()
49
+ mockListAccounts.mockReset()
50
+ mockSetCurrent.mockReset()
51
+ mockRemoveAccount.mockReset()
52
+ mockGetAccountPaths.mockReset()
53
+ mockConnect.mockReset()
54
+ mockClose.mockReset()
55
+ mockGetSocket.mockReset()
56
+ mockLogin.mockReset()
57
+
58
+ mockGetAccount.mockImplementation(() => Promise.resolve(null))
59
+ mockListAccounts.mockImplementation(() => Promise.resolve([]))
60
+ mockSetCurrent.mockImplementation(() => Promise.resolve(false))
61
+ mockRemoveAccount.mockImplementation(() => Promise.resolve(false))
62
+ mockGetAccountPaths.mockImplementation(() => ({ account_dir: '/tmp/test', auth_dir: '/tmp/test/auth' }))
63
+ mockConnect.mockImplementation(() => Promise.resolve())
64
+ mockClose.mockImplementation(() => Promise.resolve())
65
+ mockGetSocket.mockImplementation(() => null)
66
+ mockLogin.mockImplementation(function (this: unknown) { return Promise.resolve(this) })
67
+
68
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
69
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
70
+ throw new Error(`process.exit(${_code})`)
71
+ })
72
+ processExitSpy.mockClear()
73
+ })
74
+
75
+ afterEach(() => {
76
+ console.log = originalConsoleLog
77
+ processExitSpy.mockRestore()
78
+ })
79
+
80
+ // list has no throwing tests — run first to avoid Commander state corruption
81
+ describe('list', () => {
82
+ test('outputs empty array when no accounts', async () => {
83
+ mockListAccounts.mockImplementation(() => Promise.resolve([]))
84
+
85
+ await authCommand.parseAsync(['list'], { from: 'user' })
86
+
87
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
88
+ expect(output).toEqual([])
89
+ })
90
+
91
+ test('outputs accounts list with is_current flag', async () => {
92
+ mockListAccounts.mockImplementation(() =>
93
+ Promise.resolve([
94
+ {
95
+ account_id: 'plus-12025551234',
96
+ phone_number: '+12025551234',
97
+ name: 'Alice',
98
+ created_at: '2024-01-01T00:00:00.000Z',
99
+ updated_at: '2024-01-01T00:00:00.000Z',
100
+ is_current: true,
101
+ },
102
+ {
103
+ account_id: 'plus-19995551234',
104
+ phone_number: '+19995551234',
105
+ name: null,
106
+ created_at: '2024-01-02T00:00:00.000Z',
107
+ updated_at: '2024-01-02T00:00:00.000Z',
108
+ is_current: false,
109
+ },
110
+ ]),
111
+ )
112
+
113
+ await authCommand.parseAsync(['list'], { from: 'user' })
114
+
115
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
116
+ expect(output).toHaveLength(2)
117
+ expect(output[0].account_id).toBe('plus-12025551234')
118
+ expect(output[0].is_current).toBe(true)
119
+ expect(output[1].account_id).toBe('plus-19995551234')
120
+ expect(output[1].is_current).toBe(false)
121
+ })
122
+ })
123
+
124
+ // use: success test first, then throwing test last
125
+ describe('use', () => {
126
+ test('switches to specified account and outputs success', async () => {
127
+ mockSetCurrent.mockImplementation(() => Promise.resolve(true))
128
+ mockGetAccount.mockImplementation(() =>
129
+ Promise.resolve({
130
+ account_id: 'plus-12025551234',
131
+ phone_number: '+12025551234',
132
+ name: 'Alice',
133
+ created_at: '2024-01-01T00:00:00.000Z',
134
+ updated_at: '2024-01-01T00:00:00.000Z',
135
+ }),
136
+ )
137
+
138
+ await authCommand.parseAsync(['use', 'plus-12025551234'], { from: 'user' })
139
+
140
+ expect(mockSetCurrent).toHaveBeenCalledWith('plus-12025551234')
141
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
142
+ expect(output.success).toBe(true)
143
+ expect(output.account_id).toBe('plus-12025551234')
144
+ })
145
+
146
+ test('outputs error and exits when account not found', async () => {
147
+ mockSetCurrent.mockImplementation(() => Promise.resolve(false))
148
+
149
+ await expect(
150
+ authCommand.parseAsync(['use', 'nonexistent'], { from: 'user' }),
151
+ ).rejects.toThrow('process.exit(1)')
152
+
153
+ expect(processExitSpy).toHaveBeenCalledWith(1)
154
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
155
+ expect(output.error).toContain('nonexistent')
156
+ })
157
+ })
158
+
159
+ // status: no-account tests first, --account tests last (avoids Commander option caching)
160
+ describe('status', () => {
161
+ test('outputs account info when account exists', async () => {
162
+ mockGetAccount.mockImplementation(() =>
163
+ Promise.resolve({
164
+ account_id: 'plus-12025551234',
165
+ phone_number: '+12025551234',
166
+ name: 'Test User',
167
+ created_at: '2024-01-01T00:00:00.000Z',
168
+ updated_at: '2024-01-01T00:00:00.000Z',
169
+ }),
170
+ )
171
+
172
+ await authCommand.parseAsync(['status'], { from: 'user' })
173
+
174
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
175
+ expect(output.account_id).toBe('plus-12025551234')
176
+ expect(output.phone_number).toBe('+12025551234')
177
+ expect(output.name).toBe('Test User')
178
+ })
179
+
180
+ test('outputs error and exits when no account configured', async () => {
181
+ mockGetAccount.mockImplementation(() => Promise.resolve(null))
182
+
183
+ await expect(
184
+ authCommand.parseAsync(['status'], { from: 'user' }),
185
+ ).rejects.toThrow('process.exit(1)')
186
+
187
+ expect(processExitSpy).toHaveBeenCalledWith(1)
188
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
189
+ expect(output.error).toBeDefined()
190
+ expect(output.error).toContain('No WhatsApp account configured')
191
+ })
192
+
193
+ test('passes --account option to getAccount', async () => {
194
+ mockGetAccount.mockImplementation((id?: string) => {
195
+ if (id === 'plus-19995551234') {
196
+ return Promise.resolve({
197
+ account_id: 'plus-19995551234',
198
+ phone_number: '+19995551234',
199
+ name: null,
200
+ created_at: '2024-01-01T00:00:00.000Z',
201
+ updated_at: '2024-01-01T00:00:00.000Z',
202
+ })
203
+ }
204
+ return Promise.resolve(null)
205
+ })
206
+
207
+ await authCommand.parseAsync(['status', '--account', 'plus-19995551234'], { from: 'user' })
208
+
209
+ expect(mockGetAccount).toHaveBeenCalledWith('plus-19995551234')
210
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
211
+ expect(output.account_id).toBe('plus-19995551234')
212
+ })
213
+
214
+ test('outputs error for specific missing account', async () => {
215
+ mockGetAccount.mockImplementation(() => Promise.resolve(null))
216
+
217
+ await expect(
218
+ authCommand.parseAsync(['status', '--account', 'missing-id'], { from: 'user' }),
219
+ ).rejects.toThrow('process.exit(1)')
220
+
221
+ expect(processExitSpy).toHaveBeenCalledWith(1)
222
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
223
+ expect(output.error).toContain('missing-id')
224
+ })
225
+ })
226
+
227
+ // logout: no-account tests first, --account test last (avoids Commander option caching)
228
+ describe('logout', () => {
229
+ test('removes current account and outputs success', async () => {
230
+ mockGetAccount.mockImplementation(() =>
231
+ Promise.resolve({
232
+ account_id: 'plus-12025551234',
233
+ phone_number: '+12025551234',
234
+ name: 'Alice',
235
+ created_at: '2024-01-01T00:00:00.000Z',
236
+ updated_at: '2024-01-01T00:00:00.000Z',
237
+ }),
238
+ )
239
+ mockRemoveAccount.mockImplementation(() => Promise.resolve(true))
240
+
241
+ await expect(
242
+ authCommand.parseAsync(['logout'], { from: 'user' }),
243
+ ).rejects.toThrow('process.exit(0)')
244
+
245
+ expect(mockRemoveAccount).toHaveBeenCalledWith('plus-12025551234')
246
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
247
+ expect(output.success).toBe(true)
248
+ expect(output.account_id).toBe('plus-12025551234')
249
+ expect(output.logged_out).toBe(true)
250
+ })
251
+
252
+ test('outputs error and exits when no account configured', async () => {
253
+ mockGetAccount.mockImplementation(() => Promise.resolve(null))
254
+
255
+ await expect(
256
+ authCommand.parseAsync(['logout'], { from: 'user' }),
257
+ ).rejects.toThrow('process.exit(1)')
258
+
259
+ expect(processExitSpy).toHaveBeenCalledWith(1)
260
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
261
+ expect(output.error).toBeDefined()
262
+ })
263
+
264
+ test('proceeds with local cleanup even when client connection fails', async () => {
265
+ mockGetAccount.mockImplementation(() =>
266
+ Promise.resolve({
267
+ account_id: 'plus-12025551234',
268
+ phone_number: '+12025551234',
269
+ name: 'Alice',
270
+ created_at: '2024-01-01T00:00:00.000Z',
271
+ updated_at: '2024-01-01T00:00:00.000Z',
272
+ }),
273
+ )
274
+ mockConnect.mockImplementation(() => Promise.reject(new Error('Connection failed')))
275
+ mockRemoveAccount.mockImplementation(() => Promise.resolve(true))
276
+
277
+ await expect(
278
+ authCommand.parseAsync(['logout'], { from: 'user' }),
279
+ ).rejects.toThrow('process.exit(0)')
280
+
281
+ expect(mockRemoveAccount).toHaveBeenCalledWith('plus-12025551234')
282
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
283
+ expect(output.success).toBe(true)
284
+ })
285
+
286
+ test('removes specific account with --account flag', async () => {
287
+ mockGetAccount.mockImplementation((id?: string) => {
288
+ if (id === 'plus-19995551234') {
289
+ return Promise.resolve({
290
+ account_id: 'plus-19995551234',
291
+ phone_number: '+19995551234',
292
+ name: null,
293
+ created_at: '2024-01-01T00:00:00.000Z',
294
+ updated_at: '2024-01-01T00:00:00.000Z',
295
+ })
296
+ }
297
+ return Promise.resolve(null)
298
+ })
299
+ mockRemoveAccount.mockImplementation(() => Promise.resolve(true))
300
+
301
+ await expect(
302
+ authCommand.parseAsync(['logout', '--account', 'plus-19995551234'], { from: 'user' }),
303
+ ).rejects.toThrow('process.exit(0)')
304
+
305
+ expect(mockRemoveAccount).toHaveBeenCalledWith('plus-19995551234')
306
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
307
+ expect(output.success).toBe(true)
308
+ expect(output.account_id).toBe('plus-19995551234')
309
+ })
310
+ })
311
+ })
@@ -1,3 +1,4 @@
1
+ import { rm } from 'node:fs/promises'
1
2
  import { Command } from 'commander'
2
3
  import { handleError } from '@/shared/utils/error-handler'
3
4
  import { formatOutput } from '@/shared/utils/output'
@@ -19,6 +20,9 @@ async function loginAction(options: LoginOptions): Promise<void> {
19
20
  try {
20
21
  const manager = new WhatsAppCredentialManager()
21
22
  const accountId = createAccountId(options.phone)
23
+ // Clear stale session files so Baileys starts fresh for pairing
24
+ const existingPaths = manager.getAccountPaths(accountId)
25
+ await rm(existingPaths.auth_dir, { recursive: true, force: true })
22
26
  const paths = await manager.ensureAccountPaths(accountId)
23
27
  const client = await new WhatsAppClient().login({ authDir: paths.auth_dir })
24
28
 
@@ -138,29 +142,29 @@ async function logoutAction(options: { account?: string; pretty?: boolean }): Pr
138
142
  const manager = new WhatsAppCredentialManager()
139
143
  const account = await manager.getAccount(options.account)
140
144
 
141
- if (!account) {
142
- console.log(formatOutput({
143
- error: options.account
144
- ? `WhatsApp account "${options.account}" not found.`
145
- : 'No WhatsApp account configured.',
146
- }, options.pretty))
145
+ if (!account && !options.account) {
146
+ console.log(formatOutput({ error: 'No WhatsApp account configured.' }, options.pretty))
147
147
  process.exit(1)
148
148
  }
149
149
 
150
- const paths = manager.getAccountPaths(account.account_id)
151
- try {
152
- const client = await new WhatsAppClient().login({ authDir: paths.auth_dir })
153
- await client.connect()
154
- if (client.getSocket()) {
155
- await client.getSocket()!.logout('Logged out via agent-whatsapp CLI')
150
+ const accountId = account?.account_id ?? options.account!
151
+
152
+ if (account) {
153
+ const paths = manager.getAccountPaths(account.account_id)
154
+ try {
155
+ const client = await new WhatsAppClient().login({ authDir: paths.auth_dir })
156
+ await client.connect()
157
+ if (client.getSocket()) {
158
+ await client.getSocket()!.logout('Logged out via agent-whatsapp CLI')
159
+ }
160
+ await client.close()
161
+ } catch {
162
+ // Server-side deregister failed — proceed with local cleanup
156
163
  }
157
- await client.close()
158
- } catch {
159
- // Server-side deregister failed — proceed with local cleanup
160
164
  }
161
165
 
162
- await manager.removeAccount(account.account_id)
163
- console.log(formatOutput({ success: true, account_id: account.account_id, logged_out: true }, options.pretty))
166
+ await manager.removeAccount(accountId)
167
+ console.log(formatOutput({ success: true, account_id: accountId, logged_out: true }, options.pretty))
164
168
  process.exit(0)
165
169
  } catch (error) {
166
170
  handleError(error as Error)
@@ -0,0 +1,198 @@
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 mockListChats = mock(() =>
30
+ Promise.resolve([
31
+ {
32
+ id: 'chat-1',
33
+ name: 'Alice',
34
+ lastMessage: 'Hello',
35
+ unreadCount: 2,
36
+ timestamp: 1000,
37
+ },
38
+ ]),
39
+ )
40
+
41
+ const mockSearchChats = mock(() =>
42
+ Promise.resolve([
43
+ {
44
+ id: 'chat-2',
45
+ name: 'Bob',
46
+ lastMessage: 'Hey',
47
+ unreadCount: 0,
48
+ timestamp: 2000,
49
+ },
50
+ ]),
51
+ )
52
+
53
+ const mockConnect = mock(() => Promise.resolve())
54
+ const mockClose = mock(() => Promise.resolve())
55
+
56
+ mock.module('../client', () => ({
57
+ WhatsAppClient: class {
58
+ login = mock(function (this: unknown) { return Promise.resolve(this) })
59
+ connect = mockConnect
60
+ close = mockClose
61
+ listChats = mockListChats
62
+ searchChats = mockSearchChats
63
+ },
64
+ }))
65
+
66
+ import { chatCommand } from './chat'
67
+
68
+ describe('chat commands', () => {
69
+ let consoleLogSpy: ReturnType<typeof mock>
70
+ let processExitSpy: ReturnType<typeof spyOn>
71
+
72
+ beforeEach(() => {
73
+ mockGetAccount.mockReset()
74
+ mockEnsureAccountPaths.mockReset()
75
+ mockListChats.mockReset()
76
+ mockSearchChats.mockReset()
77
+ mockConnect.mockReset()
78
+ mockClose.mockReset()
79
+
80
+ mockGetAccount.mockImplementation(() =>
81
+ Promise.resolve({
82
+ account_id: 'plus-12025551234',
83
+ phone_number: '+12025551234',
84
+ name: 'Test User',
85
+ created_at: '2024-01-01T00:00:00.000Z',
86
+ updated_at: '2024-01-01T00:00:00.000Z',
87
+ }),
88
+ )
89
+ mockEnsureAccountPaths.mockImplementation(() =>
90
+ Promise.resolve({ account_dir: '/tmp/test', auth_dir: '/tmp/test/auth' }),
91
+ )
92
+ mockListChats.mockImplementation(() =>
93
+ Promise.resolve([
94
+ {
95
+ id: 'chat-1',
96
+ name: 'Alice',
97
+ lastMessage: 'Hello',
98
+ unreadCount: 2,
99
+ timestamp: 1000,
100
+ },
101
+ ]),
102
+ )
103
+ mockSearchChats.mockImplementation(() =>
104
+ Promise.resolve([
105
+ {
106
+ id: 'chat-2',
107
+ name: 'Bob',
108
+ lastMessage: 'Hey',
109
+ unreadCount: 0,
110
+ timestamp: 2000,
111
+ },
112
+ ]),
113
+ )
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('lists chats with default limit', async () => {
131
+ await expect(
132
+ chatCommand.parseAsync(['list'], { from: 'user' }),
133
+ ).rejects.toThrow('process.exit(0)')
134
+
135
+ expect(mockListChats).toHaveBeenCalledWith(20)
136
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
137
+ expect(output).toHaveLength(1)
138
+ expect(output[0].id).toBe('chat-1')
139
+ expect(output[0].name).toBe('Alice')
140
+ })
141
+
142
+ test('respects --limit option', async () => {
143
+ await expect(
144
+ chatCommand.parseAsync(['list', '--limit', '5'], { from: 'user' }),
145
+ ).rejects.toThrow('process.exit(0)')
146
+
147
+ expect(mockListChats).toHaveBeenCalledWith(5)
148
+ })
149
+
150
+ test('passes account option to credential manager', async () => {
151
+ await expect(
152
+ chatCommand.parseAsync(['list', '--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
+ chatCommand.parseAsync(['list'], { from: 'user' }),
163
+ ).rejects.toThrow('process.exit(1)')
164
+
165
+ expect(processExitSpy).toHaveBeenCalledWith(1)
166
+ })
167
+ })
168
+
169
+ describe('search', () => {
170
+ test('searches chats by query', async () => {
171
+ await expect(
172
+ chatCommand.parseAsync(['search', 'Bob'], { from: 'user' }),
173
+ ).rejects.toThrow('process.exit(0)')
174
+
175
+ expect(mockSearchChats).toHaveBeenCalledWith('Bob', 20)
176
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
177
+ expect(output).toHaveLength(1)
178
+ expect(output[0].id).toBe('chat-2')
179
+ expect(output[0].name).toBe('Bob')
180
+ })
181
+
182
+ test('respects --limit option', async () => {
183
+ await expect(
184
+ chatCommand.parseAsync(['search', 'Alice', '--limit', '3'], { from: 'user' }),
185
+ ).rejects.toThrow('process.exit(0)')
186
+
187
+ expect(mockSearchChats).toHaveBeenCalledWith('Alice', 3)
188
+ })
189
+
190
+ test('passes account option to credential manager', async () => {
191
+ await expect(
192
+ chatCommand.parseAsync(['search', 'test', '--account', 'my-account'], { from: 'user' }),
193
+ ).rejects.toThrow('process.exit(0)')
194
+
195
+ expect(mockGetAccount).toHaveBeenCalledWith('my-account')
196
+ })
197
+ })
198
+ })