agent-messenger 2.3.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 (202) 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 +8 -5
  6. package/bun.lock +70 -110
  7. package/bunfig.toml +3 -0
  8. package/dist/package.json +11 -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/client.d.ts.map +1 -1
  38. package/dist/src/platforms/line/client.js +9 -36
  39. package/dist/src/platforms/line/client.js.map +1 -1
  40. package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
  41. package/dist/src/platforms/line/commands/auth.js +32 -20
  42. package/dist/src/platforms/line/commands/auth.js.map +1 -1
  43. package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
  44. package/dist/src/platforms/teams/commands/reaction.js +2 -0
  45. package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
  46. package/dist/src/platforms/wechatbot/cli.d.ts +5 -0
  47. package/dist/src/platforms/wechatbot/cli.d.ts.map +1 -0
  48. package/dist/src/platforms/wechatbot/cli.js +18 -0
  49. package/dist/src/platforms/wechatbot/cli.js.map +1 -0
  50. package/dist/src/platforms/wechatbot/client.d.ts +36 -0
  51. package/dist/src/platforms/wechatbot/client.d.ts.map +1 -0
  52. package/dist/src/platforms/wechatbot/client.js +208 -0
  53. package/dist/src/platforms/wechatbot/client.js.map +1 -0
  54. package/dist/src/platforms/wechatbot/commands/auth.d.ts +28 -0
  55. package/dist/src/platforms/wechatbot/commands/auth.d.ts.map +1 -0
  56. package/dist/src/platforms/wechatbot/commands/auth.js +164 -0
  57. package/dist/src/platforms/wechatbot/commands/auth.js.map +1 -0
  58. package/dist/src/platforms/wechatbot/commands/index.d.ts +5 -0
  59. package/dist/src/platforms/wechatbot/commands/index.d.ts.map +1 -0
  60. package/dist/src/platforms/wechatbot/commands/index.js +5 -0
  61. package/dist/src/platforms/wechatbot/commands/index.js.map +1 -0
  62. package/dist/src/platforms/wechatbot/commands/message.d.ts +18 -0
  63. package/dist/src/platforms/wechatbot/commands/message.d.ts.map +1 -0
  64. package/dist/src/platforms/wechatbot/commands/message.js +80 -0
  65. package/dist/src/platforms/wechatbot/commands/message.js.map +1 -0
  66. package/dist/src/platforms/wechatbot/commands/shared.d.ts +9 -0
  67. package/dist/src/platforms/wechatbot/commands/shared.d.ts.map +1 -0
  68. package/dist/src/platforms/wechatbot/commands/shared.js +13 -0
  69. package/dist/src/platforms/wechatbot/commands/shared.js.map +1 -0
  70. package/dist/src/platforms/wechatbot/commands/template.d.ts +19 -0
  71. package/dist/src/platforms/wechatbot/commands/template.d.ts.map +1 -0
  72. package/dist/src/platforms/wechatbot/commands/template.js +76 -0
  73. package/dist/src/platforms/wechatbot/commands/template.js.map +1 -0
  74. package/dist/src/platforms/wechatbot/commands/user.d.ts +20 -0
  75. package/dist/src/platforms/wechatbot/commands/user.d.ts.map +1 -0
  76. package/dist/src/platforms/wechatbot/commands/user.js +53 -0
  77. package/dist/src/platforms/wechatbot/commands/user.js.map +1 -0
  78. package/dist/src/platforms/wechatbot/credential-manager.d.ts +17 -0
  79. package/dist/src/platforms/wechatbot/credential-manager.d.ts.map +1 -0
  80. package/dist/src/platforms/wechatbot/credential-manager.js +121 -0
  81. package/dist/src/platforms/wechatbot/credential-manager.js.map +1 -0
  82. package/dist/src/platforms/wechatbot/index.d.ts +5 -0
  83. package/dist/src/platforms/wechatbot/index.d.ts.map +1 -0
  84. package/dist/src/platforms/wechatbot/index.js +4 -0
  85. package/dist/src/platforms/wechatbot/index.js.map +1 -0
  86. package/dist/src/platforms/wechatbot/types.d.ts +94 -0
  87. package/dist/src/platforms/wechatbot/types.d.ts.map +1 -0
  88. package/dist/src/platforms/wechatbot/types.js +54 -0
  89. package/dist/src/platforms/wechatbot/types.js.map +1 -0
  90. package/dist/src/platforms/whatsapp/client.d.ts +1 -0
  91. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  92. package/dist/src/platforms/whatsapp/client.js +27 -13
  93. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  94. package/dist/src/platforms/whatsapp/commands/auth.d.ts.map +1 -1
  95. package/dist/src/platforms/whatsapp/commands/auth.js +21 -18
  96. package/dist/src/platforms/whatsapp/commands/auth.js.map +1 -1
  97. package/dist/src/platforms/whatsapp/credential-manager.d.ts.map +1 -1
  98. package/dist/src/platforms/whatsapp/credential-manager.js +14 -8
  99. package/dist/src/platforms/whatsapp/credential-manager.js.map +1 -1
  100. package/docs/content/docs/agent-skills.mdx +4 -4
  101. package/docs/content/docs/cli/channeltalk.mdx +1 -1
  102. package/docs/content/docs/cli/channeltalkbot.mdx +1 -1
  103. package/docs/content/docs/cli/discord.mdx +1 -1
  104. package/docs/content/docs/cli/discordbot.mdx +1 -1
  105. package/docs/content/docs/cli/instagram.mdx +1 -1
  106. package/docs/content/docs/cli/kakaotalk.mdx +1 -1
  107. package/docs/content/docs/cli/line.mdx +1 -1
  108. package/docs/content/docs/cli/meta.json +1 -0
  109. package/docs/content/docs/cli/slack.mdx +1 -1
  110. package/docs/content/docs/cli/slackbot.mdx +1 -1
  111. package/docs/content/docs/cli/teams.mdx +1 -1
  112. package/docs/content/docs/cli/webex.mdx +1 -1
  113. package/docs/content/docs/cli/wechatbot.mdx +179 -0
  114. package/docs/content/docs/cli/whatsapp.mdx +1 -1
  115. package/docs/content/docs/cli/whatsappbot.mdx +1 -1
  116. package/docs/content/docs/sdk/meta.json +1 -1
  117. package/docs/content/docs/sdk/wechatbot.mdx +282 -0
  118. package/docs/content/docs/tui.mdx +1 -1
  119. package/docs/src/app/page.tsx +5 -5
  120. package/package.json +11 -3
  121. package/skills/agent-channeltalk/SKILL.md +1 -1
  122. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  123. package/skills/agent-discord/SKILL.md +1 -1
  124. package/skills/agent-discordbot/SKILL.md +1 -1
  125. package/skills/agent-instagram/SKILL.md +1 -1
  126. package/skills/agent-kakaotalk/SKILL.md +24 -1
  127. package/skills/agent-line/SKILL.md +7 -11
  128. package/skills/agent-line/references/authentication.md +13 -4
  129. package/skills/agent-slack/SKILL.md +1 -1
  130. package/skills/agent-slackbot/SKILL.md +1 -1
  131. package/skills/agent-teams/SKILL.md +1 -1
  132. package/skills/agent-telegram/SKILL.md +1 -1
  133. package/skills/agent-webex/SKILL.md +1 -1
  134. package/skills/agent-wechatbot/SKILL.md +385 -0
  135. package/skills/agent-whatsapp/SKILL.md +12 -1
  136. package/skills/agent-whatsappbot/SKILL.md +1 -1
  137. package/src/platforms/discord/credential-manager.test.ts +18 -1
  138. package/src/platforms/discordbot/client.ts +2 -2
  139. package/src/platforms/instagram/commands/auth.test.ts +216 -0
  140. package/src/platforms/instagram/commands/chat.test.ts +127 -0
  141. package/src/platforms/instagram/commands/message.test.ts +178 -0
  142. package/src/platforms/kakaotalk/cli.ts +2 -1
  143. package/src/platforms/kakaotalk/client.test.ts +157 -0
  144. package/src/platforms/kakaotalk/client.ts +57 -3
  145. package/src/platforms/kakaotalk/commands/auth.test.ts +299 -0
  146. package/src/platforms/kakaotalk/commands/chat.test.ts +97 -0
  147. package/src/platforms/kakaotalk/commands/index.ts +1 -0
  148. package/src/platforms/kakaotalk/commands/message.test.ts +113 -0
  149. package/src/platforms/kakaotalk/commands/profile.test.ts +84 -0
  150. package/src/platforms/kakaotalk/commands/profile.ts +21 -0
  151. package/src/platforms/kakaotalk/index.test.ts +5 -0
  152. package/src/platforms/kakaotalk/index.ts +2 -0
  153. package/src/platforms/kakaotalk/protocol/session.ts +2 -0
  154. package/src/platforms/kakaotalk/types.ts +18 -0
  155. package/src/platforms/line/client.ts +14 -39
  156. package/src/platforms/line/commands/auth.test.ts +141 -0
  157. package/src/platforms/line/commands/auth.ts +28 -19
  158. package/src/platforms/line/commands/chat.test.ts +110 -0
  159. package/src/platforms/line/commands/friend.test.ts +98 -0
  160. package/src/platforms/line/commands/message.test.ts +119 -0
  161. package/src/platforms/line/commands/profile.test.ts +85 -0
  162. package/src/platforms/slackbot/commands/channel.test.ts +139 -0
  163. package/src/platforms/slackbot/commands/message.test.ts +226 -0
  164. package/src/platforms/slackbot/commands/reaction.test.ts +90 -0
  165. package/src/platforms/slackbot/commands/user.test.ts +143 -0
  166. package/src/platforms/teams/commands/reaction.test.ts +45 -61
  167. package/src/platforms/teams/commands/reaction.ts +2 -0
  168. package/src/platforms/telegram/commands/chat.test.ts +125 -0
  169. package/src/platforms/telegram/commands/message.test.ts +92 -0
  170. package/src/platforms/webex/commands/member.test.ts +65 -58
  171. package/src/platforms/webex/commands/message.test.ts +78 -121
  172. package/src/platforms/webex/commands/snapshot.test.ts +59 -46
  173. package/src/platforms/webex/commands/space.test.ts +49 -48
  174. package/src/platforms/wechatbot/cli.ts +24 -0
  175. package/src/platforms/wechatbot/client.test.ts +497 -0
  176. package/src/platforms/wechatbot/client.ts +268 -0
  177. package/src/platforms/wechatbot/commands/auth.test.ts +211 -0
  178. package/src/platforms/wechatbot/commands/auth.ts +203 -0
  179. package/src/platforms/wechatbot/commands/index.ts +4 -0
  180. package/src/platforms/wechatbot/commands/message.test.ts +155 -0
  181. package/src/platforms/wechatbot/commands/message.ts +104 -0
  182. package/src/platforms/wechatbot/commands/shared.ts +22 -0
  183. package/src/platforms/wechatbot/commands/template.test.ts +199 -0
  184. package/src/platforms/wechatbot/commands/template.ts +102 -0
  185. package/src/platforms/wechatbot/commands/user.test.ts +165 -0
  186. package/src/platforms/wechatbot/commands/user.ts +75 -0
  187. package/src/platforms/wechatbot/credential-manager.test.ts +255 -0
  188. package/src/platforms/wechatbot/credential-manager.ts +148 -0
  189. package/src/platforms/wechatbot/index.test.ts +49 -0
  190. package/src/platforms/wechatbot/index.ts +19 -0
  191. package/src/platforms/wechatbot/types.test.ts +223 -0
  192. package/src/platforms/wechatbot/types.ts +107 -0
  193. package/src/platforms/whatsapp/client.ts +24 -13
  194. package/src/platforms/whatsapp/commands/auth.test.ts +311 -0
  195. package/src/platforms/whatsapp/commands/auth.ts +21 -17
  196. package/src/platforms/whatsapp/commands/chat.test.ts +198 -0
  197. package/src/platforms/whatsapp/commands/message.test.ts +231 -0
  198. package/src/platforms/whatsapp/credential-manager.test.ts +20 -0
  199. package/src/platforms/whatsapp/credential-manager.ts +17 -8
  200. package/src/platforms/whatsappbot/commands/auth.test.ts +217 -0
  201. package/src/platforms/whatsappbot/commands/message.test.ts +198 -0
  202. package/src/platforms/whatsappbot/commands/template.test.ts +112 -0
