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
@@ -13,6 +13,8 @@ interface RateLimitBucket {
13
13
 
14
14
  export class WebexClient {
15
15
  private token: string | null = null
16
+ private deviceUrl: string | null = null
17
+ private tokenType: string | null = null
16
18
  private buckets: Map<string, RateLimitBucket> = new Map()
17
19
  private globalRateLimitUntil: number = 0
18
20
 
@@ -36,6 +38,8 @@ export class WebexClient {
36
38
  'no_credentials',
37
39
  )
38
40
  }
41
+ this.deviceUrl = config?.deviceUrl ?? null
42
+ this.tokenType = config?.tokenType ?? null
39
43
  return this.login({ token })
40
44
  }
41
45
 
@@ -175,22 +179,128 @@ export class WebexClient {
175
179
  text: string,
176
180
  options?: { markdown?: boolean },
177
181
  ): Promise<WebexMessage> {
182
+ if (this.useInternalAPI) {
183
+ return this.sendMessageInternal(roomId, text, options)
184
+ }
178
185
  const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
179
186
  return this.request<WebexMessage>('POST', '/messages', body)
180
187
  }
181
188
 
189
+ private get useInternalAPI(): boolean {
190
+ return this.tokenType === 'extracted' && this.deviceUrl !== null
191
+ }
192
+
193
+ private get convBaseUrl(): string {
194
+ const match = this.deviceUrl?.match(/wdm(-[a-z0-9]+)\.wbx2\.com/)
195
+ return `https://conv${match?.[1] ?? ''}.wbx2.com/conversation/api/v1`
196
+ }
197
+
198
+ private get internalHeaders(): Record<string, string> {
199
+ return {
200
+ Authorization: `Bearer ${this.ensureAuth()}`,
201
+ 'Content-Type': 'application/json',
202
+ 'cisco-device-url': this.deviceUrl!,
203
+ }
204
+ }
205
+
206
+ private decodeConvUuid(roomId: string): string {
207
+ return Buffer.from(roomId, 'base64').toString('utf8').split('/').pop() ?? roomId
208
+ }
209
+
210
+ private async internalRequest<T>(path: string, init?: RequestInit): Promise<T> {
211
+ const response = await fetch(`${this.convBaseUrl}${path}`, {
212
+ ...init,
213
+ headers: { ...this.internalHeaders, ...init?.headers as Record<string, string> },
214
+ })
215
+
216
+ if (!response.ok) {
217
+ const errorBody = (await response.json().catch(() => null)) as { message?: string } | null
218
+ throw new WebexError(
219
+ errorBody?.message ?? `HTTP ${response.status}`,
220
+ `http_${response.status}`,
221
+ )
222
+ }
223
+
224
+ if (response.status === 204) return undefined as T
225
+ return response.json() as Promise<T>
226
+ }
227
+
228
+ private activityToMessage(a: InternalActivity, roomId: string): WebexMessage {
229
+ return {
230
+ id: a.id,
231
+ roomId,
232
+ roomType: 'group' as const,
233
+ text: a.object?.content ?? a.object?.displayName,
234
+ personId: a.actor?.entryUUID ?? a.actor?.id ?? '',
235
+ personEmail: a.actor?.emailAddress ?? '',
236
+ created: a.published,
237
+ }
238
+ }
239
+
240
+ private async sendMessageInternal(
241
+ roomId: string,
242
+ text: string,
243
+ options?: { markdown?: boolean },
244
+ ): Promise<WebexMessage> {
245
+ 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 }
249
+ const result = await this.internalRequest<InternalActivity>('/activities', {
250
+ method: 'POST',
251
+ body: JSON.stringify({
252
+ verb: 'post',
253
+ object,
254
+ target: { id: convUuid, objectType: 'conversation' },
255
+ clientTempId: `tmp-${Date.now()}`,
256
+ }),
257
+ })
258
+ return this.activityToMessage(result, roomId)
259
+ }
260
+
182
261
  async sendDirectMessage(
183
262
  personEmail: string,
184
263
  text: string,
185
264
  options?: { markdown?: boolean },
186
265
  ): Promise<WebexMessage> {
266
+ if (this.useInternalAPI) {
267
+ const roomId = await this.findDirectRoomByEmail(personEmail)
268
+ if (!roomId) {
269
+ throw new WebexError(`No existing direct conversation with ${personEmail}`, 'not_found')
270
+ }
271
+ return this.sendMessageInternal(roomId, text, options)
272
+ }
187
273
  const body = options?.markdown
188
274
  ? { toPersonEmail: personEmail, markdown: text }
189
275
  : { toPersonEmail: personEmail, text }
190
276
  return this.request<WebexMessage>('POST', '/messages', body)
191
277
  }
