agent-messenger 2.1.0 → 2.2.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 (207) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.env.template +35 -17
  3. package/README.md +7 -7
  4. package/bun.lock +6 -6
  5. package/dist/package.json +2 -2
  6. package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
  7. package/dist/src/platforms/channeltalk/commands/auth.js +35 -28
  8. package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
  9. package/dist/src/platforms/channeltalk/ensure-auth.js +6 -6
  10. package/dist/src/platforms/channeltalk/ensure-auth.js.map +1 -1
  11. package/dist/src/platforms/channeltalk/token-extractor.d.ts +23 -1
  12. package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
  13. package/dist/src/platforms/channeltalk/token-extractor.js +299 -29
  14. package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
  15. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  16. package/dist/src/platforms/discord/commands/auth.js +57 -49
  17. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  18. package/dist/src/platforms/discord/ensure-auth.js +3 -3
  19. package/dist/src/platforms/discord/ensure-auth.js.map +1 -1
  20. package/dist/src/platforms/discord/token-extractor.d.ts +6 -1
  21. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  22. package/dist/src/platforms/discord/token-extractor.js +167 -14
  23. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  24. package/dist/src/platforms/instagram/client.d.ts +2 -0
  25. package/dist/src/platforms/instagram/client.d.ts.map +1 -1
  26. package/dist/src/platforms/instagram/client.js +2 -2
  27. package/dist/src/platforms/instagram/client.js.map +1 -1
  28. package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
  29. package/dist/src/platforms/instagram/commands/auth.js +107 -14
  30. package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
  31. package/dist/src/platforms/instagram/ensure-auth.d.ts.map +1 -1
  32. package/dist/src/platforms/instagram/ensure-auth.js +57 -11
  33. package/dist/src/platforms/instagram/ensure-auth.js.map +1 -1
  34. package/dist/src/platforms/instagram/index.d.ts +1 -0
  35. package/dist/src/platforms/instagram/index.d.ts.map +1 -1
  36. package/dist/src/platforms/instagram/index.js +1 -0
  37. package/dist/src/platforms/instagram/index.js.map +1 -1
  38. package/dist/src/platforms/instagram/token-extractor.d.ts +44 -0
  39. package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -0
  40. package/dist/src/platforms/instagram/token-extractor.js +407 -0
  41. package/dist/src/platforms/instagram/token-extractor.js.map +1 -0
  42. package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
  43. package/dist/src/platforms/kakaotalk/client.js +2 -1
  44. package/dist/src/platforms/kakaotalk/client.js.map +1 -1
  45. package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
  46. package/dist/src/platforms/kakaotalk/commands/auth.js +14 -13
  47. package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
  48. package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
  49. package/dist/src/platforms/kakaotalk/protocol/connection.js +2 -1
  50. package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
  51. package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
  52. package/dist/src/platforms/line/commands/auth.js +6 -5
  53. package/dist/src/platforms/line/commands/auth.js.map +1 -1
  54. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  55. package/dist/src/platforms/slack/commands/auth.js +11 -10
  56. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  57. package/dist/src/platforms/slack/token-extractor.d.ts +9 -0
  58. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  59. package/dist/src/platforms/slack/token-extractor.js +300 -23
  60. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  61. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  62. package/dist/src/platforms/teams/commands/auth.js +9 -8
  63. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  64. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
  65. package/dist/src/platforms/teams/ensure-auth.js +2 -1
  66. package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
  67. package/dist/src/platforms/teams/token-extractor.d.ts +5 -0
  68. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  69. package/dist/src/platforms/teams/token-extractor.js +161 -29
  70. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  71. package/dist/src/platforms/telegram/client.d.ts.map +1 -1
  72. package/dist/src/platforms/telegram/client.js +25 -7
  73. package/dist/src/platforms/telegram/client.js.map +1 -1
  74. package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
  75. package/dist/src/platforms/telegram/commands/auth.js +6 -5
  76. package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
  77. package/dist/src/platforms/webex/client.d.ts +10 -0
  78. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  79. package/dist/src/platforms/webex/client.js +124 -0
  80. package/dist/src/platforms/webex/client.js.map +1 -1
  81. package/dist/src/platforms/webex/commands/auth.d.ts +4 -0
  82. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  83. package/dist/src/platforms/webex/commands/auth.js +46 -4
  84. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  85. package/dist/src/platforms/webex/credential-manager.js +1 -1
  86. package/dist/src/platforms/webex/credential-manager.js.map +1 -1
  87. package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -1
  88. package/dist/src/platforms/webex/ensure-auth.js +21 -5
  89. package/dist/src/platforms/webex/ensure-auth.js.map +1 -1
  90. package/dist/src/platforms/webex/index.d.ts +2 -0
  91. package/dist/src/platforms/webex/index.d.ts.map +1 -1
  92. package/dist/src/platforms/webex/index.js +1 -0
  93. package/dist/src/platforms/webex/index.js.map +1 -1
  94. package/dist/src/platforms/webex/token-extractor.d.ts +28 -0
  95. package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
  96. package/dist/src/platforms/webex/token-extractor.js +344 -0
  97. package/dist/src/platforms/webex/token-extractor.js.map +1 -0
  98. package/dist/src/platforms/webex/types.d.ts +4 -1
  99. package/dist/src/platforms/webex/types.d.ts.map +1 -1
  100. package/dist/src/platforms/webex/types.js +2 -1
  101. package/dist/src/platforms/webex/types.js.map +1 -1
  102. package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
  103. package/dist/src/platforms/whatsapp/client.js +6 -2
  104. package/dist/src/platforms/whatsapp/client.js.map +1 -1
  105. package/dist/src/shared/utils/derived-key-cache.d.ts +1 -1
  106. package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
  107. package/dist/src/shared/utils/error-handler.d.ts +1 -1
  108. package/dist/src/shared/utils/error-handler.d.ts.map +1 -1
  109. package/dist/src/shared/utils/error-handler.js +3 -2
  110. package/dist/src/shared/utils/error-handler.js.map +1 -1
  111. package/dist/src/shared/utils/stderr.d.ts +5 -0
  112. package/dist/src/shared/utils/stderr.d.ts.map +1 -0
  113. package/dist/src/shared/utils/stderr.js +18 -0
  114. package/dist/src/shared/utils/stderr.js.map +1 -0
  115. package/docs/content/docs/cli/channeltalk.mdx +7 -7
  116. package/docs/content/docs/cli/discord.mdx +3 -3
  117. package/docs/content/docs/cli/instagram.mdx +28 -6
  118. package/docs/content/docs/cli/slack.mdx +2 -2
  119. package/docs/content/docs/cli/teams.mdx +6 -4
  120. package/docs/content/docs/cli/webex.mdx +30 -11
  121. package/e2e/README.md +132 -8
  122. package/e2e/channeltalk.e2e.test.ts +2 -7
  123. package/e2e/channeltalkbot.e2e.test.ts +2 -6
  124. package/e2e/config.ts +172 -10
  125. package/e2e/helpers.ts +7 -0
  126. package/e2e/instagram.e2e.test.ts +97 -0
  127. package/e2e/kakaotalk.e2e.test.ts +74 -0
  128. package/e2e/line.e2e.test.ts +92 -0
  129. package/e2e/teams.e2e.test.ts +46 -1
  130. package/e2e/telegram.e2e.test.ts +84 -0
  131. package/e2e/webex.e2e.test.ts +190 -0
  132. package/e2e/whatsapp.e2e.test.ts +90 -0
  133. package/e2e/whatsappbot.e2e.test.ts +78 -0
  134. package/package.json +2 -2
  135. package/skills/agent-channeltalk/SKILL.md +9 -9
  136. package/skills/agent-channeltalk/references/authentication.md +21 -18
  137. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  138. package/skills/agent-discord/SKILL.md +5 -5
  139. package/skills/agent-discord/references/authentication.md +8 -8
  140. package/skills/agent-discordbot/SKILL.md +1 -1
  141. package/skills/agent-instagram/SKILL.md +51 -9
  142. package/skills/agent-instagram/references/authentication.md +35 -3
  143. package/skills/agent-kakaotalk/SKILL.md +1 -1
  144. package/skills/agent-line/SKILL.md +1 -1
  145. package/skills/agent-slack/SKILL.md +5 -5
  146. package/skills/agent-slack/references/authentication.md +8 -8
  147. package/skills/agent-slackbot/SKILL.md +1 -1
  148. package/skills/agent-teams/SKILL.md +6 -6
  149. package/skills/agent-teams/references/authentication.md +8 -8
  150. package/skills/agent-telegram/SKILL.md +1 -1
  151. package/skills/agent-webex/SKILL.md +35 -15
  152. package/skills/agent-webex/references/authentication.md +62 -9
  153. package/skills/agent-webex/references/common-patterns.md +6 -3
  154. package/skills/agent-whatsapp/SKILL.md +1 -1
  155. package/skills/agent-whatsappbot/SKILL.md +1 -1
  156. package/src/platforms/channeltalk/commands/auth.test.ts +5 -5
  157. package/src/platforms/channeltalk/commands/auth.ts +38 -32
  158. package/src/platforms/channeltalk/ensure-auth.test.ts +6 -6
  159. package/src/platforms/channeltalk/ensure-auth.ts +6 -6
  160. package/src/platforms/channeltalk/token-extractor.test.ts +182 -15
  161. package/src/platforms/channeltalk/token-extractor.ts +344 -30
  162. package/src/platforms/discord/commands/auth.test.ts +3 -3
  163. package/src/platforms/discord/commands/auth.ts +58 -54
  164. package/src/platforms/discord/ensure-auth.test.ts +3 -3
  165. package/src/platforms/discord/ensure-auth.ts +3 -3
  166. package/src/platforms/discord/token-extractor.test.ts +199 -27
  167. package/src/platforms/discord/token-extractor.ts +190 -17
  168. package/src/platforms/instagram/client.ts +2 -2
  169. package/src/platforms/instagram/commands/auth.ts +133 -14
  170. package/src/platforms/instagram/ensure-auth.ts +63 -12
  171. package/src/platforms/instagram/index.ts +1 -0
  172. package/src/platforms/instagram/token-extractor.test.ts +424 -0
  173. package/src/platforms/instagram/token-extractor.ts +478 -0
  174. package/src/platforms/kakaotalk/client.ts +3 -1
  175. package/src/platforms/kakaotalk/commands/auth.ts +14 -13
  176. package/src/platforms/kakaotalk/protocol/connection.ts +3 -1
  177. package/src/platforms/line/commands/auth.ts +7 -6
  178. package/src/platforms/slack/cli.test.ts +6 -5
  179. package/src/platforms/slack/commands/auth.test.ts +11 -7
  180. package/src/platforms/slack/commands/auth.ts +11 -10
  181. package/src/platforms/slack/token-extractor.test.ts +98 -1
  182. package/src/platforms/slack/token-extractor.ts +338 -26
  183. package/src/platforms/teams/commands/auth.ts +9 -8
  184. package/src/platforms/teams/ensure-auth.ts +3 -1
  185. package/src/platforms/teams/token-extractor.test.ts +136 -17
  186. package/src/platforms/teams/token-extractor.ts +182 -31
  187. package/src/platforms/telegram/client.test.ts +134 -0
  188. package/src/platforms/telegram/client.ts +27 -6
  189. package/src/platforms/telegram/commands/auth.ts +6 -5
  190. package/src/platforms/webex/client.test.ts +314 -0
  191. package/src/platforms/webex/client.ts +158 -0
  192. package/src/platforms/webex/commands/auth.ts +67 -4
  193. package/src/platforms/webex/commands/member.test.ts +10 -1
  194. package/src/platforms/webex/commands/message.test.ts +9 -5
  195. package/src/platforms/webex/commands/snapshot.test.ts +13 -4
  196. package/src/platforms/webex/commands/space.test.ts +12 -2
  197. package/src/platforms/webex/credential-manager.ts +1 -1
  198. package/src/platforms/webex/ensure-auth.test.ts +4 -0
  199. package/src/platforms/webex/ensure-auth.ts +23 -4
  200. package/src/platforms/webex/index.ts +2 -0
  201. package/src/platforms/webex/token-extractor.test.ts +327 -0
  202. package/src/platforms/webex/token-extractor.ts +393 -0
  203. package/src/platforms/webex/types.ts +4 -2
  204. package/src/platforms/whatsapp/client.ts +11 -7
  205. package/src/shared/utils/derived-key-cache.ts +1 -1
  206. package/src/shared/utils/error-handler.ts +4 -2
  207. package/src/shared/utils/stderr.ts +22 -0