@@ -0,0 +1,299 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ const mockLoad = mock(() =>
6
+ Promise.resolve({ current_account: null, accounts: {} }),
7
+ )
8
+ const mockGetAccount = mock(() => Promise.resolve(null))
9
+ const mockListAccounts = mock(() => Promise.resolve([]))
10
+ const mockSetAccount = mock(() => Promise.resolve())
11
+ const mockSetCurrentAccount = mock(() => Promise.resolve())
12
+ const mockRemoveAccount = mock(() => Promise.resolve())
13
+ const mockLoadPendingLogin = mock(() => Promise.resolve(null))
14
+
15
+ mock.module('../credential-manager', () => ({
16
+ CredentialManager: class {
17
+ load = mockLoad
18
+ getAccount = mockGetAccount
19
+ listAccounts = mockListAccounts
20
+ setAccount = mockSetAccount
21
+ setCurrentAccount = mockSetCurrentAccount
22
+ removeAccount = mockRemoveAccount
23
+ loadPendingLogin = mockLoadPendingLogin
24
+ },
25
+ KakaoCredentialManager: class {
26
+ load = mockLoad
27
+ getAccount = mockGetAccount
28
+ listAccounts = mockListAccounts
29
+ setAccount = mockSetAccount
30
+ setCurrentAccount = mockSetCurrentAccount
31
+ removeAccount = mockRemoveAccount
32
+ loadPendingLogin = mockLoadPendingLogin
33
+ },
34
+ }))
35
+
36
+ import { authCommand } from './auth'
37
+
38
+ describe('auth commands', () => {
39
+ let consoleLogSpy: ReturnType<typeof mock>
40
+ let processExitSpy: ReturnType<typeof spyOn>
41
+
42
+ beforeEach(() => {
43
+ mockLoad.mockReset()
44
+ mockGetAccount.mockReset()
45
+ mockListAccounts.mockReset()
46
+ mockSetAccount.mockReset()
47
+ mockSetCurrentAccount.mockReset()
48
+ mockRemoveAccount.mockReset()
49
+ mockLoadPendingLogin.mockReset()
50
+
51
+ mockLoad.mockImplementation(() =>
52
+ Promise.resolve({ current_account: null, accounts: {} }),
53
+ )
54
+ mockGetAccount.mockImplementation(() => Promise.resolve(null))
55
+ mockListAccounts.mockImplementation(() => Promise.resolve([]))
56
+ mockSetAccount.mockImplementation(() => Promise.resolve())
57
+ mockSetCurrentAccount.mockImplementation(() => Promise.resolve())
58
+ mockRemoveAccount.mockImplementation(() => Promise.resolve())
59
+ mockLoadPendingLogin.mockImplementation(() => Promise.resolve(null))
60
+
61
+ consoleLogSpy = mock((..._args: unknown[]) => {})
62
+ console.log = consoleLogSpy
63
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
64
+ throw new Error(`process.exit(${_code})`)
65
+ })
66
+ })
67
+
68
+ afterEach(() => {
69
+ console.log = originalConsoleLog
70
+ processExitSpy?.mockRestore()
71
+ })
72
+
73
+ describe('list', () => {
74
+ test('outputs empty array when no accounts', async () => {
75
+ mockListAccounts.mockImplementation(() => Promise.resolve([]))
76
+
77
+ await authCommand.parseAsync(['list'], { from: 'user' })
78
+
79
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
80
+ expect(output).toEqual([])
81
+ })
82
+
83
+ test('outputs accounts list with is_current flag', async () => {
84
+ mockListAccounts.mockImplementation(() =>
85
+ Promise.resolve([
86
+ {
87
+ account_id: 'user-1',
88
+ user_id: 'user-1',
89
+ device_type: 'tablet',
90
+ created_at: '2024-01-01T00:00:00.000Z',
91
+ updated_at: '2024-01-01T00:00:00.000Z',
92
+ is_current: true,
93
+ oauth_token: 'token-1',
94
+ refresh_token: 'refresh-1',
95
+ device_uuid: 'uuid-1',
96
+ },
97
+ {
98
+ account_id: 'user-2',
99
+ user_id: 'user-2',
100
+ device_type: 'pc',
101
+ created_at: '2024-01-02T00:00:00.000Z',
102
+ updated_at: '2024-01-02T00:00:00.000Z',
103
+ is_current: false,
104
+ oauth_token: 'token-2',
105
+ refresh_token: 'refresh-2',
106
+ device_uuid: 'uuid-2',
107
+ },
108
+ ]),
109
+ )
110
+
111
+ await authCommand.parseAsync(['list'], { from: 'user' })
112
+
113
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
114
+ expect(output).toHaveLength(2)
115
+ expect(output[0].account_id).toBe('user-1')
116
+ expect(output[0].is_current).toBe(true)
117
+ expect(output[1].account_id).toBe('user-2')
118
+ expect(output[1].is_current).toBe(false)
119
+ })
120
+ })
121
+
122
+ describe('use', () => {
123
+ test('switches to specified account', async () => {
124
+ mockGetAccount.mockImplementation(() =>
125
+ Promise.resolve({
126
+ account_id: 'user-1',
127
+ user_id: 'user-1',
128
+ device_type: 'tablet',
129
+ oauth_token: 'token-1',
130
+ refresh_token: 'refresh-1',
131
+ device_uuid: 'uuid-1',
132
+ created_at: '2024-01-01T00:00:00.000Z',
133
+ updated_at: '2024-01-01T00:00:00.000Z',
134
+ }),
135
+ )
136
+
137
+ await authCommand.parseAsync(['use', 'user-1'], { from: 'user' })
138
+
139
+ expect(mockSetCurrentAccount).toHaveBeenCalledWith('user-1')
140
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
141
+ expect(output.success).toBe(true)
142
+ expect(output.account_id).toBe('user-1')
143
+ })
144
+
145
+ test('outputs error and exits when account not found', async () => {
146
+ mockGetAccount.mockImplementation(() => Promise.resolve(null))
147
+
148
+ await expect(
149
+ authCommand.parseAsync(['use', 'nonexistent'], { from: 'user' }),
150
+ ).rejects.toThrow('process.exit')
151
+
152
+ expect(processExitSpy).toHaveBeenCalledWith(1)
153
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
154
+ expect(output.error).toContain('nonexistent')
155
+ })
156
+ })
157
+
158
+ describe('status', () => {
159
+ test('outputs error and exits when no account configured', async () => {
160
+ mockGetAccount.mockImplementation(() => Promise.resolve(null))
161
+
162
+ await expect(
163
+ authCommand.parseAsync(['status'], { from: 'user' }),
164
+ ).rejects.toThrow('process.exit')
165
+
166
+ expect(processExitSpy).toHaveBeenCalledWith(1)
167
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
168
+ expect(output.error).toBeDefined()
169
+ })
170
+
171
+ test('outputs account info when account exists', async () => {
172
+ mockGetAccount.mockImplementation(() =>
173
+ Promise.resolve({
174
+ account_id: 'user-1',
175
+ user_id: 'user-1',
176
+ device_type: 'tablet',
177
+ oauth_token: 'token-1',
178
+ refresh_token: 'refresh-1',
179
+ device_uuid: 'uuid-1',
180
+ created_at: '2024-01-01T00:00:00.000Z',
181
+ updated_at: '2024-01-01T00:00:00.000Z',
182
+ }),
183
+ )
184
+
185
+ await authCommand.parseAsync(['status'], { from: 'user' })
186
+
187
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
188
+ expect(output.account_id).toBe('user-1')
189
+ expect(output.user_id).toBe('user-1')
190
+ expect(output.device_type).toBe('tablet')
191
+ expect(output.has_refresh_token).toBe(true)
192
+ expect(output.has_device_uuid).toBe(true)
193
+ })
194
+
195
+ test('outputs status for specific --account', async () => {
196
+ mockGetAccount.mockImplementation((id?: string) => {
197
+ if (id === 'user-2') {
198
+ return Promise.resolve({
199
+ account_id: 'user-2',
200
+ user_id: 'user-2',
201
+ device_type: 'pc',
202
+ oauth_token: 'token-2',
203
+ refresh_token: 'refresh-2',
204
+ device_uuid: 'uuid-2',
205
+ created_at: '2024-01-02T00:00:00.000Z',
206
+ updated_at: '2024-01-02T00:00:00.000Z',
207
+ })
208
+ }
209
+ return Promise.resolve(null)
210
+ })
211
+
212
+ await authCommand.parseAsync(['status', '--account', 'user-2'], { from: 'user' })
213
+
214
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
215
+ expect(output.account_id).toBe('user-2')
216
+ expect(output.device_type).toBe('pc')
217
+ })
218
+ })
219
+
220
+ describe('logout', () => {
221
+ test('removes current account and outputs success', async () => {
222
+ mockLoad.mockImplementation(() =>
223
+ Promise.resolve({
224
+ current_account: 'user-1',
225
+ accounts: {
226
+ 'user-1': {
227
+ account_id: 'user-1',
228
+ user_id: 'user-1',
229
+ device_type: 'tablet',
230
+ oauth_token: 'token-1',
231
+ refresh_token: 'refresh-1',
232
+ device_uuid: 'uuid-1',
233
+ created_at: '2024-01-01T00:00:00.000Z',
234
+ updated_at: '2024-01-01T00:00:00.000Z',
235
+ },
236
+ },
237
+ }),
238
+ )
239
+
240
+ await authCommand.parseAsync(['logout'], { from: 'user' })
241
+
242
+ expect(mockRemoveAccount).toHaveBeenCalledWith('user-1')
243
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
244
+ expect(output.success).toBe(true)
245
+ expect(output.removed).toBe('user-1')
246
+ })
247
+
248
+ test('removes specific account with --account flag', async () => {
249
+ mockLoad.mockImplementation(() =>
250
+ Promise.resolve({
251
+ current_account: 'user-1',
252
+ accounts: {
253
+ 'user-1': {
254
+ account_id: 'user-1',
255
+ user_id: 'user-1',
256
+ device_type: 'tablet',
257
+ oauth_token: 'token-1',
258
+ refresh_token: 'refresh-1',
259
+ device_uuid: 'uuid-1',
260
+ created_at: '2024-01-01T00:00:00.000Z',
261
+ updated_at: '2024-01-01T00:00:00.000Z',
262
+ },
263
+ 'user-2': {
264
+ account_id: 'user-2',
265
+ user_id: 'user-2',
266
+ device_type: 'pc',
267
+ oauth_token: 'token-2',
268
+ refresh_token: 'refresh-2',
269
+ device_uuid: 'uuid-2',
270
+ created_at: '2024-01-02T00:00:00.000Z',
271
+ updated_at: '2024-01-02T00:00:00.000Z',
272
+ },
273
+ },
274
+ }),
275
+ )
276
+
277
+ await authCommand.parseAsync(['logout', '--account', 'user-2'], { from: 'user' })
278
+
279
+ expect(mockRemoveAccount).toHaveBeenCalledWith('user-2')
280
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
281
+ expect(output.success).toBe(true)
282
+ expect(output.removed).toBe('user-2')
283
+ })
284
+
285
+ test('outputs error and exits when no account configured', async () => {
286
+ mockLoad.mockImplementation(() =>
287
+ Promise.resolve({ current_account: null, accounts: {} }),
288
+ )
289
+
290
+ await expect(
291
+ authCommand.parseAsync(['logout'], { from: 'user' }),
292
+ ).rejects.toThrow('process.exit')
293
+
294
+ expect(processExitSpy).toHaveBeenCalledWith(1)
295
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
296
+ expect(output.error).toBeDefined()
297
+ })
298
+ })
299
+ })
@@ -0,0 +1,97 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ const mockWithKakaoClient = mock(
6
+ async (_options: unknown, fn: (client: unknown) => Promise<unknown>) => {
7
+ return fn(mockClient)
8
+ },
9
+ )
10
+
11
+ const mockGetChats = mock(() =>
12
+ Promise.resolve([
13
+ { chat_id: 'chat-1', name: 'General', type: 'group', member_count: 5 },
14
+ { chat_id: 'chat-2', name: 'Direct', type: 'direct', member_count: 2 },
15
+ ]),
16
+ )
17
+
18
+ const mockClient = {
19
+ getChats: mockGetChats,
20
+ }
21
+
22
+ mock.module('./shared', () => ({
23
+ withKakaoClient: mockWithKakaoClient,
24
+ }))
25
+
26
+ import { chatCommand } from './chat'
27
+
28
+ describe('chat commands', () => {
29
+ let consoleLogSpy: ReturnType<typeof mock>
30
+
31
+ beforeEach(() => {
32
+ mockWithKakaoClient.mockReset()
33
+ mockGetChats.mockReset()
34
+
35
+ mockWithKakaoClient.mockImplementation(
36
+ async (_options: unknown, fn: (client: unknown) => Promise<unknown>) => {
37
+ return fn(mockClient)
38
+ },
39
+ )
40
+ mockGetChats.mockImplementation(() =>
41
+ Promise.resolve([
42
+ { chat_id: 'chat-1', name: 'General', type: 'group', member_count: 5 },
43
+ { chat_id: 'chat-2', name: 'Direct', type: 'direct', member_count: 2 },
44
+ ]),
45
+ )
46
+
47
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
48
+ })
49
+
50
+ afterEach(() => {
51
+ console.log = originalConsoleLog
52
+ })
53
+
54
+ describe('list', () => {
55
+ test('lists chat rooms', async () => {
56
+ await chatCommand.parseAsync(['list'], { from: 'user' })
57
+
58
+ expect(mockGetChats).toHaveBeenCalled()
59
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
60
+ expect(output).toHaveLength(2)
61
+ expect(output[0].chat_id).toBe('chat-1')
62
+ expect(output[0].name).toBe('General')
63
+ })
64
+
65
+ test('passes --search option to getChats', async () => {
66
+ await chatCommand.parseAsync(['list', '--search', 'General'], { from: 'user' })
67
+
68
+ const call = mockGetChats.mock.calls[0][0] as { all?: boolean; search?: string }
69
+ expect(call.search).toBe('General')
70
+ })
71
+
72
+ test('passes --all flag to getChats', async () => {
73
+ await chatCommand.parseAsync(['list', '--all'], { from: 'user' })
74
+
75
+ const call = mockGetChats.mock.calls[0][0] as { all?: boolean; search?: string }
76
+ expect(call.all).toBe(true)
77
+ })
78
+
79
+ test('passes account option to withKakaoClient', async () => {
80
+ await chatCommand.parseAsync(['list', '--account', 'my-account'], { from: 'user' })
81
+
82
+ expect(mockWithKakaoClient).toHaveBeenCalledWith(
83
+ expect.objectContaining({ account: 'my-account' }),
84
+ expect.any(Function),
85
+ )
86
+ })
87
+
88
+ test('outputs empty array when no chats', async () => {
89
+ mockGetChats.mockImplementation(() => Promise.resolve([]))
90
+
91
+ await chatCommand.parseAsync(['list'], { from: 'user' })
92
+
93
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
94
+ expect(output).toEqual([])
95
+ })
96
+ })
97
+ })
@@ -1,3 +1,4 @@
1
1
  export { authCommand } from './auth'
