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,127 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+ import type { Command } from 'commander'
5
+
6
+ const mockListChats = mock(() =>
7
+ Promise.resolve([
8
+ { id: 'thread-1', title: 'Alice', last_message: 'Hi' },
9
+ { id: 'thread-2', title: 'Bob', last_message: 'Hey' },
10
+ ]),
11
+ )
12
+
13
+ const mockSearchChats = mock(() =>
14
+ Promise.resolve([{ id: 'thread-1', title: 'Alice', last_message: 'Hi' }]),
15
+ )
16
+
17
+ const mockClient = {
18
+ listChats: mockListChats,
19
+ searchChats: mockSearchChats,
20
+ }
21
+
22
+ mock.module('./shared', () => ({
23
+ withInstagramClient: async (_options: unknown, fn: (client: typeof mockClient) => Promise<unknown>) => {
24
+ return fn(mockClient)
25
+ },
26
+ }))
27
+
28
+ import { chatCommand } from './chat'
29
+
30
+ function resetCommandState(cmd: Command): void {
31
+ for (const sub of cmd.commands) {
32
+ (sub as unknown as { _optionValues: Record<string, unknown>; _optionValueSources: Record<string, unknown> })._optionValues = {}
33
+ ;(sub as unknown as { _optionValues: Record<string, unknown>; _optionValueSources: Record<string, unknown> })._optionValueSources = {}
34
+ }
35
+ }
36
+
37
+ afterAll(() => {
38
+ mock.restore()
39
+ })
40
+
41
+ describe('chat commands', () => {
42
+ let consoleLogSpy: ReturnType<typeof mock>
43
+ let processExitSpy: ReturnType<typeof spyOn>
44
+
45
+ beforeEach(() => {
46
+ resetCommandState(chatCommand)
47
+
48
+ mockListChats.mockReset()
49
+ mockSearchChats.mockReset()
50
+
51
+ mockListChats.mockImplementation(() =>
52
+ Promise.resolve([
53
+ { id: 'thread-1', title: 'Alice', last_message: 'Hi' },
54
+ { id: 'thread-2', title: 'Bob', last_message: 'Hey' },
55
+ ]),
56
+ )
57
+ mockSearchChats.mockImplementation(() =>
58
+ Promise.resolve([{ id: 'thread-1', title: 'Alice', last_message: 'Hi' }]),
59
+ )
60
+
61
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
62
+ processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
63
+ throw new Error('process.exit called')
64
+ })
65
+ })
66
+
67
+ afterEach(() => {
68
+ console.log = originalConsoleLog
69
+ processExitSpy.mockRestore()
70
+ })
71
+
72
+ describe('list', () => {
73
+ test('lists DM conversations', async () => {
74
+ await expect(
75
+ chatCommand.parseAsync(['list'], { from: 'user' }),
76
+ ).rejects.toThrow('process.exit called')
77
+
78
+ expect(processExitSpy).toHaveBeenCalledWith(0)
79
+ expect(mockListChats).toHaveBeenCalledWith(20)
80
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
81
+ expect(output).toHaveLength(2)
82
+ expect(output[0].id).toBe('thread-1')
83
+ expect(output[1].id).toBe('thread-2')
84
+ })
85
+
86
+ test('passes custom limit', async () => {
87
+ await expect(
88
+ chatCommand.parseAsync(['list', '--limit', '5'], { from: 'user' }),
89
+ ).rejects.toThrow('process.exit called')
90
+
91
+ expect(mockListChats).toHaveBeenCalledWith(5)
92
+ })
93
+ })
94
+
95
+ describe('search', () => {
96
+ test('searches DM conversations by query', async () => {
97
+ await expect(
98
+ chatCommand.parseAsync(['search', 'Alice'], { from: 'user' }),
99
+ ).rejects.toThrow('process.exit called')
100
+
101
+ expect(processExitSpy).toHaveBeenCalledWith(0)
102
+ expect(mockSearchChats).toHaveBeenCalledWith('Alice', 20)
103
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
104
+ expect(output).toHaveLength(1)
105
+ expect(output[0].id).toBe('thread-1')
106
+ })
107
+
108
+ test('passes custom limit to search', async () => {
109
+ await expect(
110
+ chatCommand.parseAsync(['search', 'Alice', '--limit', '10'], { from: 'user' }),
111
+ ).rejects.toThrow('process.exit called')
112
+
113
+ expect(mockSearchChats).toHaveBeenCalledWith('Alice', 10)
114
+ })
115
+
116
+ test('returns empty array when no results', async () => {
117
+ mockSearchChats.mockImplementation(() => Promise.resolve([]))
118
+
119
+ await expect(
120
+ chatCommand.parseAsync(['search', 'nobody'], { from: 'user' }),
121
+ ).rejects.toThrow('process.exit called')
122
+
123
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
124
+ expect(output).toEqual([])
125
+ })
126
+ })
127
+ })
@@ -0,0 +1,178 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+ import type { Command } from 'commander'
5
+
6
+ const mockGetMessages = mock(() => Promise.resolve([{ id: 'msg-1', text: 'Hello' }]))
7
+ const mockSendMessage = mock(() => Promise.resolve({ id: 'msg-2', text: 'Sent' }))
8
+ const mockSendMessageToUser = mock(() => Promise.resolve({ id: 'msg-3', text: 'Sent to user' }))
9
+ const mockSearchMessages = mock(() => Promise.resolve([{ id: 'msg-4', text: 'Found' }]))
10
+ const mockSearchUsers = mock(() => Promise.resolve([{ pk: '999', username: 'targetuser' }]))
11
+
12
+ const mockClient = {
13
+ getMessages: mockGetMessages,
14
+ sendMessage: mockSendMessage,
15
+ sendMessageToUser: mockSendMessageToUser,
16
+ searchMessages: mockSearchMessages,
17
+ searchUsers: mockSearchUsers,
18
+ }
19
+
20
+ mock.module('./shared', () => ({
21
+ withInstagramClient: async (_options: unknown, fn: (client: typeof mockClient) => Promise<unknown>) => {
22
+ return fn(mockClient)
23
+ },
24
+ }))
25
+
26
+ import { messageCommand } from './message'
27
+
28
+ function resetCommandState(cmd: Command): void {
29
+ for (const sub of cmd.commands) {
30
+ (sub as unknown as { _optionValues: Record<string, unknown>; _optionValueSources: Record<string, unknown> })._optionValues = {}
31
+ ;(sub as unknown as { _optionValues: Record<string, unknown>; _optionValueSources: Record<string, unknown> })._optionValueSources = {}
32
+ }
33
+ }
34
+
35
+ afterAll(() => {
36
+ mock.restore()
37
+ })
38
+
39
+ describe('message commands', () => {
40
+ let consoleLogSpy: ReturnType<typeof mock>
41
+ let processExitSpy: ReturnType<typeof spyOn>
42
+
43
+ beforeEach(() => {
44
+ resetCommandState(messageCommand)
45
+
46
+ mockGetMessages.mockReset()
47
+ mockSendMessage.mockReset()
48
+ mockSendMessageToUser.mockReset()
49
+ mockSearchMessages.mockReset()
50
+ mockSearchUsers.mockReset()
51
+
52
+ mockGetMessages.mockImplementation(() => Promise.resolve([{ id: 'msg-1', text: 'Hello' }]))
53
+ mockSendMessage.mockImplementation(() => Promise.resolve({ id: 'msg-2', text: 'Sent' }))
54
+ mockSendMessageToUser.mockImplementation(() => Promise.resolve({ id: 'msg-3', text: 'Sent to user' }))
55
+ mockSearchMessages.mockImplementation(() => Promise.resolve([{ id: 'msg-4', text: 'Found' }]))
56
+ mockSearchUsers.mockImplementation(() => Promise.resolve([{ pk: '999', username: 'targetuser' }]))
57
+
58
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
59
+ processExitSpy = spyOn(process, 'exit').mockImplementation(() => {
60
+ throw new Error('process.exit called')
61
+ })
62
+ })
63
+
64
+ afterEach(() => {
65
+ console.log = originalConsoleLog
66
+ processExitSpy.mockRestore()
67
+ })
68
+
69
+ describe('list', () => {
70
+ test('lists messages from a thread', async () => {
71
+ await expect(
72
+ messageCommand.parseAsync(['list', 'thread-123'], { from: 'user' }),
73
+ ).rejects.toThrow('process.exit called')
74
+
75
+ expect(processExitSpy).toHaveBeenCalledWith(0)
76
+ expect(mockGetMessages).toHaveBeenCalledWith('thread-123', 25)
77
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
78
+ expect(output).toEqual([{ id: 'msg-1', text: 'Hello' }])
79
+ })
80
+
81
+ test('passes custom limit', async () => {
82
+ await expect(
83
+ messageCommand.parseAsync(['list', 'thread-123', '--limit', '10'], { from: 'user' }),
84
+ ).rejects.toThrow('process.exit called')
85
+
86
+ expect(mockGetMessages).toHaveBeenCalledWith('thread-123', 10)
87
+ })
88
+ })
89
+
90
+ describe('send', () => {
91
+ test('sends a message to a thread', async () => {
92
+ await expect(
93
+ messageCommand.parseAsync(['send', 'thread-123', 'Hello world'], { from: 'user' }),
94
+ ).rejects.toThrow('process.exit called')
95
+
96
+ expect(processExitSpy).toHaveBeenCalledWith(0)
97
+ expect(mockSendMessage).toHaveBeenCalledWith('thread-123', 'Hello world')
98
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
99
+ expect(output).toEqual({ id: 'msg-2', text: 'Sent' })
100
+ })
101
+ })
102
+
103
+ describe('send-to', () => {
104
+ test('sends a message to a user by username', async () => {
105
+ await expect(
106
+ messageCommand.parseAsync(['send-to', 'targetuser', 'Hi there'], { from: 'user' }),
107
+ ).rejects.toThrow('process.exit called')
108
+
109
+ expect(processExitSpy).toHaveBeenCalledWith(0)
110
+ expect(mockSearchUsers).toHaveBeenCalledWith('targetuser')
111
+ expect(mockSendMessageToUser).toHaveBeenCalledWith('999', 'Hi there')
112
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
113
+ expect(output).toEqual({ id: 'msg-3', text: 'Sent to user' })
114
+ })
115
+
116
+ test('strips @ prefix from username', async () => {
117
+ await expect(
118
+ messageCommand.parseAsync(['send-to', '@targetuser', 'Hi there'], { from: 'user' }),
119
+ ).rejects.toThrow('process.exit called')
120
+
121
+ expect(mockSearchUsers).toHaveBeenCalledWith('targetuser')
122
+ })
123
+
124
+ test('handles user not found error', async () => {
125
+ mockSearchUsers.mockImplementation(() => Promise.resolve([]))
126
+
127
+ try {
128
+ await messageCommand.parseAsync(['send-to', 'unknownuser', 'Hi'], { from: 'user' })
129
+ } catch {
130
+ /* empty */
131
+ }
132
+
133
+ expect(mockSendMessageToUser).not.toHaveBeenCalled()
134
+ })
135
+ })
136
+
137
+ describe('search', () => {
138
+ test('searches messages by query', async () => {
139
+ await expect(
140
+ messageCommand.parseAsync(['search', 'hello'], { from: 'user' }),
141
+ ).rejects.toThrow('process.exit called')
142
+
143
+ expect(processExitSpy).toHaveBeenCalledWith(0)
144
+ expect(mockSearchMessages).toHaveBeenCalledWith('hello', { threadId: undefined, limit: 20 })
145
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
146
+ expect(output).toEqual([{ id: 'msg-4', text: 'Found' }])
147
+ })
148
+
149
+ test('passes thread option to search', async () => {
150
+ await expect(
151
+ messageCommand.parseAsync(['search', 'hello', '--thread', 'thread-456'], { from: 'user' }),
152
+ ).rejects.toThrow('process.exit called')
153
+
154
+ expect(mockSearchMessages).toHaveBeenCalledWith('hello', { threadId: 'thread-456', limit: 20 })
155
+ })
156
+
157
+ test('passes limit option to search', async () => {
158
+ await expect(
159
+ messageCommand.parseAsync(['search', 'hello2', '--limit', '5'], { from: 'user' }),
160
+ ).rejects.toThrow('process.exit called')
161
+
162
+ expect(mockSearchMessages).toHaveBeenCalledWith('hello2', { threadId: undefined, limit: 5 })
163
+ })
164
+ })
165
+
166
+ describe('search-users', () => {
167
+ test('searches users by query', async () => {
168
+ await expect(
169
+ messageCommand.parseAsync(['search-users', 'target'], { from: 'user' }),
170
+ ).rejects.toThrow('process.exit called')
171
+
172
+ expect(processExitSpy).toHaveBeenCalledWith(0)
173
+ expect(mockSearchUsers).toHaveBeenCalledWith('target')
174
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
175
+ expect(output).toEqual([{ pk: '999', username: 'targetuser' }])
176
+ })
177
+ })
178
+ })
@@ -4,7 +4,7 @@ import type { Command as CommandType } from 'commander'
4
4
  import { Command } from 'commander'