@@ -16,6 +16,140 @@ const mockPaths: TelegramAccountPaths = {
16
16
  files_dir: '/tmp/test-files',
17
17
  }
18
18
 
19
+ function createMockClient(sendHandler: (request: any, events: any[]) => void) {
20
+ const events: any[] = []
21
+ const createClientId = mock(() => 1)
22
+ const send = mock((_clientId: number, request: any) => sendHandler(request, events))
23
+ const receive = mock(() => events.shift() ?? null)
24
+
25
+ const client = new (TelegramTdlibClient as unknown as new (
26
+ account: TelegramAccount,
27
+ paths: TelegramAccountPaths,
28
+ tdjson: any,
29
+ ) => TelegramTdlibClient)(mockAccount, mockPaths, {
30
+ createClientId,
31
+ send,
32
+ receive,
33
+ libraryPath: '/mock/lib',
34
+ })
35
+
36
+ return { client, events, send }
37
+ }
38
+
39
+ function pushAuthReady(events: any[], extra: string) {
40
+ events.push({
41
+ '@type': 'updateAuthorizationState',
42
+ authorization_state: { '@type': 'authorizationStateReady' },
43
+ '@extra': extra,
44
+ })
45
+ }
46
+
47
+ describe('listChats', () => {
48
+ test('loads chats across multiple loadChats calls until 404', async () => {
49
+ let loadChatsCallCount = 0
50
+ const allChatIds = [1, 2, 3, 4, 5]
51
+
52
+ const { client } = createMockClient((request, events) => {
53
+ if (request['@type'] === 'getAuthorizationState') {
54
+ pushAuthReady(events, request['@extra'])
55
+ return
56
+ }
57
+
58
+ if (request['@type'] === 'loadChats') {
59
+ loadChatsCallCount += 1
60
+ if (loadChatsCallCount <= 2) {
61
+ // given — first two calls succeed (simulate partial loading)
62
+ events.push({ '@type': 'ok', '@extra': request['@extra'] })
63
+ } else {
64
+ // given — third call returns 404 (all chats loaded)
65
+ events.push({ '@type': 'error', code: 404, message: 'Chat list has been loaded completely', '@extra': request['@extra'] })
66
+ }
67
+ return
68
+ }
69
+
70
+ if (request['@type'] === 'getChats') {
71
+ // when — after first two loadChats calls, return partial; after 404, return all
72
+ const returnCount = loadChatsCallCount >= 3 ? allChatIds.length : Math.min(loadChatsCallCount * 2, allChatIds.length)
73
+ events.push({
74
+ '@type': 'chats',
75
+ total_count: returnCount,
76
+ chat_ids: allChatIds.slice(0, returnCount),
77
+ '@extra': request['@extra'],
78
+ })
79
+ return
80
+ }
81
+
82
+ if (request['@type'] === 'getChat') {
83
+ const chatId = request.chat_id
84
+ const typeNames = ['chatTypePrivate', 'chatTypePrivate', 'chatTypeBasicGroup', 'chatTypeSupergroup', 'chatTypeSupergroup']
85
+ const idx = allChatIds.indexOf(chatId)
86
+ events.push({
87
+ '@type': 'chat',
88
+ id: chatId,
89
+ title: `Chat ${chatId}`,
90
+ type: { '@type': typeNames[idx] ?? 'chatTypePrivate' },
91
+ unread_count: 0,
92
+ '@extra': request['@extra'],
93
+ })
94
+ return
95
+ }
96
+ })
97
+
98
+ const chats = await client.listChats(5)
99
+
100
+ // then — all 5 chats returned including groups
101
+ expect(chats).toHaveLength(5)
102
+ expect(chats.map((c) => c.type)).toEqual(['private', 'private', 'basicgroup', 'supergroup', 'supergroup'])
103
+ expect(loadChatsCallCount).toBe(3)
104
+ })
105
+
106
+ test('stops loading when enough chats are cached before 404', async () => {
107
+ let loadChatsCallCount = 0
108
+
109
+ const { client } = createMockClient((request, events) => {
110
+ if (request['@type'] === 'getAuthorizationState') {
111
+ pushAuthReady(events, request['@extra'])
112
+ return
113
+ }
114
+
115
+ if (request['@type'] === 'loadChats') {
116
+ loadChatsCallCount += 1
117
+ events.push({ '@type': 'ok', '@extra': request['@extra'] })
118
+ return
119
+ }
120
+
121
+ if (request['@type'] === 'getChats') {
122
+ // given — always return 3 chats (enough for limit=3)
123
+ events.push({
124
+ '@type': 'chats',
125
+ total_count: 3,
126
+ chat_ids: [10, 20, 30],
127
+ '@extra': request['@extra'],
128
+ })
129
+ return
130
+ }
131
+
132
+ if (request['@type'] === 'getChat') {
133
+ events.push({
134
+ '@type': 'chat',
135
+ id: request.chat_id,
136
+ title: `Chat ${request.chat_id}`,
137
+ type: { '@type': 'chatTypeSupergroup' },
138
+ unread_count: 0,
139
+ '@extra': request['@extra'],
140
+ })
141
+ return
142
+ }
143
+ })
144
+
145
+ const chats = await client.listChats(3)
146
+
147
+ // then — stops after first loop iteration since we have enough
148
+ expect(chats).toHaveLength(3)
149
+ expect(loadChatsCallCount).toBe(1)
150
+ })
151
+ })
152
+
19
153
  describe('sendMessage confirmation', () => {
20
154
  test('returns confirmed message id when updateMessageSendSucceeded arrives', async () => {
21
155
  const tempId = 100
@@ -162,16 +162,37 @@ export class TelegramTdlibClient {
162
162
  async listChats(limit: number = 20): Promise<TelegramChatSummary[]> {
163
163
  await this.ensureReady()
164
164
 
165
- try {
166
- await this.call({
167
- '@type': 'loadChats',
165
+ // loadChats may load fewer chats than requested per call. Loop until TDLib
166
+ // signals 404 ("chat list fully loaded") or we have enough cached entries.
167
+ for (;;) {
168
+ try {
169
+ await this.call({
170
+ '@type': 'loadChats',
171
+ chat_list: {
172
+ '@type': 'chatListMain',
173
+ },
174
+ limit,
175
+ })
176
+ } catch (error) {
177
+ // TDLib signals 404 when the entire chat list has been loaded.
178
+ if (error instanceof TelegramError && error.code === 404) {
179
+ break
180
+ }
181
+
182
+ break
183
+ }
184
+
185
+ const partial = (await this.call({
186
+ '@type': 'getChats',
168
187
  chat_list: {
169
188
  '@type': 'chatListMain',
170
189
  },
171
190
  limit,
172
- })
173
- } catch {
174
- // Best-effort cache warmup only.
191
+ })) as TdChats
192
+
193
+ if ((partial.chat_ids ?? []).length >= limit) {
194
+ break
195
+ }
175
196
  }
176
197
 
177
198
  const response = (await this.call({
@@ -4,6 +4,7 @@ import { Command } from 'commander'
4
4
 
5
5
  import { handleError } from '../../../shared/utils/error-handler'
6
6
  import { formatOutput } from '../../../shared/utils/output'
7
+ import { info, error as stderrError } from '@/shared/utils/stderr'
7
8
  import { getTelegramAppCredentials } from '../app-config'
8
9
  import { TelegramTdlibClient } from '../client'
9
10
  import { TelegramCredentialManager } from '../credential-manager'
@@ -126,8 +127,8 @@ async function fillMissingBootstrappingInputs(
126
127
  if (!resolved.apiId && !existing?.api_id) {
127
128
  if (shouldUseInteractivePrompts()) {
128
129
  try {
129
- console.error('No API credentials found. Provisioning via my.telegram.org...')
130
- console.error('A verification code will be sent to your Telegram account.\n')
130
+ info('No API credentials found. Provisioning via my.telegram.org...')
131
+ info('A verification code will be sent to your Telegram account.\n')
131
132
 
132
133
  const phone = resolved.phone || (await promptText('Phone number (e.g. +14155551234)'))
133
134
  if (!phone) {
@@ -148,10 +149,10 @@ async function fillMissingBootstrappingInputs(
148
149
 
149
150
  resolved.apiId = String(app.api_id)
150
151
  resolved.apiHash = app.api_hash
151
- console.error(`\n✓ API credentials obtained (api_id: ${app.api_id})`)
152
+ info(`\n✓ API credentials obtained (api_id: ${app.api_id})`)
152
153
  } catch (error) {
153
- console.error(`\nAuto-provisioning failed: ${error instanceof Error ? error.message : error}`)
154
- console.error('Enter your API credentials manually (from https://my.telegram.org/apps):\n')
154
+ stderrError(`\nAuto-provisioning failed: ${error instanceof Error ? error.message : error}`)
155
+ info('Enter your API credentials manually (from https://my.telegram.org/apps):\n')
155
156
  resolved.apiId = await promptText('Telegram API ID')
156
157
  resolved.apiHash = await promptHidden('Telegram API hash')
157
158
  }
@@ -388,6 +388,320 @@ describe('WebexClient', () => {
388
388
  })
389
389
  })
390
390
 
391
+ describe('internal conversation API', () => {
392
+ const TEST_DEVICE_URL = 'https://wdm-r.wbx2.com/wdm/api/v1/devices/test-device-id'
393
+ const CONV_BASE = 'https://conv-r.wbx2.com/conversation/api/v1'
394
+ const TEST_ROOM_ID = Buffer.from('ciscospark://urn:TEAM:us-west-2_r/ROOM/abc123-def456').toString('base64')
395
+ const TEST_CONV_UUID = 'abc123-def456'
396
+
397
+ const mockActivity = (text: string, overrides?: Partial<Record<string, unknown>>) => ({
398
+ id: 'activity-123',
399
+ verb: 'post',
400
+ actor: { displayName: 'Test User', emailAddress: 'test@example.com', entryUUID: 'user-uuid' },
401
+ object: { objectType: 'comment', content: text, displayName: text },
402
+ target: { id: TEST_CONV_UUID },
403
+ published: '2026-01-01T00:00:00.000Z',
404
+ ...overrides,
405
+ })
406
+
407
+ const mockConversation = (activities: ReturnType<typeof mockActivity>[]) => ({
408
+ id: TEST_CONV_UUID,
409
+ activities: { items: activities },
410
+ })
411
+
412
+ const createExtractedClient = async () => {
413
+ const client = await new WebexClient().login({ token: 'extracted-token' })
414
+ ;(client as any).deviceUrl = TEST_DEVICE_URL
415
+ ;(client as any).tokenType = 'extracted'
416
+ return client
417
+ }
418
+
419
+ describe('sendMessage', () => {
420
+ test('posts activity to /activities with POST method', async () => {
421
+ mockResponse(mockActivity('Hello world'))
422
+
423
+ const client = await createExtractedClient()
424
+ await client.sendMessage(TEST_ROOM_ID, 'Hello world')
425
+
426
+ expect(fetchCalls[0].url).toBe(`${CONV_BASE}/activities`)
427
+ expect(fetchCalls[0].options?.method).toBe('POST')
428
+ })
429
+
430
+ test('body has verb, object type, displayName, and content', async () => {
431
+ mockResponse(mockActivity('Hello world'))
432
+
433
+ const client = await createExtractedClient()
434
+ await client.sendMessage(TEST_ROOM_ID, 'Hello world')
435
+
436
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
437
+ expect(body.verb).toBe('post')
438
+ expect(body.object.objectType).toBe('comment')
439
+ expect(body.object.displayName).toBe('Hello world')
440
+ expect(body.object.content).toBe('Hello world')
441
+ })
442
+
443
+ test('body has target with decoded conv UUID and conversation type', async () => {
444
+ mockResponse(mockActivity('Hello world'))
445
+
446
+ const client = await createExtractedClient()
447
+ await client.sendMessage(TEST_ROOM_ID, 'Hello world')
448
+
449
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
450
+ expect(body.target.id).toBe(TEST_CONV_UUID)
451
+ expect(body.target.objectType).toBe('conversation')
452
+ })
453
+
454
+ test('body has clientTempId starting with tmp-', async () => {
455
+ mockResponse(mockActivity('Hello world'))
456
+
457
+ const client = await createExtractedClient()
458
+ await client.sendMessage(TEST_ROOM_ID, 'Hello world')
459
+
460
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
461
+ expect(body.clientTempId).toStartWith('tmp-')
462
+ })
463
+
464
+ test('includes cisco-device-url header', async () => {
465
+ mockResponse(mockActivity('Hello world'))
466
+
467
+ const client = await createExtractedClient()
468
+ await client.sendMessage(TEST_ROOM_ID, 'Hello world')
469
+
470
+ expect(fetchCalls[0].options?.headers).toMatchObject({
471
+ 'cisco-device-url': TEST_DEVICE_URL,
472
+ })
473
+ })
474
+
475
+ test('returns WebexMessage mapped from activity response', async () => {
476
+ mockResponse(mockActivity('Hello world'))
477
+
478
+ const client = await createExtractedClient()
479
+ const message = await client.sendMessage(TEST_ROOM_ID, 'Hello world')
480
+
481
+ expect(message.id).toBe('activity-123')
482
+ expect(message.text).toBe('Hello world')
483
+ expect(message.personEmail).toBe('test@example.com')
484
+ expect(message.created).toBe('2026-01-01T00:00:00.000Z')
485
+ })
486
+ })
487
+
488
+ describe('listMessages', () => {
489
+ test('calls GET on conversations endpoint with activitiesLimit and participantsLimit', async () => {
490
+ mockResponse(mockConversation([mockActivity('Hello')]))
491
+
492
+ const client = await createExtractedClient()
493
+ await client.listMessages(TEST_ROOM_ID)
494
+
495
+ expect(fetchCalls[0].url).toBe(
496
+ `${CONV_BASE}/conversations/${TEST_CONV_UUID}?activitiesLimit=50&participantsLimit=0`,
497
+ )
498
+ })
499
+
500
+ test('filters activities to only those with verb post', async () => {
501
+ mockResponse(
502
+ mockConversation([
503
+ mockActivity('Hello'),
504
+ { ...mockActivity('Deleted'), verb: 'delete' },
505
+ mockActivity('World'),
506
+ ]),
507
+ )
508
+
509
+ const client = await createExtractedClient()
510
+ const messages = await client.listMessages(TEST_ROOM_ID)
511
+
512
+ expect(messages).toHaveLength(2)
513
+ expect(messages[0].text).toBe('Hello')
514
+ expect(messages[1].text).toBe('World')
515
+ })
516
+
517
+ test('maps each activity to WebexMessage format', async () => {
518
+ mockResponse(mockConversation([mockActivity('Hello')]))
519
+
520
+ const client = await createExtractedClient()
521
+ const messages = await client.listMessages(TEST_ROOM_ID)
522
+
523
+ expect(messages[0].id).toBe('activity-123')
524
+ expect(messages[0].text).toBe('Hello')
525
+ expect(messages[0].personEmail).toBe('test@example.com')
526
+ expect(messages[0].created).toBe('2026-01-01T00:00:00.000Z')
527
+ })
528
+
529
+ test('passes custom max to activitiesLimit', async () => {
530
+ mockResponse(mockConversation([]))
531
+
532
+ const client = await createExtractedClient()
533
+ await client.listMessages(TEST_ROOM_ID, { max: 25 })
534
+
535
+ expect(fetchCalls[0].url).toContain('activitiesLimit=25')
536
+ })
537
+
538
+ test('includes cisco-device-url header', async () => {
539
+ mockResponse(mockConversation([]))
540
+
541
+ const client = await createExtractedClient()
542
+ await client.listMessages(TEST_ROOM_ID)
543
+
544
+ expect(fetchCalls[0].options?.headers).toMatchObject({
545
+ 'cisco-device-url': TEST_DEVICE_URL,
546
+ })
547
+ })
548
+ })
549
+
550
+ describe('getMessage', () => {
551
+ test('calls GET on activities endpoint', async () => {
552
+ mockResponse(mockActivity('Hello'))
553
+
554
+ const client = await createExtractedClient()
555
+ await client.getMessage('activity-123')
556
+
557
+ expect(fetchCalls[0].url).toBe(`${CONV_BASE}/activities/activity-123`)
558
+ })
559
+
560
+ test('maps activity to WebexMessage format', async () => {
561
+ mockResponse(mockActivity('Hello'))
562
+
563
+ const client = await createExtractedClient()
564
+ const message = await client.getMessage('activity-123')
565
+
566
+ expect(message.id).toBe('activity-123')
567
+ expect(message.text).toBe('Hello')
568
+ expect(message.personEmail).toBe('test@example.com')
569
+ })
570
+ })
571
+
572
+ describe('deleteMessage', () => {
573
+ test('first GETs the activity then POSTs a delete activity', async () => {
574
+ mockResponse(mockActivity('Hello'))
575
+ mockResponse({})
576
+
577
+ const client = await createExtractedClient()
578
+ await client.deleteMessage('activity-123')
579
+
580
+ expect(fetchCalls[0].url).toBe(`${CONV_BASE}/activities/activity-123`)
581
+ expect(fetchCalls[1].url).toBe(`${CONV_BASE}/activities`)
582
+ expect(fetchCalls[1].options?.method).toBe('POST')
583
+ })
584
+
585
+ test('delete activity body has correct verb, object, and target', async () => {
586
+ mockResponse(mockActivity('Hello'))
587
+ mockResponse({})
588
+
589
+ const client = await createExtractedClient()
590
+ await client.deleteMessage('activity-123')
591
+
592
+ const body = JSON.parse(fetchCalls[1].options?.body as string)
593
+ expect(body.verb).toBe('delete')
594
+ expect(body.object.id).toBe('activity-123')
595
+ expect(body.object.objectType).toBe('activity')
596
+ expect(body.target.id).toBe(TEST_CONV_UUID)
597
+ })
598
+
599
+ test('throws WebexError when activity has no target', async () => {
600
+ mockResponse({ ...mockActivity('Hello'), target: undefined })
601
+
602
+ const client = await createExtractedClient()
603
+ await expect(client.deleteMessage('activity-123')).rejects.toThrow(WebexError)
604
+ })
605
+ })
606
+
607
+ describe('editMessage', () => {
608
+ test('posts activity with verb post and parent edit reference', async () => {
609
+ mockResponse(mockActivity('Edited text'))
610
+
611
+ const client = await createExtractedClient()
612
+ await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
613
+
614
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
615
+ expect(body.verb).toBe('post')
616
+ expect(body.parent).toEqual({ id: 'activity-123', type: 'edit' })
617
+ })
618
+
619
+ test('body has object with comment type and new text', async () => {
620
+ mockResponse(mockActivity('Edited text'))
621
+
622
+ const client = await createExtractedClient()
623
+ await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
624
+
625
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
626
+ expect(body.object.objectType).toBe('comment')
627
+ expect(body.object.displayName).toBe('Edited text')
628
+ expect(body.object.content).toBe('Edited text')
629
+ })
630
+
631
+ test('target has decoded conv UUID', async () => {
632
+ mockResponse(mockActivity('Edited text'))
633
+
634
+ const client = await createExtractedClient()
635
+ await client.editMessage('activity-123', TEST_ROOM_ID, 'Edited text')
636
+
637
+ const body = JSON.parse(fetchCalls[0].options?.body as string)
638
+ expect(body.target.id).toBe(TEST_CONV_UUID)
639
+ })
640
+ })
641
+
642
+ describe('sendDirectMessage', () => {
643
+ test('calls public rooms and memberships API to find room, then sends via internal API', async () => {
644
+ mockResponse({ items: [{ id: TEST_ROOM_ID, title: 'DM', type: 'direct' }] })
645
+ mockResponse({
646
+ items: [{ id: 'm1', roomId: TEST_ROOM_ID, personEmail: 'target@example.com', isModerator: false }],
647
+ })
648
+ mockResponse(mockActivity('Hello'))
649
+
650
+ const client = await createExtractedClient()
651
+ const message = await client.sendDirectMessage('target@example.com', 'Hello')
652
+
653
+ expect(fetchCalls[0].url).toContain('/rooms?type=direct&max=100')
654
+ expect(fetchCalls[1].url).toContain('/memberships?roomId=')
655
+ expect(fetchCalls[2].url).toBe(`${CONV_BASE}/activities`)
656
+ expect(message.id).toBe('activity-123')
657
+ })
658
+
659
+ test('throws WebexError when no existing direct conversation found', async () => {
660
+ mockResponse({ items: [{ id: 'room-x', title: 'DM', type: 'direct' }] })
661
+ mockResponse({
662
+ items: [{ id: 'm1', roomId: 'room-x', personEmail: 'other@example.com', isModerator: false }],
663
+ })
664
+
665
+ const client = await createExtractedClient()
666
+ await expect(client.sendDirectMessage('target@example.com', 'Hello')).rejects.toThrow(WebexError)
667
+ })
668
+ })
669
+
670
+ describe('error handling', () => {
671
+ test('throws WebexError when internal API returns non-OK response', async () => {
672
+ fetchResponses.push(
673
+ new Response(JSON.stringify({ message: 'Activity not found' }), {
674
+ status: 404,
675
+ headers: { 'Content-Type': 'application/json' },
676
+ }),
677
+ )
678
+
679
+ const client = await createExtractedClient()
680
+ await expect(client.getMessage('bad-activity')).rejects.toThrow(WebexError)
681
+ })
682
+
683
+ test('error message extracted from internal API response body', async () => {
684
+ fetchResponses.push(
685
+ new Response(JSON.stringify({ message: 'Activity not found' }), {
686
+ status: 404,
687
+ headers: { 'Content-Type': 'application/json' },
688
+ }),
689
+ )
690
+
691
+ const client = await createExtractedClient()
692
+ let error: WebexError | null = null
693
+ try {
694
+ await client.getMessage('bad-activity')
695
+ } catch (err) {
696
+ error = err as WebexError
697
+ }
698
+
699
+ expect(error).toBeInstanceOf(WebexError)
700
+ expect(error?.message).toBe('Activity not found')
701
+ })
702
+ })
703
+ })
704
+
391
705
  describe('error handling', () => {
392
706
  test('throws WebexError with parsed message from response body', async () => {
393
707
  mockResponse({ message: 'The requested resource could not be found.', trackingId: 'abc' }, 404)