2
2
  export { chatCommand } from './chat'
3
3
  export { messageCommand } from './message'
4
+ export { profileCommand } from './profile'
@@ -0,0 +1,113 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ const mockWithKakaoClient = mock(
6
+ async (_options: unknown, fn: (client: unknown) => Promise<unknown>) => {
7
+ return fn(mockClient)
8
+ },
9
+ )
10
+
11
+ const mockGetMessages = mock(() =>
12
+ Promise.resolve([
13
+ { log_id: '1', message: 'Hello', sender_id: 'user-1', created_at: 1000 },
14
+ ]),
15
+ )
16
+
17
+ const mockSendMessage = mock(() =>
18
+ Promise.resolve({ log_id: '2', message: 'Hi there', created_at: 2000 }),
19
+ )
20
+
21
+ const mockClient = {
22
+ getMessages: mockGetMessages,
23
+ sendMessage: mockSendMessage,
24
+ }
25
+
26
+ mock.module('./shared', () => ({
27
+ withKakaoClient: mockWithKakaoClient,
28
+ }))
29
+
30
+ import { messageCommand } from './message'
31
+
32
+ describe('message commands', () => {
33
+ let consoleLogSpy: ReturnType<typeof mock>
34
+
35
+ beforeEach(() => {
36
+ mockWithKakaoClient.mockReset()
37
+ mockGetMessages.mockReset()
38
+ mockSendMessage.mockReset()
39
+
40
+ mockWithKakaoClient.mockImplementation(
41
+ async (_options: unknown, fn: (client: unknown) => Promise<unknown>) => {
42
+ return fn(mockClient)
43
+ },
44
+ )
45
+ mockGetMessages.mockImplementation(() =>
46
+ Promise.resolve([
47
+ { log_id: '1', message: 'Hello', sender_id: 'user-1', created_at: 1000 },
48
+ ]),
49
+ )
50
+ mockSendMessage.mockImplementation(() =>
51
+ Promise.resolve({ log_id: '2', message: 'Hi there', created_at: 2000 }),
52
+ )
53
+
54
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
55
+ })
56
+
57
+ afterEach(() => {
58
+ console.log = originalConsoleLog
59
+ })
60
+
61
+ describe('list', () => {
62
+ test('fetches messages for a chat room with default count', async () => {
63
+ await messageCommand.parseAsync(['list', 'chat-123', '--count', '20'], { from: 'user' })
64
+
65
+ expect(mockGetMessages).toHaveBeenCalledWith('chat-123', { count: 20, from: undefined })
66
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
67
+ expect(output).toHaveLength(1)
68
+ expect(output[0].log_id).toBe('1')
69
+ expect(output[0].message).toBe('Hello')
70
+ })
71
+
72
+ test('respects --count option', async () => {
73
+ await messageCommand.parseAsync(['list', 'chat-123', '--count', '5'], { from: 'user' })
74
+
75
+ expect(mockGetMessages).toHaveBeenCalledWith('chat-123', { count: 5, from: undefined })
76
+ })
77
+
78
+ test('respects --from option', async () => {
79
+ await messageCommand.parseAsync(['list', 'chat-123', '--count', '20', '--from', '999'], { from: 'user' })
80
+
81
+ expect(mockGetMessages).toHaveBeenCalledWith('chat-123', { count: 20, from: '999' })
82
+ })
83
+
84
+ test('passes account option to withKakaoClient', async () => {
85
+ await messageCommand.parseAsync(['list', 'chat-123', '--count', '20', '--account', 'my-account'], { from: 'user' })
86
+
87
+ expect(mockWithKakaoClient).toHaveBeenCalledWith(
88
+ expect.objectContaining({ account: 'my-account' }),
89
+ expect.any(Function),
90
+ )
91
+ })
92
+ })
93
+
94
+ describe('send', () => {
95
+ test('sends a message to a chat room', async () => {
96
+ await messageCommand.parseAsync(['send', 'chat-123', 'Hello world'], { from: 'user' })
97
+
98
+ expect(mockSendMessage).toHaveBeenCalledWith('chat-123', 'Hello world')
99
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
100
+ expect(output.log_id).toBe('2')
101
+ expect(output.message).toBe('Hi there')
102
+ })
103
+
104
+ test('passes account option to withKakaoClient', async () => {
105
+ await messageCommand.parseAsync(['send', 'chat-123', 'Hi', '--account', 'my-account'], { from: 'user' })
106
+
107
+ expect(mockWithKakaoClient).toHaveBeenCalledWith(
108
+ expect.objectContaining({ account: 'my-account' }),
109
+ expect.any(Function),
110
+ )
111
+ })
112
+ })
113
+ })
@@ -0,0 +1,84 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ const mockWithKakaoClient = mock(
6
+ async (_options: unknown, fn: (client: unknown) => Promise<unknown>) => {
7
+ return fn(mockClient)
8
+ },
9
+ )
10
+
11
+ const mockGetProfile = mock(() =>
12
+ Promise.resolve({
13
+ user_id: 'user-1',
14
+ nickname: 'Test User',
15
+ profile_image_url: 'https://example.com/avatar.jpg',
16
+ }),
17
+ )
18
+
19
+ const mockClient = {
20
+ getProfile: mockGetProfile,
21
+ }
22
+
23
+ mock.module('./shared', () => ({
24
+ withKakaoClient: mockWithKakaoClient,
25
+ }))
26
+
27
+ import { profileCommand } from './profile'
28
+
29
+ describe('profile command', () => {
30
+ let consoleLogSpy: ReturnType<typeof mock>
31
+
32
+ beforeEach(() => {
33
+ mockWithKakaoClient.mockReset()
34
+ mockGetProfile.mockReset()
35
+
36
+ mockWithKakaoClient.mockImplementation(
37
+ async (_options: unknown, fn: (client: unknown) => Promise<unknown>) => {
38
+ return fn(mockClient)
39
+ },
40
+ )
41
+ mockGetProfile.mockImplementation(() =>
42
+ Promise.resolve({
43
+ user_id: 'user-1',
44
+ nickname: 'Test User',
45
+ profile_image_url: 'https://example.com/avatar.jpg',
46
+ }),
47
+ )
48
+
49
+ consoleLogSpy = mock((..._args: unknown[]) => {})
50
+ console.log = consoleLogSpy
51
+ })
52
+
53
+ afterEach(() => {
54
+ console.log = originalConsoleLog
55
+ })
56
+
57
+ test('outputs profile information', async () => {
58
+ await profileCommand.parseAsync([], { from: 'user' })
59
+
60
+ expect(mockGetProfile).toHaveBeenCalled()
61
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
62
+ expect(output.user_id).toBe('user-1')
63
+ expect(output.nickname).toBe('Test User')
64
+ expect(output.profile_image_url).toBe('https://example.com/avatar.jpg')
65
+ })
66
+
67
+ test('passes account option to withKakaoClient', async () => {
68
+ await profileCommand.parseAsync(['--account', 'my-account'], { from: 'user' })
69
+
70
+ expect(mockWithKakaoClient).toHaveBeenCalledWith(
71
+ expect.objectContaining({ account: 'my-account' }),
72
+ expect.any(Function),
73
+ )
74
+ })
75
+
76
+ test('outputs profile with pretty flag', async () => {
77
+ await profileCommand.parseAsync(['--pretty'], { from: 'user' })
78
+
79
+ expect(mockGetProfile).toHaveBeenCalled()
80
+ const rawOutput = consoleLogSpy.mock.calls[0][0]
81
+ const output = JSON.parse(rawOutput)
82
+ expect(output.user_id).toBe('user-1')
83
+ })
84
+ })
@@ -0,0 +1,21 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { handleError } from '@/shared/utils/error-handler'
4
+ import { formatOutput } from '@/shared/utils/output'
5
+
6
+ import { withKakaoClient } from './shared'
7
+
8
+ async function profileAction(options: { account?: string; pretty?: boolean }): Promise<void> {
9
+ try {
10
+ const profile = await withKakaoClient(options, (client) => client.getProfile())
11
+ console.log(formatOutput(profile, options.pretty))
12
+ } catch (error) {
13
+ handleError(error as Error)
14
+ }
15
+ }
16
+
17
+ export const profileCommand = new Command('profile')
18
+ .description('Show your KakaoTalk profile')
19
+ .option('--account <id>', 'Use a specific KakaoTalk account')
20
+ .option('--pretty', 'Pretty print JSON output')
21
+ .action(profileAction)
@@ -12,6 +12,7 @@ import {
12
12
  KakaoTalkListener,
13
13
  KakaoTalkPushMemberEventSchema,
14
14
  KakaoTalkPushMessageEventSchema,
15
+ KakaoProfileSchema,
15
16
  KakaoTalkPushReadEventSchema,
16
17
  } from '@/platforms/kakaotalk/index'
