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
@@ -1,89 +1,73 @@
1
- import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test'
1
+ import { afterEach, beforeEach, expect, spyOn, test } from 'bun:test'
2
2
 
3
3
  import { TeamsClient } from '../client'
4
4
  import { TeamsCredentialManager } from '../credential-manager'
5
5
  import { addAction, removeAction } from './reaction'
6
6
 
7
- let clientAddReactionSpy: ReturnType<typeof spyOn>
8
- let clientRemoveReactionSpy: ReturnType<typeof spyOn>
9
- let credManagerLoadConfigSpy: ReturnType<typeof spyOn>
7
+ let addReactionSpy: ReturnType<typeof spyOn>
8
+ let removeReactionSpy: ReturnType<typeof spyOn>
9
+ let getTokenWithExpirySpy: ReturnType<typeof spyOn>
10
+ let consoleLogSpy: ReturnType<typeof spyOn>
11
+ let processExitSpy: ReturnType<typeof spyOn>
10
12
 
11
13
  beforeEach(() => {
12
- clientAddReactionSpy = spyOn(TeamsClient.prototype, 'addReaction').mockResolvedValue(undefined)
13
- clientRemoveReactionSpy = spyOn(TeamsClient.prototype, 'removeReaction').mockResolvedValue(undefined)
14
- credManagerLoadConfigSpy = spyOn(TeamsCredentialManager.prototype, 'loadConfig').mockResolvedValue({
15
- current_account: 'work',
16
- accounts: {
17
- work: {
18
- token: 'test-token',
19
- account_type: 'work' as const,
20
- current_team: null,
21
- teams: {},
22
- },
23
- },
14
+ addReactionSpy = spyOn(TeamsClient.prototype, 'addReaction').mockResolvedValue(undefined)
15
+ removeReactionSpy = spyOn(TeamsClient.prototype, 'removeReaction').mockResolvedValue(undefined)
16
+ getTokenWithExpirySpy = spyOn(TeamsCredentialManager.prototype, 'getTokenWithExpiry').mockImplementation(() =>
17
+ Promise.resolve({ token: 'test-token', tokenExpiresAt: undefined }),
18
+ )
19
+
20
+ consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {})
21
+ processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
22
+ throw new Error(`process.exit(${_code})`)
24
23
  })
25
24
  })
26
25
 
27
26
  afterEach(() => {
28
- clientAddReactionSpy?.mockRestore()
29
- clientRemoveReactionSpy?.mockRestore()
30
- credManagerLoadConfigSpy?.mockRestore()
27
+ addReactionSpy.mockRestore()
28
+ removeReactionSpy.mockRestore()
29
+ getTokenWithExpirySpy.mockRestore()
30
+ consoleLogSpy.mockRestore()
31
+ processExitSpy.mockRestore()
31
32
  })
32
33
 
33
34
  test('add: sends correct POST request with emoji', async () => {
34
- const consoleSpy = mock((_msg: string) => {})
35
- const originalLog = console.log
36
- console.log = consoleSpy
37
-
38
35
  try {
39
36
  await addAction('team123', 'ch123', 'msg123', 'like', { pretty: false })
40
- expect(consoleSpy).toHaveBeenCalled()
41
- const output = JSON.parse(consoleSpy.mock.calls[0][0])
42
- expect(output.success).toBe(true)
43
- expect(output.team_id).toBe('team123')
44
- expect(output.channel_id).toBe('ch123')
45
- expect(output.message_id).toBe('msg123')
46
- expect(output.emoji).toBe('like')
47
- } finally {
48
- console.log = originalLog
49
- }
37
+ } catch {}
38
+
39
+ expect(consoleLogSpy).toHaveBeenCalled()
40
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
41
+ expect(output.success).toBe(true)
42
+ expect(output.team_id).toBe('team123')
43
+ expect(output.channel_id).toBe('ch123')
44
+ expect(output.message_id).toBe('msg123')
45
+ expect(output.emoji).toBe('like')
50
46
  })
51
47
 