192
278
 
279
+ private async findDirectRoomByEmail(email: string): Promise<string | null> {
280
+ const rooms = await this.request<{ items: WebexSpace[] }>('GET', `/rooms?type=direct&max=100`)
281
+ for (const room of rooms.items) {
282
+ const members = await this.request<{ items: WebexMembership[] }>(
283
+ 'GET',
284
+ `/memberships?roomId=${room.id}&max=10`,
285
+ )
286
+ if (members.items.some((m) => m.personEmail === email)) {
287
+ return room.id
288
+ }
289
+ }
290
+ return null
291
+ }
292
+
193
293
  async listMessages(roomId: string, options?: { max?: number }): Promise<WebexMessage[]> {
294
+ if (this.useInternalAPI) {
295
+ const convUuid = this.decodeConvUuid(roomId)
296
+ const max = options?.max ?? 50
297
+ const conv = await this.internalRequest<InternalConversation>(
298
+ `/conversations/${convUuid}?activitiesLimit=${max}&participantsLimit=0`,
299
+ )
300
+ return (conv.activities?.items ?? [])
301
+ .filter((a) => a.verb === 'post')
302
+ .map((a) => this.activityToMessage(a, roomId))
303
+ }
194
304
  const params = new URLSearchParams()
195
305
  params.set('roomId', roomId)
196
306
  params.set('max', String(options?.max ?? 50))
@@ -199,10 +309,30 @@ export class WebexClient {
199
309
  }
200
310
 
201
311
  async getMessage(messageId: string): Promise<WebexMessage> {
312
+ if (this.useInternalAPI) {
313
+ const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
314
+ const convId = activity.target?.id ?? ''
315
+ const roomId = convId ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64') : ''
316
+ return this.activityToMessage(activity, roomId)
317
+ }
202
318
  return this.request<WebexMessage>('GET', `/messages/${messageId}`)
203
319
  }
204
320
 
205
321
  async deleteMessage(messageId: string): Promise<void> {
322
+ if (this.useInternalAPI) {
323
+ const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
324
+ const convId = activity.target?.id
325
+ if (!convId) throw new WebexError('Cannot determine conversation for activity', 'internal_error')
326
+ await this.internalRequest<unknown>('/activities', {
327
+ method: 'POST',
328
+ body: JSON.stringify({
329
+ verb: 'delete',
330
+ object: { id: messageId, objectType: 'activity' },
331
+ target: { id: convId, objectType: 'conversation' },
332
+ }),
333
+ })
334
+ return
335
+ }
206
336
  return this.request<void>('DELETE', `/messages/${messageId}`)
207
337
  }
208
338
 
@@ -212,6 +342,20 @@ export class WebexClient {
212
342
  text: string,
213
343
  options?: { markdown?: boolean },
214
344
  ): Promise<WebexMessage> {
345
+ if (this.useInternalAPI) {
346
+ const convUuid = this.decodeConvUuid(roomId)
347
+ const result = await this.internalRequest<InternalActivity>('/activities', {
348
+ 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
+ }),
356
+ })
357
+ return this.activityToMessage(result, roomId)
358
+ }
215
359
  const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
216
360
  return this.request<WebexMessage>('PUT', `/messages/${messageId}`, body)
217
361
  }
@@ -245,3 +389,17 @@ export class WebexClient {
245
389
  return data.items
246
390
  }