17
18
 
@@ -66,3 +67,7 @@ test('KakaoTalkPushMemberEventSchema is exported from barrel', () => {
66
67
  test('KakaoTalkPushReadEventSchema is exported from barrel', () => {
67
68
  expect(typeof KakaoTalkPushReadEventSchema.parse).toBe('function')
68
69
  })
70
+
71
+ test('KakaoProfileSchema is exported from barrel', () => {
72
+ expect(typeof KakaoProfileSchema.parse).toBe('function')
73
+ })
@@ -8,6 +8,7 @@ export type {
8
8
  KakaoConfig,
9
9
  KakaoDeviceType,
10
10
  KakaoMessage,
11
+ KakaoProfile,
11
12
  KakaoSendResult,
12
13
  KakaoTalkListenerEventMap,
13
14
  KakaoTalkPushEvent,
@@ -21,6 +22,7 @@ export {
21
22
  KakaoChatSchema,
22
23
  KakaoConfigSchema,
23
24
  KakaoMessageSchema,
25
+ KakaoProfileSchema,
24
26
  KakaoSendResultSchema,
25
27
  KakaoTalkPushMemberEventSchema,
26
28
  KakaoTalkPushMessageEventSchema,
@@ -7,6 +7,7 @@ import {
7
7
  CHECKIN_HOST,
8
8
  CHECKIN_PORT,
9
9
  COUNTRY_ISO,
10
+ DTYPE,
10
11
  LANG,
11
12
  MCCMNC,
12
13
  PING_INTERVAL_MS,
@@ -39,6 +40,7 @@ export class LocoSession {
39
40
  prtVer: PROTOCOL_VERSION,
40
41
  os: 'mac',
41
42
  lang: LANG,
43
+ dtype: DTYPE,
42
44
  duuid: deviceUuid,
43
45
  oauthToken,
44
46
  ntype: 0,
@@ -125,6 +125,24 @@ export const KakaoSendResultSchema = z.object({
125
125
  sent_at: z.number(),
126
126
  })
127
127
 
128
+ export interface KakaoProfile {
129
+ user_id: string
130
+ nickname: string
131
+ profile_image_url: string | null
132
+ original_profile_image_url: string | null
133
+ status_message: string | null
134
+ account_display_id: string | null
135
+ }
136
+
137
+ export const KakaoProfileSchema = z.object({
138
+ user_id: z.string(),
139
+ nickname: z.string(),
140
+ profile_image_url: z.string().nullable(),
141
+ original_profile_image_url: z.string().nullable(),
142
+ status_message: z.string().nullable(),
143
+ account_display_id: z.string().nullable(),
144
+ })
145
+
128
146
  export const KakaoAccountCredentialsSchema = z.object({
129
147
  account_id: z.string(),
130
148
  oauth_token: z.string(),