5
5
 
6
6
  import pkg from '../../../package.json' with { type: 'json' }
7
- import { authCommand, chatCommand, messageCommand } from './commands/index'
7
+ import { authCommand, chatCommand, messageCommand, profileCommand } from './commands/index'
8
8
  import { ensureKakaoAuth } from './ensure-auth'
9
9
 
10
10
  function isAuthCommand(command: CommandType): boolean {
@@ -31,6 +31,7 @@ program.hook('preAction', async (_thisCommand, actionCommand) => {
31
31
  program.addCommand(authCommand)
32
32
  program.addCommand(chatCommand)
33
33
  program.addCommand(messageCommand)
34
+ program.addCommand(profileCommand)
34
35
 
35
36
  program.parse(process.argv)
36
37
 
@@ -146,6 +146,68 @@ describe('KakaoTalkClient', () => {
146
146
  client.close()
147
147
  })
148
148
 
149
+ test('falls back to LCHATLIST when login snapshot is empty (new device)', async () => {
150
+ // given — LOGINLIST returns empty chatDatas with eof:true (new device scenario)
151
+ const emptyLoginResult = {
152
+ chatDatas: [],
153
+ lastTokenId: makeLong(0),
154
+ lastChatId: makeLong(0),
155
+ eof: true,
156
+ }
157
+ mockLogin.mockResolvedValue(emptyLoginResult)
158
+
159
+ mockGetChatList.mockResolvedValueOnce({
160
+ body: {
161
+ chatDatas: [
162
+ {
163
+ c: 100,
164
+ t: 1,
165
+ k: ['Alice', 'Bob'],
166
+ a: 2,
167
+ n: 3,
168
+ o: 1700000000,
169
+ l: { authorId: 1, message: 'hi', sendAt: 1700000000 },
170
+ ll: makeLong(999),
171
+ },
172
+ {
173
+ c: 200,
174
+ t: 2,
175
+ k: ['Charlie'],
176
+ a: 1,
177
+ n: 0,
178
+ o: 1699999000,
179
+ l: null,
180
+ ll: makeLong(500),
181
+ },
182
+ ],
183
+ lastTokenId: makeLong(1),
184
+ lastChatId: makeLong(200),
185
+ eof: true,
186
+ },
187
+ })
188
+
189
+ // when — default chat list (no --all flag)
190
+ const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
191
+ const chats = await client.getChats()
192
+
193
+ // then — fetched via LCHATLIST despite eof:true and no --all
194
+ expect(chats).toHaveLength(2)
195
+ expect(mockGetChatList).toHaveBeenCalledTimes(1)
196
+ expect(chats[0].display_name).toBe('Alice, Bob')
197
+ expect(chats[1].display_name).toBe('Charlie')
198
+
199
+ client.close()
200
+ })
201
+
202
+ test('does not call LCHATLIST when login snapshot has chats', async () => {
203
+ const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
204
+ await client.getChats()
205
+
206
+ expect(mockGetChatList).not.toHaveBeenCalled()
207
+
208
+ client.close()
209
+ })
210
+
149
211
  test('paginates when all=true and not eof', async () => {
150
212
  const loginResult = {
151
213
  ...DEFAULT_LOGIN_RESULT,
@@ -384,6 +446,101 @@ describe('KakaoTalkClient', () => {
384
446
  })
385
447
  })
386
448
 
449
+ describe('getProfile', () => {
450
+ const mockFetch = mock(() => Promise.resolve(new Response()))
451
+
452
+ beforeEach(() => {
453
+ mockFetch.mockReset()
454
+ globalThis.fetch = mockFetch as unknown as typeof fetch
455
+ })
456
+
457
+ afterEach(() => {
458
+ mockFetch.mockReset()
459
+ })
460
+
461
+ function makeJsonResponse(data: unknown, status = 200): Response {
462
+ return new Response(JSON.stringify(data), {
463
+ status,
464
+ headers: { 'Content-Type': 'application/json' },
465
+ })
466
+ }
467
+
468
+ test('returns profile data on success', async () => {
469
+ mockFetch
470
+ .mockResolvedValueOnce(makeJsonResponse({
471
+ profile: {
472
+ nickName: 'Test User',
473
+ profileImageUrl: 'https://example.com/profile.jpg',
474
+ originalProfileImageUrl: 'https://example.com/original.jpg',
475
+ statusMessage: 'Hello world',
476
+ },
477
+ }))
478
+ .mockResolvedValueOnce(makeJsonResponse({ accountDisplayId: 'testuser123' }))
479
+
480
+ const client = await new KakaoTalkClient().login({ oauthToken: 'mytoken', userId: 'user42', deviceUuid: 'device1' })
481
+ const profile = await client.getProfile()
482
+
483
+ expect(profile.user_id).toBe('user42')
484
+ expect(profile.nickname).toBe('Test User')
485
+ expect(profile.profile_image_url).toBe('https://example.com/profile.jpg')
486
+ expect(profile.original_profile_image_url).toBe('https://example.com/original.jpg')
487
+ expect(profile.status_message).toBe('Hello world')
488
+ expect(profile.account_display_id).toBe('testuser123')
489
+
490
+ client.close()
491
+ })
492
+
493
+ test('throws not_authenticated when not logged in', async () => {
494
+ const client = new KakaoTalkClient()
495
+ try {
496
+ await client.getProfile()
497
+ expect.unreachable('should have thrown')
498
+ } catch (e) {
499
+ expect(e).toBeInstanceOf(KakaoTalkError)
500
+ expect((e as KakaoTalkError).code).toBe('not_authenticated')
501
+ }
502
+ })
503
+
504
+ test('throws profile_request_failed when profile HTTP request fails', async () => {
505
+ mockFetch
506
+ .mockResolvedValueOnce(makeJsonResponse({}, 401))
507
+ .mockResolvedValueOnce(makeJsonResponse({ accountDisplayId: null }))
508
+
509
+ const client = await new KakaoTalkClient().login({ oauthToken: 'mytoken', userId: 'user42', deviceUuid: 'device1' })
510
+ try {
511
+ await client.getProfile()
512
+ expect.unreachable('should have thrown')
513
+ } catch (e) {
514
+ expect(e).toBeInstanceOf(KakaoTalkError)
515
+ expect((e as KakaoTalkError).code).toBe('profile_request_failed')
516
+ }
517
+
518
+ client.close()
519
+ })
520
+
521
+ test('returns null account_display_id when more_settings request fails', async () => {
522
+ mockFetch
523
+ .mockResolvedValueOnce(makeJsonResponse({
524
+ profile: {
525
+ nickName: 'Test User',
526
+ profileImageUrl: null,
527
+ originalProfileImageUrl: null,
528
+ statusMessage: null,
529
+ },
530
+ }))
531
+ .mockResolvedValueOnce(makeJsonResponse({}, 500))
532
+
533
+ const client = await new KakaoTalkClient().login({ oauthToken: 'mytoken', userId: 'user42', deviceUuid: 'device1' })
534
+ const profile = await client.getProfile()
535
+
536
+ expect(profile.user_id).toBe('user42')
537
+ expect(profile.nickname).toBe('Test User')
538
+ expect(profile.account_display_id).toBeNull()
539
+
540
+ client.close()
541
+ })
542
+ })
543
+
387
544
  describe('session lifecycle', () => {
388
545
  test('lazy init: does not call login until first method call', async () => {
389
546
  const client = await new KakaoTalkClient().login({ oauthToken: 'token', userId: 'user1', deviceUuid: 'device1' })
@@ -2,9 +2,10 @@ import { Long } from 'bson'
2
2
 
3
3
  import { warn } from '@/shared/utils/stderr'
4
4
 
5
+ import { APP_VERSION, LANG, OS } from './protocol/config'
5
6
  import { LocoSession } from './protocol/session'
6
7
  import type { ChatListResponse, LoginListResponse } from './protocol/types'
7
- import type { KakaoChat, KakaoMessage, KakaoSendResult } from './types'
8
+ import type { KakaoChat, KakaoMessage, KakaoProfile, KakaoSendResult } from './types'
8
9
 
9
10
  export class KakaoTalkError extends Error {
10
11
  code: string
@@ -206,11 +207,22 @@ export class KakaoTalkClient {
206
207
 
207
208
  collectChats((loginResult.chatDatas ?? []) as ChatData[], allChats, seenChatIds)
208
209
 
209
- if (options?.all || options?.search) {
210
+ // Paginate via LCHATLIST when explicitly requested (--all / --search) OR when
211
+ // the login snapshot is empty. New device registrations often return an empty
212
+ // chatDatas with eof=true because the server has no prior sync state for the
213
+ // device — LCHATLIST fetches the canonical chat list regardless of device history.
214
+ const snapshotEmpty = allChats.length === 0
215
+ if (options?.all || options?.search || snapshotEmpty) {
210
216
  let cursor: ChatListResponse = loginResult
211
217
  let pages = 0
212
218
 
213
- while (!cursor.eof && pages < MAX_PAGES) {
219
+ while (pages < MAX_PAGES) {
220
+ // Trust eof only when the snapshot had data. When the snapshot was empty
221
+ // (new device), ignore eof for the first iteration so we always attempt
222
+ // at least one LCHATLIST call.
223
+ if (cursor.eof && !snapshotEmpty) break
224
+ if (cursor.eof && snapshotEmpty && pages > 0) break
225
+
214
226
  const lastTokenId = bsonToLong(cursor.lastTokenId)
215
227
  const lastChatId = bsonToLong(cursor.lastChatId)
216
228
 
@@ -317,6 +329,48 @@ export class KakaoTalkClient {
317
329
  })
318
330
  }
319
331
 
332
+ async getProfile(): Promise<KakaoProfile> {
333
+ this.ensureAuth()
334
+ try {
335
+ const headers = {
336
+ Authorization: `${this.oauthToken}-${this.deviceUuid}`,
337
+ A: `${OS}/${APP_VERSION}/${LANG}`,
338
+ 'User-Agent': `KT/${APP_VERSION} Md/macOS ${LANG}`,
339
+ Accept: '*/*',
340
+ 'Accept-Language': LANG,
341
+ }
342
+
343
+ const [profileRes, settingsRes] = await Promise.all([
344
+ fetch('https://katalk.kakao.com/mac/profile3/me.json', { headers }),
345
+ fetch('https://katalk.kakao.com/mac/account/more_settings.json?since=0&lang=ko', { headers }),
346
+ ])
347
+
348
+ if (!profileRes.ok) {
349
+ throw new KakaoTalkError(`Profile request failed: ${profileRes.status}`, 'profile_request_failed')
350
+ }
351
+
352
+ const profileData = await profileRes.json() as Record<string, unknown>
353
+ const profile = profileData.profile as Record<string, unknown> | undefined
354
+
355
+ let accountDisplayId: string | null = null
356
+ if (settingsRes.ok) {
357
+ const settingsData = await settingsRes.json() as Record<string, unknown>
358
+ accountDisplayId = (settingsData.accountDisplayId as string) || null
359
+ }
360
+
361
+ return {
362
+ user_id: this.userId!,
363
+ nickname: (profile?.nickName as string) || '',
364
+ profile_image_url: (profile?.profileImageUrl as string) || null,
365
+ original_profile_image_url: (profile?.originalProfileImageUrl as string) || null,
366
+ status_message: (profile?.statusMessage as string) || null,
367
+ account_display_id: accountDisplayId,
368
+ }
369
+ } catch (error) {
370
+ throw wrapError(error, 'get_profile_failed')
371
+ }
372
+ }
373
+
320
374
  close(): void {
321
375
  this.closed = true
322
376
  if (this.state) {