247
391
  }
392
+
393
+ interface InternalActivity {
394
+ id: string
395
+ verb: string
396
+ actor?: { displayName?: string; emailAddress?: string; entryUUID?: string; id?: string }
397
+ object?: { content?: string; displayName?: string; objectType?: string }
398
+ target?: { id: string }
399
+ published: string
400
+ }
401
+
402
+ interface InternalConversation {
403
+ id: string
404
+ activities?: { items: InternalActivity[] }
405
+ }
@@ -2,10 +2,12 @@ import { Command } from 'commander'
2
2
 
3
3
  import { handleError } from '@/shared/utils/error-handler'
4
4
  import { formatOutput } from '@/shared/utils/output'
5
+ import { info, debug } from '@/shared/utils/stderr'
5
6
 
6
7
  import { getWebexAppCredentials } from '../app-config'
7
8
  import { WebexClient } from '../client'
8
9
  import { WebexCredentialManager } from '../credential-manager'
10
+ import { WebexTokenExtractor } from '../token-extractor'
9
11
 
10
12
  interface ResolvedCredentials {
11
13
  clientId: string
@@ -68,11 +70,11 @@ export async function loginAction(options: { token?: string; clientId?: string;
68
70
 
69
71
  const device = await credManager.requestDeviceCode(clientId)
70
72
 
71
- console.error(`Open this URL and enter the code: ${device.verificationUri}`)
72
- console.error(`Code: ${device.userCode}`)
73
- console.error('')
73
+ info(`Open this URL and enter the code: ${device.verificationUri}`)
74
+ info(`Code: ${device.userCode}`)
75
+ info('')
74
76
  await openBrowser(device.verificationUriComplete)
75
- console.error('Waiting for authorization...')
77
+ info('Waiting for authorization...')
76
78
 
77
79
  const config = await credManager.pollDeviceToken(
78
80
  device.deviceCode,
@@ -135,6 +137,60 @@ export async function statusAction(options: { pretty?: boolean }): Promise<void>
135
137
  }
136
138
  }
137
139
 
140
+ export async function extractAction(options: { pretty?: boolean; debug?: boolean }): Promise<void> {
141
+ try {
142
+ const extractor = new WebexTokenExtractor(
143
+ undefined,
144
+ options.debug ? (msg) => debug(`[debug] ${msg}`) : undefined,
145
+ )
146
+
147
+ if (options.debug) {
148
+ debug('[debug] Searching browser profiles for Webex tokens...')
149
+ }
150
+
151
+ const extracted = await extractor.extract()
152
+
153
+ if (!extracted) {
154
+ console.log(
155
+ formatOutput(
156
+ {
157
+ error: 'No Webex token found in any browser. Make sure you are logged in to web.webex.com in Chrome, Edge, Arc, or Brave.',
158
+ hint: 'Run "auth login" for OAuth Device Grant flow, or --debug for more info.',
159
+ },
160
+ options.pretty,
161
+ ),
162
+ )
163
+ process.exit(1)
164
+ return
165
+ }
166
+
167
+ const client = await new WebexClient().login({ token: extracted.accessToken })
168
+ const person = await client.testAuth()
169
+
170
+ const credManager = new WebexCredentialManager()
171
+ await credManager.saveConfig({
172
+ accessToken: extracted.accessToken,
173
+ refreshToken: extracted.refreshToken ?? '',
174
+ expiresAt: extracted.expiresAt ?? 0,
175
+ tokenType: 'extracted',
176
+ deviceUrl: extracted.deviceUrl,
177
+ })
178
+
179
+ console.log(
180
+ formatOutput(
181
+ {
182
+ user: { id: person.id, displayName: person.displayName, emails: person.emails },
183
+ authenticated: true,
184
+ tokenType: 'extracted',
185
+ },
186
+ options.pretty,
187
+ ),
188
+ )
189
+ } catch (error) {
190
+ handleError(error as Error)
191
+ }
192
+ }
193
+
138
194
  export async function logoutAction(options: { pretty?: boolean }): Promise<void> {
139
195
  try {
140
196
  const credManager = new WebexCredentialManager()
@@ -166,6 +222,13 @@ export const authCommand = new Command('auth')
166
222
  .option('--pretty', 'Pretty print JSON output')
167
223
  .action(loginAction),
168
224
  )
225
+ .addCommand(
226
+ new Command('extract')
227
+ .description('Extract Webex token from browser (Chrome, Edge, Arc, Brave)')
228
+ .option('--pretty', 'Pretty print JSON output')
229
+ .option('--debug', 'Show debug output')
230
+ .action(extractAction),
231
+ )
169
232
  .addCommand(
170
233
  new Command('status')
171
234
  .description('Show authentication status')
@@ -8,6 +8,8 @@ describe('member commands', () => {
8
8
  let consoleSpy: ReturnType<typeof spyOn>
9
9
  let consoleErrorSpy: ReturnType<typeof spyOn>
10
10
  let processExitSpy: ReturnType<typeof spyOn>
11
+ let stderrOutput: string
12
+ let origStderrWrite: typeof process.stderr.write
11
13
  const mockMembers = [
12
14
  {
13
15
  id: 'mem-1',
@@ -35,11 +37,18 @@ describe('member commands', () => {
35
37
  processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
36
38
  throw new Error(`process.exit(${_code})`)
37
39
  })
40
+ stderrOutput = ''
41
+ origStderrWrite = process.stderr.write
42
+ process.stderr.write = ((chunk: string | Uint8Array) => {
43
+ stderrOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
44
+ return true
45
+ }) as typeof process.stderr.write
38
46
  spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient() as any)
39
47
  spyOn(WebexClient.prototype, 'listMemberships').mockResolvedValue(mockMembers)
40
48
  })
41
49
 
42
50
  afterEach(() => {
51
+ process.stderr.write = origStderrWrite
43
52
  mock.restore()
44
53
  })
45
54
 
@@ -90,7 +99,7 @@ describe('member commands', () => {
90
99
 
91
100
  await expect(listAction('room-1', {})).rejects.toThrow('process.exit(1)')
92
101
 
93
- expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('No Webex credentials found'))
102
+ expect(stderrOutput).toContain('No Webex credentials found')
94
103
  })
95
104
 
96
105
  test('listAction handles API error', async () => {
@@ -97,8 +97,13 @@ test('send: with --markdown passes markdown option', async () => {
97
97
 
98
98
  test('send: not authenticated shows error', async () => {
99
99
  clientLoginSpy.mockRejectedValue(new WebexError('No Webex credentials found.', 'no_credentials'))
100
- const errorSpy = mock((_msg: string) => {})
101
- console.error = errorSpy
100
+
101
+ let stderrOutput = ''
102
+ const origWrite = process.stderr.write
103
+ process.stderr.write = ((chunk: string | Uint8Array) => {
104
+ stderrOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
105
+ return true
106
+ }) as typeof process.stderr.write
102
107
 
103
108
  const originalExit = process.exit
104
109
  process.exit = mock((_code?: number) => {
@@ -110,11 +115,10 @@ test('send: not authenticated shows error', async () => {
110
115
  } catch {
111
116
  } finally {
112
117
  process.exit = originalExit
118
+ process.stderr.write = origWrite
113
119
  }
114
120
 
115
- expect(errorSpy).toHaveBeenCalled()
116
- const output = errorSpy.mock.calls[0][0]
117
- expect(output).toContain('No Webex credentials found')
121
+ expect(stderrOutput).toContain('No Webex credentials found')
118
122
  })
119
123
 
120
124
  test('dm: calls sendDirectMessage with email and text', async () => {
@@ -6,6 +6,8 @@ import { snapshotAction } from './snapshot'
6
6
  describe('snapshot command', () => {
7
7
  let consoleSpy: ReturnType<typeof spyOn>
8
8
  let consoleErrorSpy: ReturnType<typeof spyOn>
9
+ let stderrOutput: string
10
+ let origStderrWrite: typeof process.stderr.write
9
11
 
10
12
  const mockSpaces = [
11
13
  { id: 'space-1', title: 'General', type: 'group', isLocked: false, lastActivity: '2024-01-15T00:00:00.000Z', created: '2024-01-01T00:00:00.000Z', creatorId: 'person-1' },
@@ -22,13 +24,22 @@ describe('snapshot command', () => {
22
24
  beforeEach(() => {
23
25
  consoleSpy = spyOn(console, 'log').mockImplementation(() => {})
24
26
  consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {})
27
+ stderrOutput = ''
28
+ origStderrWrite = process.stderr.write
29
+ process.stderr.write = ((chunk: string | Uint8Array) => {
30
+ stderrOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
31
+ return true
32
+ }) as typeof process.stderr.write
25
33
  spyOn(WebexClient.prototype, 'login').mockResolvedValue(new WebexClient() as any)
26
34
  spyOn(WebexClient.prototype, 'listSpaces').mockResolvedValue(mockSpaces as any)
27
35
  spyOn(WebexClient.prototype, 'listMessages').mockResolvedValue(mockMessages as any)
28
36
  spyOn(WebexClient.prototype, 'listMemberships').mockResolvedValue(mockMembers as any)
29
37
  })
30
38
 
31
- afterEach(() => { mock.restore() })
39
+ afterEach(() => {
40
+ process.stderr.write = origStderrWrite
41
+ mock.restore()
42
+ })
32
43
 
33
44
  test('full snapshot includes spaces, recent_messages, members', async () => {
34
45
  await snapshotAction({})
@@ -81,9 +92,7 @@ describe('snapshot command', () => {
81
92
  process.exit = originalExit
82
93
  }
83
94
 
84
- expect(consoleErrorSpy).toHaveBeenCalled()
85
- const output = consoleErrorSpy.mock.calls[0][0]
86
- expect(output).toContain('No Webex credentials found')
95
+ expect(stderrOutput).toContain('No Webex credentials found')
87
96
  })
88
97
 
89
98
  test('passes limit option to listMessages', async () => {
@@ -42,6 +42,8 @@ let clientGetSpaceSpy: ReturnType<typeof spyOn>
42
42
  let consoleLogSpy: ReturnType<typeof spyOn>
43
43
  let consoleErrorSpy: ReturnType<typeof spyOn>
44
44
  let processExitSpy: ReturnType<typeof spyOn>
45
+ let stderrOutput: string
46
+ let origStderrWrite: typeof process.stderr.write
45
47
 
46
48
  beforeEach(() => {
47
49
  clientLoginSpy = spyOn(WebexClient.prototype, 'login').mockResolvedValue(
@@ -58,9 +60,17 @@ beforeEach(() => {
58
60
  processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => {
59
61
  throw new Error(`process.exit(${_code})`)
60
62
  })
63
+
64
+ stderrOutput = ''
65
+ origStderrWrite = process.stderr.write
66
+ process.stderr.write = ((chunk: string | Uint8Array) => {
67
+ stderrOutput += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
68
+ return true
69
+ }) as typeof process.stderr.write
61
70
  })
62
71
 
63
72
  afterEach(() => {
73
+ process.stderr.write = origStderrWrite
64
74
  clientLoginSpy?.mockRestore()
65
75
  clientListSpacesSpy?.mockRestore()
66
76
  clientGetSpaceSpy?.mockRestore()
@@ -139,7 +149,7 @@ describe('listAction', () => {
139
149
  await expect(listAction({})).rejects.toThrow('process.exit(1)')
140
150
 
141
151
  expect(clientListSpacesSpy).not.toHaveBeenCalled()
142
- expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('No Webex credentials found'))
152
+ expect(stderrOutput).toContain('No Webex credentials found')
143
153
  })
144
154
  })
145
155
 
@@ -201,6 +211,6 @@ describe('infoAction', () => {
201
211
  await expect(infoAction('space-1', {})).rejects.toThrow('process.exit(1)')
202
212
 
203
213
  expect(clientGetSpaceSpy).not.toHaveBeenCalled()
204
- expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('No Webex credentials found'))
214
+ expect(stderrOutput).toContain('No Webex credentials found')
205
215
  })
206
216
  })
@@ -45,7 +45,7 @@ export class WebexCredentialManager {
45
45
  const config = await this.loadConfig()
46
46
  if (!config) return null
47
47
 
48
- if (config.tokenType === 'manual') {
48
+ if (config.tokenType === 'manual' || config.tokenType === 'extracted') {
49
49
  return config.accessToken
50
50
  }
51
51
 
@@ -3,11 +3,13 @@ import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
3
3
  import { WebexClient } from './client'
4
4
  import { WebexCredentialManager } from './credential-manager'
5
5
  import { ensureWebexAuth } from './ensure-auth'
6
+ import { WebexTokenExtractor } from './token-extractor'
6
7
 
7
8
  let loadConfigSpy: ReturnType<typeof spyOn>
8
9
  let getTokenSpy: ReturnType<typeof spyOn>
9
10
  let loginSpy: ReturnType<typeof spyOn>
10
11
  let testAuthSpy: ReturnType<typeof spyOn>
12
+ let extractSpy: ReturnType<typeof spyOn>
11
13
 
12
14
  beforeEach(() => {
13
15
  loadConfigSpy = spyOn(WebexCredentialManager.prototype, 'loadConfig').mockResolvedValue(null)
@@ -19,6 +21,7 @@ beforeEach(() => {
19
21
  emails: ['test@example.com'],
20
22
  type: 'person',
21
23
  })
24
+ extractSpy = spyOn(WebexTokenExtractor.prototype, 'extract').mockResolvedValue(null)
22
25
  })
23
26
 
24
27
  afterEach(() => {
@@ -26,6 +29,7 @@ afterEach(() => {
26
29
  getTokenSpy?.mockRestore()
27
30
  loginSpy?.mockRestore()
28
31
  testAuthSpy?.mockRestore()
32
+ extractSpy?.mockRestore()
29
33
  })
30
34
 
31
35
  describe('ensureWebexAuth', () => {
@@ -1,18 +1,37 @@
1
1
  import { WebexClient } from './client'
2
2
  import { WebexCredentialManager } from './credential-manager'
3
+ import { WebexTokenExtractor } from './token-extractor'
3
4
 
4
5
  export async function ensureWebexAuth(): Promise<void> {
5
6
  try {
6
7
  const credManager = new WebexCredentialManager()
7
8
  const config = await credManager.loadConfig()
8
- if (!config) return
9
9
 
10
- const token = await credManager.getToken(config.clientId, config.clientSecret)
11
- if (!token) return
10
+ if (config) {
11
+ const token = await credManager.getToken(config.clientId, config.clientSecret)
12
+ if (token) {
13
+ const client = new WebexClient()
14
+ await client.login({ token })
15
+ await client.testAuth()
16
+ return
17
+ }
18
+ }
19
+
20
+ const extractor = new WebexTokenExtractor()
21
+ const extracted = await extractor.extract()
22
+ if (!extracted) return
12
23
 
13
24
  const client = new WebexClient()
14
- await client.login({ token })
25
+ await client.login({ token: extracted.accessToken })
15
26
  await client.testAuth()
27
+
28
+ await credManager.saveConfig({
29
+ accessToken: extracted.accessToken,
30
+ refreshToken: extracted.refreshToken ?? '',
31
+ expiresAt: extracted.expiresAt ?? 0,
32
+ tokenType: 'extracted',
33
+ deviceUrl: extracted.deviceUrl,
34
+ })
16
35
  } catch {
17
36
  // Intentionally silent — best-effort preflight that should not block commands
18
37
  }
@@ -1,5 +1,7 @@
1
1
  export { WebexClient } from './client'
2
2
  export { WebexCredentialManager } from './credential-manager'
3
+ export { WebexTokenExtractor } from './token-extractor'
4
+ export type { ExtractedWebexToken } from './token-extractor'
3
5
  export { WebexError } from './types'
4
6
  export type {
5
7
  WebexConfig,