52
48
  test('remove: sends correct DELETE request with emoji', async () => {
53
- const consoleSpy = mock((_msg: string) => {})
54
- const originalLog = console.log
55
- console.log = consoleSpy
56
-
57
49
  try {
58
50
  await removeAction('team123', 'ch123', 'msg123', 'like', { pretty: false })
59
- expect(consoleSpy).toHaveBeenCalled()
60
- const output = JSON.parse(consoleSpy.mock.calls[0][0])
61
- expect(output.success).toBe(true)
62
- expect(output.team_id).toBe('team123')
63
- expect(output.channel_id).toBe('ch123')
64
- expect(output.message_id).toBe('msg123')
65
- expect(output.emoji).toBe('like')
66
- } finally {
67
- console.log = originalLog
68
- }
51
+ } catch {}
52
+
53
+ expect(consoleLogSpy).toHaveBeenCalled()
54
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
55
+ expect(output.success).toBe(true)
56
+ expect(output.team_id).toBe('team123')
57
+ expect(output.channel_id).toBe('ch123')
58
+ expect(output.message_id).toBe('msg123')
59
+ expect(output.emoji).toBe('like')
69
60
  })
70
61
 
71
62
  test('add: handles missing token gracefully', async () => {
72
- credManagerLoadConfigSpy?.mockResolvedValue(null)
73
-
74
- const consoleSpy = mock((_msg: string) => {})
75
- const originalLog = console.log
76
- const originalExit = process.exit
77
- process.exit = mock(() => {}) as any
78
- console.log = consoleSpy
63
+ getTokenWithExpirySpy.mockImplementation(() => Promise.resolve(null))
79
64
 
80
65
  try {
81
66
  await addAction('team123', 'ch123', 'msg123', 'like', { pretty: false })
82
- expect(consoleSpy).toHaveBeenCalled()
83
- const output = JSON.parse(consoleSpy.mock.calls[0][0])
84
- expect(output.error).toBeDefined()
85
- } finally {
86
- console.log = originalLog
87
- process.exit = originalExit
88
- }
67
+ } catch {}
68
+
69
+ expect(consoleLogSpy).toHaveBeenCalled()
70
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0])
71
+ expect(output.error).toBeDefined()
72
+ expect(processExitSpy).toHaveBeenCalledWith(1)
89
73
  })
