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,141 @@
1
+ import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ import { LineCredentialManager } from '../credential-manager'
6
+ import { authCommand } from './auth'
7
+
8
+ let getAccountSpy: ReturnType<typeof spyOn>
9
+ let listAccountsSpy: ReturnType<typeof spyOn>
10
+ let setCurrentAccountSpy: ReturnType<typeof spyOn>
11
+ let removeAccountSpy: ReturnType<typeof spyOn>
12
+ let clearAllSpy: ReturnType<typeof spyOn>
13
+ let consoleLogSpy: ReturnType<typeof mock>
14
+
15
+ beforeEach(() => {
16
+ getAccountSpy = spyOn(LineCredentialManager.prototype, 'getAccount').mockResolvedValue({
17
+ account_id: 'u123',
18
+ auth_token: 'token-abc',
19
+ device: 'ANDROIDSECONDARY',
20
+ display_name: 'Test User',
21
+ created_at: '2024-01-01T00:00:00Z',
22
+ updated_at: '2024-01-01T00:00:00Z',
23
+ })
24
+
25
+ listAccountsSpy = spyOn(LineCredentialManager.prototype, 'listAccounts').mockResolvedValue([
26
+ {
27
+ account_id: 'u123',
28
+ display_name: 'Test User',
29
+ device: 'ANDROIDSECONDARY',
30
+ is_current: true,
31
+ created_at: '2024-01-01T00:00:00Z',
32
+ },
33
+ {
34
+ account_id: 'u456',
35
+ display_name: 'Other User',
36
+ device: 'DESKTOPMAC',
37
+ is_current: false,
38
+ created_at: '2024-01-02T00:00:00Z',
39
+ },
40
+ ])
41
+
42
+ setCurrentAccountSpy = spyOn(LineCredentialManager.prototype, 'setCurrentAccount').mockResolvedValue(undefined)
43
+ removeAccountSpy = spyOn(LineCredentialManager.prototype, 'removeAccount').mockResolvedValue(undefined)
44
+ clearAllSpy = spyOn(LineCredentialManager.prototype, 'clearAll').mockResolvedValue(undefined)
45
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
46
+ })
47
+
48
+ afterEach(() => {
49
+ getAccountSpy?.mockRestore()
50
+ listAccountsSpy?.mockRestore()
51
+ setCurrentAccountSpy?.mockRestore()
52
+ removeAccountSpy?.mockRestore()
53
+ clearAllSpy?.mockRestore()
54
+ console.log = originalConsoleLog
55
+ })
56
+
57
+ test('status: outputs account info when account exists', async () => {
58
+ // when
59
+ await authCommand.parseAsync(['node', 'auth', 'status'])
60
+
61
+ // then
62
+ expect(getAccountSpy).toHaveBeenCalledTimes(1)
63
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
64
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
65
+ expect(output.account_id).toBe('u123')
66
+ expect(output.display_name).toBe('Test User')
67
+ expect(output.device).toBe('ANDROIDSECONDARY')
68
+ })
69
+
70
+ test('status: outputs error when no account configured', async () => {
71
+ // given
72
+ getAccountSpy.mockResolvedValue(null)
73
+
74
+ // when
75
+ await authCommand.parseAsync(['node', 'auth', 'status'])
76
+
77
+ // then
78
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
79
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
80
+ expect(output.error).toBe('No LINE account configured')
81
+ })
82
+
83
+ test('list: outputs all accounts', async () => {
84
+ // when
85
+ await authCommand.parseAsync(['node', 'auth', 'list'])
86
+
87
+ // then
88
+ expect(listAccountsSpy).toHaveBeenCalledTimes(1)
89
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
90
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
91
+ expect(output).toHaveLength(2)
92
+ expect(output[0].account_id).toBe('u123')
93
+ expect(output[1].account_id).toBe('u456')
94
+ })
95
+
96
+ test('list: outputs empty array when no accounts', async () => {
97
+ // given
98
+ listAccountsSpy.mockResolvedValue([])
99
+
100
+ // when
101
+ await authCommand.parseAsync(['node', 'auth', 'list'])
102
+
103
+ // then
104
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
105
+ expect(output).toHaveLength(0)
106
+ })
107
+
108
+ test('use: sets current account and outputs result', async () => {
109
+ // when
110
+ await authCommand.parseAsync(['node', 'auth', 'use', 'u456'])
111
+
112
+ // then
113
+ expect(setCurrentAccountSpy).toHaveBeenCalledWith('u456')
114
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
115
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
116
+ expect(output.current_account).toBe('u456')
117
+ })
118
+
119
+ test('logout: removes specific account when account-id provided', async () => {
120
+ // when
121
+ await authCommand.parseAsync(['node', 'auth', 'logout', 'u123'])
122
+
123
+ // then
124
+ expect(removeAccountSpy).toHaveBeenCalledWith('u123')
125
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
126
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
127
+ expect(output.success).toBe(true)
128
+ expect(output.message).toContain('u123')
129
+ })
130
+
131
+ test('logout: clears all accounts when no account-id provided', async () => {
132
+ // when
133
+ await authCommand.parseAsync(['node', 'auth', 'logout'])
134
+
135
+ // then
136
+ expect(clearAllSpy).toHaveBeenCalledTimes(1)
137
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
138
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
139
+ expect(output.success).toBe(true)
140
+ expect(output.message).toBe('Logged out')
141
+ })
@@ -13,7 +13,6 @@ import { info } from '@/shared/utils/stderr'
13
13
  import { LineClient } from '../client'
14
14
  import { LineCredentialManager } from '../credential-manager'
15
15
  import type { LineDevice } from '../types'
16
- import { LINE_NEXT_ACTIONS } from '../types'
17
16
 
18
17
  function isInteractiveSession(): boolean {
19
18
  return Boolean(process.stdin.isTTY && process.stdout.isTTY)
@@ -23,7 +22,7 @@ function getDefaultDevice(): LineDevice {
23
22
  return 'ANDROIDSECONDARY'
24
23
  }
25
24
 
26
- async function openQRInBrowser(url: string): Promise<void> {
25
+ async function createQRHtmlFile(url: string): Promise<string> {
27
26
  const svgString = await QRCode.toString(url, { type: 'svg', margin: 2 })
28
27
  const html = `<!DOCTYPE html>
29
28
  <html><head><meta charset="utf-8"><title>LINE QR Login</title>
@@ -35,18 +34,20 @@ svg{width:280px;height:280px}</style></head>
35
34
 
36
35
  const htmlPath = join(tmpdir(), `line-qr-${Date.now()}.html`)
37
36
  writeFileSync(htmlPath, html)
37
+ setTimeout(() => { try { unlinkSync(htmlPath) } catch {} }, 300_000).unref()
38
+ return htmlPath
39
+ }
38
40
 
41
+ function openInBrowser(filePath: string): void {
39
42
  try {
40
43
  if (process.platform === 'darwin') {
41
- execSync(`open "${htmlPath}"`, { stdio: 'ignore' })
44
+ execSync(`open "${filePath}"`, { stdio: 'ignore' })
42
45
  } else if (process.platform === 'win32') {
43
- execSync(`start "" "${htmlPath}"`, { stdio: 'ignore' })
46
+ execSync(`start "" "${filePath}"`, { stdio: 'ignore' })
44
47
  } else {
45
- execSync(`xdg-open "${htmlPath}"`, { stdio: 'ignore' })
48
+ execSync(`xdg-open "${filePath}"`, { stdio: 'ignore' })
46
49
  }
47
50
  } catch {}
48
-
49
- setTimeout(() => { try { unlinkSync(htmlPath) } catch {} }, 30_000)
50
51
  }
51
52
 
52
53
  async function loginAction(options: {
@@ -94,21 +95,29 @@ async function loginAction(options: {
94
95
  })
95
96
  console.log(formatOutput(result, options.pretty))
96
97
  } else {
97
- if (!interactive) {
98
- console.log(formatOutput(LINE_NEXT_ACTIONS.run_interactive, options.pretty))
99
- return
100
- }
101
-
102
98
  const result = await client.loginWithQR({
103
99
  device,
104
100
  onQRUrl: async (url) => {
105
- await openQRInBrowser(url).catch(() => {})
106
- try {
107
- const qrAscii = await QRCode.toString(url, { type: 'terminal', small: true })
108
- info('\nScan this QR code with the LINE mobile app:\n')
109
- info(qrAscii)
110
- } catch {
111
- info(`\nOpen the QR code in the browser window, or scan this URL:\n${url}\n`)
101
+ const htmlPath = await createQRHtmlFile(url).catch(() => null)
102
+ if (htmlPath) openInBrowser(htmlPath)
103
+
104
+ if (interactive) {
105
+ try {
106
+ const qrAscii = await QRCode.toString(url, { type: 'terminal', small: true })
107
+ info('\nScan this QR code with the LINE mobile app:\n')
108
+ info(qrAscii)
109
+ } catch {
110
+ info(`\nOpen the QR code in the browser window, or scan this URL:\n${url}\n`)
111
+ }
112
+ } else {
113
+ console.log(formatOutput({
114
+ next_action: 'scan_qr',
115
+ qr_url: url,
116
+ qr_html_path: htmlPath,
117
+ message: htmlPath
118
+ ? 'QR code opened in browser. Scan with LINE mobile app to complete login.'
119
+ : 'QR code generated. Open qr_url to scan with LINE mobile app.',
120
+ }, options.pretty))
112
121
  }
113
122
  },
114
123
  onPincode: (pin) => {
@@ -0,0 +1,110 @@
1
+ import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ import { LineClient } from '../client'
6
+ import { chatCommand } from './chat'
7
+
8
+ let loginSpy: ReturnType<typeof spyOn>
9
+ let getChatsSpy: ReturnType<typeof spyOn>
10
+ let closeSpy: ReturnType<typeof spyOn>
11
+ let consoleLogSpy: ReturnType<typeof mock>
12
+
13
+ beforeEach(() => {
14
+ loginSpy = spyOn(LineClient.prototype, 'login').mockImplementation(async function (this: LineClient) {
15
+ return this
16
+ })
17
+
18
+ getChatsSpy = spyOn(LineClient.prototype, 'getChats').mockResolvedValue([
19
+ {
20
+ chat_id: 'c111',
21
+ type: 'user',
22
+ display_name: 'Alice',
23
+ member_count: 2,
24
+ },
25
+ {
26
+ chat_id: 'c222',
27
+ type: 'group',
28
+ display_name: 'Team Chat',
29
+ member_count: 10,
30
+ },
31
+ {
32
+ chat_id: 'c333',
33
+ type: 'room',
34
+ display_name: 'Project Room',
35
+ member_count: 5,
36
+ },
37
+ ])
38
+
39
+ closeSpy = spyOn(LineClient.prototype, 'close').mockImplementation(() => {})
40
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
41
+ })
42
+
43
+ afterEach(() => {
44
+ loginSpy?.mockRestore()
45
+ getChatsSpy?.mockRestore()
46
+ closeSpy?.mockRestore()
47
+ console.log = originalConsoleLog
48
+ })
49
+
50
+ test('list: fetches and outputs chats', async () => {
51
+ // when
52
+ await chatCommand.parseAsync(['node', 'chat', 'list'])
53
+
54
+ // then
55
+ expect(loginSpy).toHaveBeenCalledTimes(1)
56
+ expect(getChatsSpy).toHaveBeenCalledWith({ limit: 50 })
57
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
58
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
59
+ expect(output).toHaveLength(3)
60
+ expect(output[0].chat_id).toBe('c111')
61
+ expect(output[1].chat_id).toBe('c222')
62
+ })
63
+
64
+ test('list: uses default limit of 50 when no limit provided', async () => {
65
+ // when
66
+ await chatCommand.parseAsync(['node', 'chat', 'list'])
67
+
68
+ // then
69
+ expect(getChatsSpy).toHaveBeenCalledWith({ limit: 50 })
70
+ })
71
+
72
+ test('list: uses custom limit when --limit option provided', async () => {
73
+ // when
74
+ await chatCommand.parseAsync(['node', 'chat', 'list', '--limit', '10'])
75
+
76
+ // then
77
+ expect(getChatsSpy).toHaveBeenCalledWith({ limit: 10 })
78
+ })
79
+
80
+ test('list: closes client after fetching chats', async () => {
81
+ // when
82
+ await chatCommand.parseAsync(['node', 'chat', 'list'])
83
+
84
+ // then
85
+ expect(closeSpy).toHaveBeenCalledTimes(1)
86
+ })
87
+
88
+ test('list: outputs chat metadata', async () => {
89
+ // when
90
+ await chatCommand.parseAsync(['node', 'chat', 'list'])
91
+
92
+ // then
93
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
94
+ const chat = output[0]
95
+ expect(chat.chat_id).toBeDefined()
96
+ expect(chat.type).toBeDefined()
97
+ expect(chat.display_name).toBeDefined()
98
+ })
99
+
100
+ test('list: includes different chat types', async () => {
101
+ // when
102
+ await chatCommand.parseAsync(['node', 'chat', 'list'])
103
+
104
+ // then
105
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
106
+ const types = output.map((c: { type: string }) => c.type)
107
+ expect(types).toContain('user')
108
+ expect(types).toContain('group')
109
+ expect(types).toContain('room')
110
+ })
@@ -0,0 +1,98 @@
1
+ import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ import { LineClient } from '../client'
6
+ import { friendCommand } from './friend'
7
+
8
+ let loginSpy: ReturnType<typeof spyOn>
9
+ let getFriendsSpy: ReturnType<typeof spyOn>
10
+ let closeSpy: ReturnType<typeof spyOn>
11
+ let consoleLogSpy: ReturnType<typeof mock>
12
+
13
+ beforeEach(() => {
14
+ loginSpy = spyOn(LineClient.prototype, 'login').mockImplementation(async function (this: LineClient) {
15
+ return this
16
+ })
17
+
18
+ getFriendsSpy = spyOn(LineClient.prototype, 'getFriends').mockResolvedValue([
19
+ {
20
+ mid: 'u111',
21
+ display_name: 'Alice',
22
+ picture_url: 'https://example.com/alice.jpg',
23
+ status_message: 'Hey!',
24
+ },
25
+ {
26
+ mid: 'u222',
27
+ display_name: 'Bob',
28
+ picture_url: 'https://example.com/bob.jpg',
29
+ status_message: 'Hello',
30
+ },
31
+ ])
32
+
33
+ closeSpy = spyOn(LineClient.prototype, 'close').mockImplementation(() => {})
34
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
35
+ })
36
+
37
+ afterEach(() => {
38
+ loginSpy?.mockRestore()
39
+ getFriendsSpy?.mockRestore()
40
+ closeSpy?.mockRestore()
41
+ console.log = originalConsoleLog
42
+ })
43
+
44
+ test('list: fetches and outputs friends', async () => {
45
+ // when
46
+ await friendCommand.parseAsync(['node', 'friend', 'list'])
47
+
48
+ // then
49
+ expect(loginSpy).toHaveBeenCalledTimes(1)
50
+ expect(getFriendsSpy).toHaveBeenCalledTimes(1)
51
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
52
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
53
+ expect(output).toHaveLength(2)
54
+ expect(output[0].mid).toBe('u111')
55
+ expect(output[0].display_name).toBe('Alice')
56
+ })
57
+
58
+ test('list: outputs friends with metadata', async () => {
59
+ // when
60
+ await friendCommand.parseAsync(['node', 'friend', 'list'])
61
+
62
+ // then
63
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
64
+ const friend = output[0]
65
+ expect(friend.mid).toBeDefined()
66
+ expect(friend.display_name).toBeDefined()
67
+ })
68
+
69
+ test('list: closes client after fetching friends', async () => {
70
+ // when
71
+ await friendCommand.parseAsync(['node', 'friend', 'list'])
72
+
73
+ // then
74
+ expect(closeSpy).toHaveBeenCalledTimes(1)
75
+ })
76
+
77
+ test('list: outputs empty array when no friends', async () => {
78
+ // given
79
+ getFriendsSpy.mockResolvedValue([])
80
+
81
+ // when
82
+ await friendCommand.parseAsync(['node', 'friend', 'list'])
83
+
84
+ // then
85
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
86
+ expect(output).toHaveLength(0)
87
+ })
88
+
89
+ test('list: includes all friend fields', async () => {
90
+ // when
91
+ await friendCommand.parseAsync(['node', 'friend', 'list'])
92
+
93
+ // then
94
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
95
+ expect(output[0].picture_url).toBe('https://example.com/alice.jpg')
96
+ expect(output[0].status_message).toBe('Hey!')
97
+ expect(output[1].display_name).toBe('Bob')
98
+ })
@@ -0,0 +1,119 @@
1
+ import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ import { LineClient } from '../client'
6
+ import { messageCommand } from './message'
7
+
8
+ let loginSpy: ReturnType<typeof spyOn>
9
+ let getMessagesSpy: ReturnType<typeof spyOn>
10
+ let sendMessageSpy: ReturnType<typeof spyOn>
11
+ let closeSpy: ReturnType<typeof spyOn>
12
+ let consoleLogSpy: ReturnType<typeof mock>
13
+
14
+ beforeEach(() => {
15
+ loginSpy = spyOn(LineClient.prototype, 'login').mockImplementation(async function (this: LineClient) {
16
+ return this
17
+ })
18
+
19
+ getMessagesSpy = spyOn(LineClient.prototype, 'getMessages').mockResolvedValue([
20
+ {
21
+ message_id: 'msg-1',
22
+ chat_id: 'chat-1',
23
+ author_id: 'u123',
24
+ text: 'Hello world',
25
+ content_type: 'NONE',
26
+ sent_at: '2024-01-01T10:00:00Z',
27
+ },
28
+ {
29
+ message_id: 'msg-2',
30
+ chat_id: 'chat-1',
31
+ author_id: 'u456',
32
+ text: 'Hi there',
33
+ content_type: 'NONE',
34
+ sent_at: '2024-01-01T09:00:00Z',
35
+ },
36
+ ])
37
+
38
+ sendMessageSpy = spyOn(LineClient.prototype, 'sendMessage').mockResolvedValue({
39
+ success: true,
40
+ chat_id: 'chat-1',
41
+ message_id: 'msg-new',
42
+ sent_at: '2024-01-01T11:00:00Z',
43
+ })
44
+
45
+ closeSpy = spyOn(LineClient.prototype, 'close').mockImplementation(() => {})
46
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
47
+ })
48
+
49
+ afterEach(() => {
50
+ loginSpy?.mockRestore()
51
+ getMessagesSpy?.mockRestore()
52
+ sendMessageSpy?.mockRestore()
53
+ closeSpy?.mockRestore()
54
+ console.log = originalConsoleLog
55
+ })
56
+
57
+ test('list: fetches and outputs messages for a chat', async () => {
58
+ // when
59
+ await messageCommand.parseAsync(['node', 'message', 'list', 'chat-1'])
60
+
61
+ // then
62
+ expect(loginSpy).toHaveBeenCalledTimes(1)
63
+ expect(getMessagesSpy).toHaveBeenCalledWith('chat-1', { count: 20 })
64
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
65
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
66
+ expect(output).toHaveLength(2)
67
+ expect(output[0].message_id).toBe('msg-1')
68
+ expect(output[0].text).toBe('Hello world')
69
+ })
70
+
71
+ test('list: uses custom count when --count option provided', async () => {
72
+ // when
73
+ await messageCommand.parseAsync(['node', 'message', 'list', 'chat-1', '--count', '5'])
74
+
75
+ // then
76
+ expect(getMessagesSpy).toHaveBeenCalledWith('chat-1', { count: 5 })
77
+ })
78
+
79
+ test('list: closes client after fetching messages', async () => {
80
+ // when
81
+ await messageCommand.parseAsync(['node', 'message', 'list', 'chat-1'])
82
+
83
+ // then
84
+ expect(closeSpy).toHaveBeenCalledTimes(1)
85
+ })
86
+
87
+ test('send: sends message and outputs result', async () => {
88
+ // when
89
+ await messageCommand.parseAsync(['node', 'message', 'send', 'chat-1', 'Hello!'])
90
+
91
+ // then
92
+ expect(loginSpy).toHaveBeenCalledTimes(1)
93
+ expect(sendMessageSpy).toHaveBeenCalledWith('chat-1', 'Hello!')
94
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
95
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
96
+ expect(output.success).toBe(true)
97
+ expect(output.message_id).toBe('msg-new')
98
+ })
99
+
100
+ test('send: closes client after sending message', async () => {
101
+ // when
102
+ await messageCommand.parseAsync(['node', 'message', 'send', 'chat-1', 'Hello!'])
103
+
104
+ // then
105
+ expect(closeSpy).toHaveBeenCalledTimes(1)
106
+ })
107
+
108
+ test('list: outputs messages with metadata', async () => {
109
+ // when
110
+ await messageCommand.parseAsync(['node', 'message', 'list', 'chat-1'])
111
+
112
+ // then
113
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
114
+ const msg = output[0]
115
+ expect(msg.message_id).toBeDefined()
116
+ expect(msg.chat_id).toBeDefined()
117
+ expect(msg.author_id).toBeDefined()
118
+ expect(msg.sent_at).toBeDefined()
119
+ })
@@ -0,0 +1,85 @@
1
+ import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const originalConsoleLog = console.log
4
+
5
+ import { LineClient } from '../client'
6
+ import { profileCommand } from './profile'
7
+
8
+ let loginSpy: ReturnType<typeof spyOn>
9
+ let getProfileSpy: ReturnType<typeof spyOn>
10
+ let closeSpy: ReturnType<typeof spyOn>
11
+ let consoleLogSpy: ReturnType<typeof mock>
12
+
13
+ beforeEach(() => {
14
+ loginSpy = spyOn(LineClient.prototype, 'login').mockImplementation(async function (this: LineClient) {
15
+ return this
16
+ })
17
+
18
+ getProfileSpy = spyOn(LineClient.prototype, 'getProfile').mockResolvedValue({
19
+ mid: 'u1234567890abcdef',
20
+ display_name: 'Test User',
21
+ picture_url: 'https://example.com/pic.jpg',
22
+ status_message: 'Hello LINE!',
23
+ })
24
+
25
+ closeSpy = spyOn(LineClient.prototype, 'close').mockImplementation(() => {})
26
+ consoleLogSpy = mock((..._args: unknown[]) => {}); console.log = consoleLogSpy
27
+ })
28
+
29
+ afterEach(() => {
30
+ loginSpy?.mockRestore()
31
+ getProfileSpy?.mockRestore()
32
+ closeSpy?.mockRestore()
33
+ console.log = originalConsoleLog
34
+ })
35
+
36
+ test('profile: fetches and outputs profile', async () => {
37
+ // when
38
+ await profileCommand.parseAsync(['node', 'profile'])
39
+
40
+ // then
41
+ expect(loginSpy).toHaveBeenCalledTimes(1)
42
+ expect(getProfileSpy).toHaveBeenCalledTimes(1)
43
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1)
44
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
45
+ expect(output.mid).toBe('u1234567890abcdef')
46
+ expect(output.display_name).toBe('Test User')
47
+ })
48
+
49
+ test('profile: outputs profile with all fields', async () => {
50
+ // when
51
+ await profileCommand.parseAsync(['node', 'profile'])
52
+
53
+ // then
54
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
55
+ expect(output.mid).toBeDefined()
56
+ expect(output.display_name).toBeDefined()
57
+ expect(output.picture_url).toBeDefined()
58
+ expect(output.status_message).toBeDefined()
59
+ })
60
+
61
+ test('profile: closes client after fetching profile', async () => {
62
+ // when
63
+ await profileCommand.parseAsync(['node', 'profile'])
64
+
65
+ // then
66
+ expect(closeSpy).toHaveBeenCalledTimes(1)
67
+ })
68
+
69
+ test('profile: outputs profile with picture_url', async () => {
70
+ // when
71
+ await profileCommand.parseAsync(['node', 'profile'])
72
+
73
+ // then
74
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
75
+ expect(output.picture_url).toBe('https://example.com/pic.jpg')
76
+ })
77
+
78
+ test('profile: outputs profile with status_message', async () => {
79
+ // when
80
+ await profileCommand.parseAsync(['node', 'profile'])
81
+
82
+ // then
83
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
84
+ expect(output.status_message).toBe('Hello LINE!')
85
+ })