@@ -20,6 +20,7 @@ export async function addAction(
20
20
  if (!cred) {
21
21
  console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
22
22
  process.exit(1)
23
+ return
23
24
  }
24
25
 
25
26
  const client = await new TeamsClient().login({ token: cred.token, tokenExpiresAt: cred.tokenExpiresAt })
@@ -56,6 +57,7 @@ export async function removeAction(
56
57
  if (!cred) {
57
58
  console.log(formatOutput({ error: 'Not authenticated. Run "auth extract" first.' }, options.pretty))
58
59
  process.exit(1)
60
+ return
59
61
  }
60
62
 
61
63
  const client = await new TeamsClient().login({ token: cred.token, tokenExpiresAt: cred.tokenExpiresAt })
@@ -0,0 +1,125 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const mockListChats = mock(() =>
4
+ Promise.resolve([
5
+ { id: 'chat-1', title: 'General', type: 'supergroup', unread_count: 0 },
6
+ { id: 'chat-2', title: 'Random', type: 'group', unread_count: 5 },
7
+ ]),
8
+ )
9
+
10
+ const mockSearchChats = mock(() =>
11
+ Promise.resolve([{ id: 'chat-1', title: 'General', type: 'supergroup', unread_count: 0 }]),
12
+ )
13
+
14
+ const mockGetChat = mock(() =>
15
+ Promise.resolve({ id: 'chat-1', title: 'General', type: 'supergroup', unread_count: 0 }),
16
+ )
17
+
18
+ const mockClient = {
19
+ listChats: mockListChats,
20
+ searchChats: mockSearchChats,
21
+ getChat: mockGetChat,
22
+ }
23
+
24
+ mock.module('./shared', () => ({
25
+ withTelegramClient: async (_opts: unknown, fn: (client: typeof mockClient) => Promise<unknown>) =>
26
+ fn(mockClient),
27
+ }))
28
+
29
+ import { chatCommand } from './chat'
30
+
31
+ describe('chat commands', () => {
32
+ let consoleSpy: ReturnType<typeof spyOn>
33
+ let processExitSpy: ReturnType<typeof spyOn>
34
+
35
+ beforeEach(() => {
36
+ mockListChats.mockReset()
37
+ mockListChats.mockImplementation(() =>
38
+ Promise.resolve([
39
+ { id: 'chat-1', title: 'General', type: 'supergroup', unread_count: 0 },
40
+ { id: 'chat-2', title: 'Random', type: 'group', unread_count: 5 },
41
+ ]),
42
+ )
43
+ mockSearchChats.mockReset()
44
+ mockSearchChats.mockImplementation(() =>
45
+ Promise.resolve([{ id: 'chat-1', title: 'General', type: 'supergroup', unread_count: 0 }]),
46
+ )
47
+ mockGetChat.mockReset()
48
+ mockGetChat.mockImplementation(() =>
49
+ Promise.resolve({ id: 'chat-1', title: 'General', type: 'supergroup', unread_count: 0 }),
50
+ )
51
+ consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
52
+ processExitSpy = spyOn(process, 'exit').mockImplementation((() => {}) as (code?: number) => never)
53
+ })
54
+
55
+ afterEach(() => {
56
+ consoleSpy.mockRestore()
57
+ processExitSpy.mockRestore()
58
+ })
59
+
60
+ describe('list subcommand', () => {
61
+ test('calls listChats with default limit', async () => {
62
+ await chatCommand.parseAsync(['list'], { from: 'user' })
63
+
64
+ expect(mockListChats).toHaveBeenCalledWith(20)
65
+ })
66
+
67
+ test('calls listChats with custom limit', async () => {
68
+ await chatCommand.parseAsync(['list', '--limit', '5'], { from: 'user' })
69
+
70
+ expect(mockListChats).toHaveBeenCalledWith(5)
71
+ })
72
+
73
+ test('outputs JSON array to console', async () => {
74
+ await chatCommand.parseAsync(['list'], { from: 'user' })
75
+
76
+ expect(consoleSpy).toHaveBeenCalled()
77
+ const output = consoleSpy.mock.calls[0][0] as string
78
+ const parsed = JSON.parse(output)
79
+ expect(parsed).toBeArray()
80
+ expect(parsed).toHaveLength(2)
81
+ })
82
+ })
83
+
84
+ describe('search subcommand', () => {
85
+ test('calls searchChats with query and default limit', async () => {
86
+ await chatCommand.parseAsync(['search', 'General'], { from: 'user' })
87
+
88
+ expect(mockSearchChats).toHaveBeenCalledWith('General', 20)
89
+ })
90
+
91
+ test('calls searchChats with query and custom limit', async () => {
92
+ await chatCommand.parseAsync(['search', 'General', '--limit', '3'], { from: 'user' })
93
+
94
+ expect(mockSearchChats).toHaveBeenCalledWith('General', 3)
95
+ })
96
+
97
+ test('outputs JSON array to console', async () => {
98
+ await chatCommand.parseAsync(['search', 'General'], { from: 'user' })
99
+
100
+ expect(consoleSpy).toHaveBeenCalled()
101
+ const output = consoleSpy.mock.calls[0][0] as string
102
+ const parsed = JSON.parse(output)
103
+ expect(parsed).toBeArray()
104
+ expect(parsed).toHaveLength(1)
105
+ })
106
+ })
107
+
108
+ describe('get subcommand', () => {
109
+ test('calls getChat with chat reference', async () => {
110
+ await chatCommand.parseAsync(['get', 'chat-1'], { from: 'user' })
111
+
112
+ expect(mockGetChat).toHaveBeenCalledWith('chat-1')
113
+ })
114
+
115
+ test('outputs JSON object to console', async () => {
116
+ await chatCommand.parseAsync(['get', 'chat-1'], { from: 'user' })
117
+
118
+ expect(consoleSpy).toHaveBeenCalled()
119
+ const output = consoleSpy.mock.calls[0][0] as string
120
+ const parsed = JSON.parse(output)
121
+ expect(parsed).toBeObject()
122
+ expect(parsed.id).toBe('chat-1')
123
+ })
124
+ })
125
+ })
@@ -0,0 +1,92 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test'
2
+
3
+ const mockListMessages = mock(() =>
4
+ Promise.resolve([
5
+ { id: 1, text: 'Hello', sender_id: 'user-1', date: 1000 },
6
+ { id: 2, text: 'World', sender_id: 'user-2', date: 2000 },
7
+ ]),
8
+ )
9
+
10
+ const mockSendMessage = mock(() =>
11
+ Promise.resolve({ id: 3, text: 'Sent message', sender_id: 'user-1', date: 3000 }),
12
+ )
13
+
14
+ const mockClient = {
15
+ listMessages: mockListMessages,
16
+ sendMessage: mockSendMessage,
17
+ }
18
+
19
+ mock.module('./shared', () => ({
20
+ withTelegramClient: async (_opts: unknown, fn: (client: typeof mockClient) => Promise<unknown>) =>
21
+ fn(mockClient),
22
+ }))
23
+
24
+ import { messageCommand } from './message'
25
+
26
+ describe('message commands', () => {
27
+ let consoleSpy: ReturnType<typeof spyOn>
28
+ let processExitSpy: ReturnType<typeof spyOn>
29
+
30
+ beforeEach(() => {
31
+ mockListMessages.mockReset()
32
+ mockListMessages.mockImplementation(() =>
33
+ Promise.resolve([
34
+ { id: 1, text: 'Hello', sender_id: 'user-1', date: 1000 },
35
+ { id: 2, text: 'World', sender_id: 'user-2', date: 2000 },
36
+ ]),
37
+ )
38
+ mockSendMessage.mockReset()
39
+ mockSendMessage.mockImplementation(() =>
40
+ Promise.resolve({ id: 3, text: 'Sent message', sender_id: 'user-1', date: 3000 }),
41
+ )
42
+ consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
43
+ processExitSpy = spyOn(process, 'exit').mockImplementation((() => {}) as (code?: number) => never)
44
+ })
45
+
46
+ afterEach(() => {
47
+ consoleSpy.mockRestore()
48
+ processExitSpy.mockRestore()
49
+ })
50
+
51
+ describe('list subcommand', () => {
52
+ test('calls listMessages with chat reference and default limit', async () => {
53
+ await messageCommand.parseAsync(['list', 'chat-123'], { from: 'user' })
54
+
55
+ expect(mockListMessages).toHaveBeenCalledWith('chat-123', 20)
56
+ })
57
+
58
+ test('calls listMessages with custom limit', async () => {
59
+ await messageCommand.parseAsync(['list', 'chat-123', '--limit', '10'], { from: 'user' })
60
+
61
+ expect(mockListMessages).toHaveBeenCalledWith('chat-123', 10)
62
+ })
63
+
64
+ test('outputs JSON to console', async () => {
65
+ await messageCommand.parseAsync(['list', 'chat-123'], { from: 'user' })
66
+
67
+ expect(consoleSpy).toHaveBeenCalled()
68
+ const output = consoleSpy.mock.calls[0][0] as string
69
+ const parsed = JSON.parse(output)
70
+ expect(parsed).toBeArray()
71
+ expect(parsed).toHaveLength(2)
72
+ })
73
+ })
74
+
75
+ describe('send subcommand', () => {
76
+ test('calls sendMessage with chat reference and text', async () => {
77
+ await messageCommand.parseAsync(['send', 'chat-123', 'Hello there'], { from: 'user' })
78
+
79
+ expect(mockSendMessage).toHaveBeenCalledWith('chat-123', 'Hello there')
80
+ })
81
+
82
+ test('outputs JSON to console', async () => {
83
+ await messageCommand.parseAsync(['send', 'chat-123', 'Hello there'], { from: 'user' })
84
+
85
+ expect(consoleSpy).toHaveBeenCalled()
86
+ const output = consoleSpy.mock.calls[0][0] as string
87
+ const parsed = JSON.parse(output)
88
+ expect(parsed).toBeObject()
89
+ expect(parsed.id).toBe(3)
90
+ })
91
+ })
92
+ })
@@ -1,6 +1,7 @@
1
1
  import type { WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './types'
2
2
  import { WebexError } from './types'
3
3
  import { WebexCredentialManager } from './credential-manager'
4
+ import { WebexEncryptionService } from './encryption'
4
5
 
5
6
  const BASE_URL = 'https://webexapis.com/v1'
6
7
  const MAX_RETRIES = 3
@@ -17,6 +18,7 @@ export class WebexClient {
17
18
  private tokenType: string | null = null
18
19
  private buckets: Map<string, RateLimitBucket> = new Map()
19
20
  private globalRateLimitUntil: number = 0
21
+ private encryption: WebexEncryptionService | null = null
20
22
 
21
23
  async login(credentials?: { token: string }): Promise<this> {
22
24
  if (credentials) {
@@ -40,7 +42,16 @@ export class WebexClient {
40
42
  }
41
43
  this.deviceUrl = config?.deviceUrl ?? null
42
44
  this.tokenType = config?.tokenType ?? null
43
- return this.login({ token })
45
+ await this.login({ token })
46
+
47
+ if (this.tokenType === 'extracted' && config?.encryptionKeys) {
48
+ const keysMap = new Map(Object.entries(config.encryptionKeys))
49
+ if (keysMap.size > 0) {
50
+ this.encryption = new WebexEncryptionService(keysMap)
51
+ }
52
+ }
53
+
54
+ return this
44
55
  }
45
56
 
46
57
  private ensureAuth(): string {
@@ -210,7 +221,7 @@ export class WebexClient {
210
221
  private async internalRequest<T>(path: string, init?: RequestInit): Promise<T> {
211
222
  const response = await fetch(`${this.convBaseUrl}${path}`, {
212
223
  ...init,
213
- headers: { ...this.internalHeaders, ...init?.headers as Record<string, string> },
224
+ headers: { ...this.internalHeaders, ...(init?.headers as Record<string, string>) },
214
225
  })
215
226
 
216
227
  if (!response.ok) {
@@ -225,35 +236,78 @@ export class WebexClient {
225
236
  return response.json() as Promise<T>
226
237
  }
227
238
 
228
- private activityToMessage(a: InternalActivity, roomId: string): WebexMessage {
239
+ private async activityToMessage(a: InternalActivity, roomId: string): Promise<WebexMessage> {
240
+ let text = a.object?.content ?? a.object?.displayName
241
+
242
+ if (this.encryption && text?.startsWith('eyJ')) {
243
+ const keyUrl = a.encryptionKeyUrl ?? a.object?.encryptionKeyUrl
244
+ if (keyUrl) {
245
+ const decrypted = await this.encryption.decryptText(keyUrl, text)
246
+ if (decrypted !== null) {
247
+ text = decrypted
248
+ }
249
+ }
250
+ }
251
+
229
252
  return {
230
253
  id: a.id,
231
254
  roomId,
232
255
  roomType: 'group' as const,
233
- text: a.object?.content ?? a.object?.displayName,
256
+ text,
234
257
  personId: a.actor?.entryUUID ?? a.actor?.id ?? '',
235
258
  personEmail: a.actor?.emailAddress ?? '',
236
259
  created: a.published,
237
260
  }
238
261
  }
239
262
 
263
+ private async buildEncryptedObject(
264
+ convUuid: string,
265
+ text: string,
266
+ options?: { markdown?: boolean },
267
+ ): Promise<{ object: Record<string, string>; encryptionKeyUrl?: string }> {
268
+ const buildObject = (content: string): Record<string, string> =>
269
+ options?.markdown
270
+ ? { objectType: 'comment', displayName: content, content, markdown: content }
271
+ : { objectType: 'comment', displayName: content, content }
272
+
273
+ if (this.encryption) {
274
+ const conv = await this.internalRequest<InternalConversation>(
275
+ `/conversations/${convUuid}?activitiesLimit=0&participantsLimit=0`,
276
+ )
277
+ const keyUri = conv.defaultActivityEncryptionKeyUrl
278
+ if (keyUri) {
279
+ const encrypted = await this.encryption.encryptText(keyUri, text)
280
+ if (encrypted) {
281
+ return { object: buildObject(encrypted), encryptionKeyUrl: keyUri }
282
+ }
283
+ }
284
+ }
285
+
286
+ return { object: buildObject(text) }
287
+ }
288
+
240
289
  private async sendMessageInternal(
241
290
  roomId: string,
242
291
  text: string,
243
292
  options?: { markdown?: boolean },
244
293
  ): Promise<WebexMessage> {
245
294
  const convUuid = this.decodeConvUuid(roomId)
246
- const object = options?.markdown
247
- ? { objectType: 'comment', displayName: text, content: text, markdown: text }
248
- : { objectType: 'comment', displayName: text, content: text }
295
+ const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, options)
296
+
297
+ const activity: Record<string, unknown> = {
298
+ verb: 'post',
299
+ object,
300
+ target: { id: convUuid, objectType: 'conversation' },
301
+ clientTempId: `tmp-${Date.now()}`,
302
+ }
303
+
304
+ if (encryptionKeyUrl) {
305
+ activity['encryptionKeyUrl'] = encryptionKeyUrl
306
+ }
307
+
249
308
  const result = await this.internalRequest<InternalActivity>('/activities', {
250
309
  method: 'POST',
251
- body: JSON.stringify({
252
- verb: 'post',
253
- object,
254
- target: { id: convUuid, objectType: 'conversation' },
255
- clientTempId: `tmp-${Date.now()}`,
256
- }),
310
+ body: JSON.stringify(activity),
257
311
  })
258
312
  return this.activityToMessage(result, roomId)
259
313
  }
@@ -297,9 +351,8 @@ export class WebexClient {
297
351
  const conv = await this.internalRequest<InternalConversation>(
298
352
  `/conversations/${convUuid}?activitiesLimit=${max}&participantsLimit=0`,
299
353
  )
300
- return (conv.activities?.items ?? [])
301
- .filter((a) => a.verb === 'post')
302
- .map((a) => this.activityToMessage(a, roomId))
354
+ const activities = (conv.activities?.items ?? []).filter((a) => a.verb === 'post')
355
+ return Promise.all(activities.map((a) => this.activityToMessage(a, roomId)))
303
356
  }
304
357
  const params = new URLSearchParams()
305
358
  params.set('roomId', roomId)
@@ -312,7 +365,9 @@ export class WebexClient {
312
365
  if (this.useInternalAPI) {
313
366
  const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
314
367
  const convId = activity.target?.id ?? ''
315
- const roomId = convId ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64') : ''
368
+ const roomId = convId
369
+ ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64')
370
+ : ''
316
371
  return this.activityToMessage(activity, roomId)
317
372
  }
318
373
  return this.request<WebexMessage>('GET', `/messages/${messageId}`)
@@ -344,15 +399,23 @@ export class WebexClient {
344
399
  ): Promise<WebexMessage> {
345
400
  if (this.useInternalAPI) {
346
401
  const convUuid = this.decodeConvUuid(roomId)
402
+ const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, options)
403
+
404
+ const activity: Record<string, unknown> = {
405
+ verb: 'post',
406
+ object,
407
+ target: { id: convUuid, objectType: 'conversation' },
408
+ parent: { id: messageId, type: 'edit' },
409
+ clientTempId: `tmp-${Date.now()}`,
410
+ }
411
+
412
+ if (encryptionKeyUrl) {
413
+ activity['encryptionKeyUrl'] = encryptionKeyUrl
414
+ }
415
+
347
416
  const result = await this.internalRequest<InternalActivity>('/activities', {
348
417
  method: 'POST',
349
- body: JSON.stringify({
350
- verb: 'post',
351
- object: { objectType: 'comment', displayName: text, content: text },
352
- target: { id: convUuid, objectType: 'conversation' },
353
- parent: { id: messageId, type: 'edit' },
354
- clientTempId: `tmp-${Date.now()}`,
355
- }),
418
+ body: JSON.stringify(activity),
356
419
  })
357
420
  return this.activityToMessage(result, roomId)
358
421
  }
@@ -394,12 +457,21 @@ interface InternalActivity {
394
457
  id: string
395
458
  verb: string
396
459
  actor?: { displayName?: string; emailAddress?: string; entryUUID?: string; id?: string }
397
- object?: { content?: string; displayName?: string; objectType?: string }
398
- target?: { id: string }
460
+ object?: {
461
+ content?: string
462
+ displayName?: string
463
+ objectType?: string
464
+ encryptionKeyUrl?: string
465
+ }
466
+ target?: { id: string; encryptionKeyUrl?: string }
399
467
  published: string
468
+ encryptionKeyUrl?: string
400
469
  }
401
470
 
402
471
  interface InternalConversation {
403
472
  id: string
404
473
  activities?: { items: InternalActivity[] }
474
+ defaultActivityEncryptionKeyUrl?: string
475
+ kmsResourceObjectUrl?: string
476
+ encryptionKeyUrl?: string
405
477
  }
@@ -174,6 +174,10 @@ export async function extractAction(options: { pretty?: boolean; debug?: boolean
174
174
  expiresAt: extracted.expiresAt ?? 0,
175
175
  tokenType: 'extracted',
176
176
  deviceUrl: extracted.deviceUrl,
177
+ userId: extracted.userId,
178
+ encryptionKeys: extracted.encryptionKeys
179
+ ? Object.fromEntries(extracted.encryptionKeys)
180
+ : undefined,
177
181
  })
178
182
 
179
183
  console.log(