agent-messenger 2.23.1 → 2.23.2

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 (122) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/src/platforms/webex/client.d.ts +18 -0
  4. package/dist/src/platforms/webex/client.d.ts.map +1 -1
  5. package/dist/src/platforms/webex/client.js +178 -37
  6. package/dist/src/platforms/webex/client.js.map +1 -1
  7. package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -1
  8. package/dist/src/platforms/webex/commands/auth.js +10 -6
  9. package/dist/src/platforms/webex/commands/auth.js.map +1 -1
  10. package/dist/src/platforms/webex/commands/member.d.ts.map +1 -1
  11. package/dist/src/platforms/webex/commands/member.js +3 -0
  12. package/dist/src/platforms/webex/commands/member.js.map +1 -1
  13. package/dist/src/platforms/webex/commands/message.d.ts.map +1 -1
  14. package/dist/src/platforms/webex/commands/message.js +3 -0
  15. package/dist/src/platforms/webex/commands/message.js.map +1 -1
  16. package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -1
  17. package/dist/src/platforms/webex/commands/snapshot.js +3 -1
  18. package/dist/src/platforms/webex/commands/snapshot.js.map +1 -1
  19. package/dist/src/platforms/webex/commands/space.d.ts.map +1 -1
  20. package/dist/src/platforms/webex/commands/space.js +5 -0
  21. package/dist/src/platforms/webex/commands/space.js.map +1 -1
  22. package/dist/src/platforms/webex/commands/whoami.d.ts.map +1 -1
  23. package/dist/src/platforms/webex/commands/whoami.js +3 -0
  24. package/dist/src/platforms/webex/commands/whoami.js.map +1 -1
  25. package/dist/src/platforms/webex/id-normalizer.d.ts +7 -0
  26. package/dist/src/platforms/webex/id-normalizer.d.ts.map +1 -1
  27. package/dist/src/platforms/webex/id-normalizer.js +16 -0
  28. package/dist/src/platforms/webex/id-normalizer.js.map +1 -1
  29. package/dist/src/platforms/webex/index.d.ts +2 -2
  30. package/dist/src/platforms/webex/index.d.ts.map +1 -1
  31. package/dist/src/platforms/webex/index.js +1 -1
  32. package/dist/src/platforms/webex/index.js.map +1 -1
  33. package/dist/src/platforms/webexbot/client.d.ts +0 -4
  34. package/dist/src/platforms/webexbot/client.d.ts.map +1 -1
  35. package/dist/src/platforms/webexbot/client.js +8 -65
  36. package/dist/src/platforms/webexbot/client.js.map +1 -1
  37. package/dist/src/platforms/webexbot/commands/file.d.ts +2 -0
  38. package/dist/src/platforms/webexbot/commands/file.d.ts.map +1 -1
  39. package/dist/src/platforms/webexbot/commands/file.js +3 -0
  40. package/dist/src/platforms/webexbot/commands/file.js.map +1 -1
  41. package/dist/src/platforms/webexbot/commands/member.d.ts +2 -0
  42. package/dist/src/platforms/webexbot/commands/member.d.ts.map +1 -1
  43. package/dist/src/platforms/webexbot/commands/member.js +3 -0
  44. package/dist/src/platforms/webexbot/commands/member.js.map +1 -1
  45. package/dist/src/platforms/webexbot/commands/message.d.ts +4 -0
  46. package/dist/src/platforms/webexbot/commands/message.d.ts.map +1 -1
  47. package/dist/src/platforms/webexbot/commands/message.js +7 -0
  48. package/dist/src/platforms/webexbot/commands/message.js.map +1 -1
  49. package/dist/src/platforms/webexbot/commands/snapshot.d.ts +2 -0
  50. package/dist/src/platforms/webexbot/commands/snapshot.d.ts.map +1 -1
  51. package/dist/src/platforms/webexbot/commands/snapshot.js +10 -2
  52. package/dist/src/platforms/webexbot/commands/snapshot.js.map +1 -1
  53. package/dist/src/platforms/webexbot/commands/space.d.ts +4 -0
  54. package/dist/src/platforms/webexbot/commands/space.d.ts.map +1 -1
  55. package/dist/src/platforms/webexbot/commands/space.js +5 -0
  56. package/dist/src/platforms/webexbot/commands/space.js.map +1 -1
  57. package/dist/src/platforms/webexbot/commands/user.d.ts +3 -0
  58. package/dist/src/platforms/webexbot/commands/user.d.ts.map +1 -1
  59. package/dist/src/platforms/webexbot/commands/user.js +4 -0
  60. package/dist/src/platforms/webexbot/commands/user.js.map +1 -1
  61. package/dist/src/platforms/webexbot/commands/whoami.d.ts +2 -0
  62. package/dist/src/platforms/webexbot/commands/whoami.d.ts.map +1 -1
  63. package/dist/src/platforms/webexbot/commands/whoami.js +3 -0
  64. package/dist/src/platforms/webexbot/commands/whoami.js.map +1 -1
  65. package/dist/src/platforms/webexbot/index.d.ts +2 -2
  66. package/dist/src/platforms/webexbot/index.d.ts.map +1 -1
  67. package/dist/src/platforms/webexbot/index.js +1 -1
  68. package/dist/src/platforms/webexbot/index.js.map +1 -1
  69. package/dist/src/tui/adapters/types.d.ts +3 -0
  70. package/dist/src/tui/adapters/types.d.ts.map +1 -1
  71. package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -1
  72. package/dist/src/tui/adapters/webex-adapter.js +4 -0
  73. package/dist/src/tui/adapters/webex-adapter.js.map +1 -1
  74. package/docs/content/docs/cli/webex.mdx +2 -2
  75. package/package.json +1 -1
  76. package/skills/agent-channeltalk/SKILL.md +1 -1
  77. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  78. package/skills/agent-discord/SKILL.md +1 -1
  79. package/skills/agent-discordbot/SKILL.md +1 -1
  80. package/skills/agent-instagram/SKILL.md +1 -1
  81. package/skills/agent-kakaotalk/SKILL.md +1 -1
  82. package/skills/agent-line/SKILL.md +1 -1
  83. package/skills/agent-slack/SKILL.md +1 -1
  84. package/skills/agent-slackbot/SKILL.md +1 -1
  85. package/skills/agent-teams/SKILL.md +1 -1
  86. package/skills/agent-telegram/SKILL.md +1 -1
  87. package/skills/agent-telegrambot/SKILL.md +1 -1
  88. package/skills/agent-webex/SKILL.md +3 -3
  89. package/skills/agent-webexbot/SKILL.md +2 -2
  90. package/skills/agent-webexbot/references/common-patterns.md +1 -1
  91. package/skills/agent-wechatbot/SKILL.md +1 -1
  92. package/skills/agent-whatsapp/SKILL.md +1 -1
  93. package/skills/agent-whatsappbot/SKILL.md +1 -1
  94. package/src/platforms/webex/client.test.ts +94 -6
  95. package/src/platforms/webex/client.ts +194 -32
  96. package/src/platforms/webex/commands/auth.test.ts +3 -1
  97. package/src/platforms/webex/commands/auth.ts +12 -7
  98. package/src/platforms/webex/commands/member.test.ts +18 -8
  99. package/src/platforms/webex/commands/member.ts +3 -0
  100. package/src/platforms/webex/commands/message.test.ts +31 -23
  101. package/src/platforms/webex/commands/message.ts +3 -0
  102. package/src/platforms/webex/commands/snapshot.test.ts +18 -10
  103. package/src/platforms/webex/commands/snapshot.ts +3 -1
  104. package/src/platforms/webex/commands/space.test.ts +36 -17
  105. package/src/platforms/webex/commands/space.ts +5 -0
  106. package/src/platforms/webex/commands/whoami.test.ts +14 -6
  107. package/src/platforms/webex/commands/whoami.ts +3 -0
  108. package/src/platforms/webex/id-normalizer.test.ts +37 -0
  109. package/src/platforms/webex/id-normalizer.ts +21 -0
  110. package/src/platforms/webex/index.ts +2 -2
  111. package/src/platforms/webexbot/client.ts +8 -74
  112. package/src/platforms/webexbot/commands/file.ts +5 -0
  113. package/src/platforms/webexbot/commands/member.ts +5 -0
  114. package/src/platforms/webexbot/commands/message.ts +11 -0
  115. package/src/platforms/webexbot/commands/snapshot.ts +12 -2
  116. package/src/platforms/webexbot/commands/space.ts +9 -0
  117. package/src/platforms/webexbot/commands/user.test.ts +11 -5
  118. package/src/platforms/webexbot/commands/user.ts +7 -0
  119. package/src/platforms/webexbot/commands/whoami.ts +5 -0
  120. package/src/platforms/webexbot/index.ts +2 -2
  121. package/src/tui/adapters/types.ts +3 -0
  122. package/src/tui/adapters/webex-adapter.ts +4 -0
@@ -1,5 +1,6 @@
1
1
  import { WebexCredentialManager } from './credential-manager'
2
2
  import { WebexEncryptionService } from './encryption'
3
+ import { decodeWebexId, toRestId } from './id-normalizer'
3
4
  import { KmsKeyProvider } from './kms-key-provider'
4
5
  import { escapeHtml, markdownToHtml, stripMarkdown } from './markdown-to-html'
5
6
  import type { WebexConfig, WebexMembership, WebexMessage, WebexPerson, WebexSpace } from './types'
@@ -15,6 +16,10 @@ interface RateLimitBucket {
15
16
  resetAt: number
16
17
  }
17
18
 
19
+ interface WebexClientOptions {
20
+ roomResolutionWarningPrefix?: string
21
+ }
22
+
18
23
  export class WebexClient {
19
24
  private token: string | null = null
20
25
  private deviceUrl: string | null = null
@@ -22,6 +27,13 @@ export class WebexClient {
22
27
  private buckets: Map<string, RateLimitBucket> = new Map()
23
28
  private globalRateLimitUntil: number = 0
24
29
  private encryption: WebexEncryptionService | null = null
30
+ private clusteredRoomIds = new Map<string, string>()
31
+ private roomIdLookups = new Map<string, Promise<string>>()
32
+ private roomResolutionWarningPrefix: string
33
+
34
+ constructor(options: WebexClientOptions = {}) {
35
+ this.roomResolutionWarningPrefix = options.roomResolutionWarningPrefix ?? '[webex]'
36
+ }
25
37
 
26
38
  async login(credentials?: { token: string; deviceUrl?: string; tokenType?: string }): Promise<this> {
27
39
  if (credentials) {
@@ -245,8 +257,55 @@ export class WebexClient {
245
257
  }
246
258
  }
247
259
 
260
+ async resolveRoomId(roomId: string): Promise<string> {
261
+ const decoded = decodeWebexId(roomId)
262
+ let uuid: string
263
+ let fallback: string
264
+
265
+ if (decoded) {
266
+ if (decoded.type !== 'ROOM' || decoded.cluster.startsWith('urn:')) return roomId
267
+ uuid = decoded.uuid
268
+ fallback = roomId
269
+ } else if (looksLikeUuid(roomId)) {
270
+ uuid = roomId
271
+ fallback = toRestId(roomId, 'ROOM')
272
+ } else {
273
+ return roomId
274
+ }
275
+
276
+ const cached = this.clusteredRoomIds.get(uuid)
277
+ if (cached) return cached
278
+
279
+ const inFlight = this.roomIdLookups.get(uuid)
280
+ if (inFlight) return inFlight
281
+
282
+ const lookup = this.lookupRoomId(uuid, fallback)
283
+ this.roomIdLookups.set(uuid, lookup)
284
+ try {
285
+ return await lookup
286
+ } finally {
287
+ this.roomIdLookups.delete(uuid)
288
+ }
289
+ }
290
+
291
+ async resolvePersonId(personId: string): Promise<string> {
292
+ if (!personId || decodeWebexId(personId)) return personId
293
+
294
+ if (looksLikeEmail(personId)) {
295
+ const [person] = await this.listPeople({ email: personId, max: 1 })
296
+ if (!person) {
297
+ throw new WebexError(`Person not found for ref: ${personId}`, 'not_found')
298
+ }
299
+ return person.id
300
+ }
301
+
302
+ if (looksLikeUuid(personId)) return toRestId(personId, 'PEOPLE')
303
+ return personId
304
+ }
305
+
248
306
  async getSpace(spaceId: string): Promise<WebexSpace> {
249
- return this.request<WebexSpace>('GET', `/rooms/${spaceId}`)
307
+ const resolvedSpaceId = await this.resolveRoomId(spaceId)
308
+ return this.request<WebexSpace>('GET', `/rooms/${resolvedSpaceId}`)
250
309
  }
251
310
 
252
311
  async sendMessage(
@@ -254,14 +313,17 @@ export class WebexClient {
254
313
  text: string,
255
314
  options?: { markdown?: boolean; parentId?: string; files?: string[] },
256
315
  ): Promise<WebexMessage> {
316
+ const resolvedRoomId = await this.resolveRoomId(roomId)
317
+ const resolvedOptions = this.resolveMessageOptions(options)
318
+
257
319
  if (this.useInternalAPI) {
258
- return this.sendMessageInternal(roomId, text, options)
320
+ return this.sendMessageInternal(resolvedRoomId, text, resolvedOptions)
259
321
  }
260
- const body: Record<string, unknown> = { roomId }
261
- if (options?.markdown) body.markdown = text
322
+ const body: Record<string, unknown> = { roomId: resolvedRoomId }
323
+ if (resolvedOptions?.markdown) body.markdown = text
262
324
  else body.text = text
263
- if (options?.parentId) body.parentId = options.parentId
264
- if (options?.files?.length) body.files = options.files
325
+ if (resolvedOptions?.parentId) body.parentId = resolvedOptions.parentId
326
+ if (resolvedOptions?.files?.length) body.files = resolvedOptions.files
265
327
  return this.request<WebexMessage>('POST', '/messages', body)
266
328
  }
267
329
 
@@ -283,7 +345,7 @@ export class WebexClient {
283
345
  }
284
346
 
285
347
  private decodeConvUuid(roomId: string): string {
286
- return Buffer.from(roomId, 'base64').toString('utf8').split('/').pop() ?? roomId
348
+ return decodeWebexId(roomId)?.uuid ?? roomId
287
349
  }
288
350
 
289
351
  private async internalRequest<T>(path: string, init?: RequestInit): Promise<T> {
@@ -315,11 +377,11 @@ export class WebexClient {
315
377
  }
316
378
 
317
379
  return {
318
- id: a.id,
380
+ id: this.normalizeMessageId(a.id),
319
381
  roomId,
320
382
  roomType: 'group' as const,
321
383
  text,
322
- personId: a.actor?.entryUUID ?? a.actor?.id ?? '',
384
+ personId: this.normalizePersonId(a.actor?.entryUUID ?? a.actor?.id ?? ''),
323
385
  personEmail: a.actor?.emailAddress ?? '',
324
386
  created: a.published,
325
387
  }
@@ -425,27 +487,31 @@ export class WebexClient {
425
487
  roomId: string,
426
488
  options?: { max?: number; mentionedPeople?: string; parentId?: string },
427
489
  ): Promise<WebexMessage[]> {
490
+ const resolvedRoomId = await this.resolveRoomId(roomId)
491
+ const resolvedOptions = await this.resolveListMessageOptions(options)
492
+
428
493
  if (this.useInternalAPI) {
429
- const convUuid = this.decodeConvUuid(roomId)
430
- const max = options?.max ?? 50
494
+ const convUuid = this.decodeConvUuid(resolvedRoomId)
495
+ const max = resolvedOptions?.max ?? 50
431
496
  const conv = await this.internalRequest<InternalConversation>(
432
497
  `/conversations/${convUuid}?activitiesLimit=${max}&participantsLimit=0`,
433
498
  )
434
499
  const activities = (conv.activities?.items ?? []).filter((a) => a.verb === 'post')
435
- return Promise.all(activities.map((a) => this.activityToMessage(a, roomId)))
500
+ return Promise.all(activities.map((a) => this.activityToMessage(a, resolvedRoomId)))
436
501
  }
437
502
  const params = new URLSearchParams()
438
- params.set('roomId', roomId)
439
- params.set('max', String(options?.max ?? 50))
440
- if (options?.mentionedPeople) params.set('mentionedPeople', options.mentionedPeople)
441
- if (options?.parentId) params.set('parentId', options.parentId)
503
+ params.set('roomId', resolvedRoomId)
504
+ params.set('max', String(resolvedOptions?.max ?? 50))
505
+ if (resolvedOptions?.mentionedPeople) params.set('mentionedPeople', resolvedOptions.mentionedPeople)
506
+ if (resolvedOptions?.parentId) params.set('parentId', resolvedOptions.parentId)
442
507
  const data = await this.request<{ items: WebexMessage[] }>('GET', `/messages?${params}`)
443
508
  return data.items
444
509
  }
445
510
 
446
511
  async getMessage(messageId: string): Promise<WebexMessage> {
447
512
  if (this.useInternalAPI) {
448
- const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
513
+ const activityId = this.toMessageRef(messageId)
514
+ const activity = await this.internalRequest<InternalActivity>(`/activities/${activityId}`)
449
515
  const convId = activity.target?.id ?? ''
450
516
  // Internal API responses don't carry the cluster shard (e.g. `us-west-2_r`) the
451
517
  // public roomId encoding requires. The `unknown` placeholder is a sentinel — it
@@ -455,25 +521,26 @@ export class WebexClient {
455
521
  const roomId = convId ? Buffer.from(`ciscospark://urn:TEAM:unknown/ROOM/${convId}`).toString('base64') : ''
456
522
  return this.activityToMessage(activity, roomId)
457
523
  }
458
- return this.request<WebexMessage>('GET', `/messages/${messageId}`)
524
+ return this.request<WebexMessage>('GET', `/messages/${this.resolveMessageId(messageId)}`)
459
525
  }
460
526
 
461
527
  async deleteMessage(messageId: string): Promise<void> {
462
528
  if (this.useInternalAPI) {
463
- const activity = await this.internalRequest<InternalActivity>(`/activities/${messageId}`)
529
+ const activityId = this.toMessageRef(messageId)
530
+ const activity = await this.internalRequest<InternalActivity>(`/activities/${activityId}`)
464
531
  const convId = activity.target?.id
465
532
  if (!convId) throw new WebexError('Cannot determine conversation for activity', 'internal_error')
466
533
  await this.internalRequest<unknown>('/activities', {
467
534
  method: 'POST',
468
535
  body: JSON.stringify({
469
536
  verb: 'delete',
470
- object: { id: messageId, objectType: 'activity' },
537
+ object: { id: activityId, objectType: 'activity' },
471
538
  target: { id: convId, objectType: 'conversation' },
472
539
  }),
473
540
  })
474
541
  return
475
542
  }
476
- return this.request<void>('DELETE', `/messages/${messageId}`)
543
+ return this.request<void>('DELETE', `/messages/${this.resolveMessageId(messageId)}`)
477
544
  }
478
545
 
479
546
  async editMessage(
@@ -482,8 +549,11 @@ export class WebexClient {
482
549
  text: string,
483
550
  options?: { markdown?: boolean },
484
551
  ): Promise<WebexMessage> {
552
+ const resolvedRoomId = await this.resolveRoomId(roomId)
553
+
485
554
  if (this.useInternalAPI) {
486
- const convUuid = this.decodeConvUuid(roomId)
555
+ const activityId = this.toMessageRef(messageId)
556
+ const convUuid = this.decodeConvUuid(resolvedRoomId)
487
557
  const { object, encryptionKeyUrl } = await this.buildEncryptedObject(convUuid, text, {
488
558
  ...options,
489
559
  forEdit: true,
@@ -493,7 +563,7 @@ export class WebexClient {
493
563
  verb: 'post',
494
564
  object,
495
565
  target: { id: convUuid, objectType: 'conversation' },
496
- parent: { id: messageId, type: 'edit' },
566
+ parent: { id: activityId, type: 'edit' },
497
567
  clientTempId: `tmp-${Date.now()}-edit`,
498
568
  }
499
569
 
@@ -508,17 +578,17 @@ export class WebexClient {
508
578
 
509
579
  // Tolerate responses that omit `parent` (server may return minimal shape) —
510
580
  // only fail on an explicit mismatch between the echoed parent and the edited id.
511
- if (result.parent && result.parent.id !== messageId) {
581
+ if (result.parent && result.parent.id !== activityId) {
512
582
  throw new WebexError(
513
- `Edit rejected: server linked the new activity ${result.id} to ${result.parent.id} instead of ${messageId}.`,
583
+ `Edit rejected: server linked the new activity ${result.id} to ${result.parent.id} instead of ${activityId}.`,
514
584
  'edit_failed',
515
585
  )
516
586
  }
517
587
 
518
- return this.activityToMessage(result, roomId)
588
+ return this.activityToMessage(result, resolvedRoomId)
519
589
  }
520
- const body = options?.markdown ? { roomId, markdown: text } : { roomId, text }
521
- return this.request<WebexMessage>('PUT', `/messages/${messageId}`, body)
590
+ const body = options?.markdown ? { roomId: resolvedRoomId, markdown: text } : { roomId: resolvedRoomId, text }
591
+ return this.request<WebexMessage>('PUT', `/messages/${this.resolveMessageId(messageId)}`, body)
522
592
  }
523
593
 
524
594
  async listPeople(options?: { email?: string; displayName?: string; max?: number }): Promise<WebexPerson[]> {
@@ -533,7 +603,14 @@ export class WebexClient {
533
603
  }
534
604
 
535
605
  async getPerson(personId: string): Promise<WebexPerson> {
536
- return this.request<WebexPerson>('GET', `/people/${personId}`)
606
+ if (!decodeWebexId(personId) && looksLikeEmail(personId)) {
607
+ const [person] = await this.listPeople({ email: personId, max: 1 })
608
+ if (!person) {
609
+ throw new WebexError(`Person not found for ref: ${personId}`, 'not_found')
610
+ }
611
+ return person
612
+ }
613
+ return this.request<WebexPerson>('GET', `/people/${await this.resolvePersonId(personId)}`)
537
614
  }
538
615
 
539
616
  async listMyMemberships(options?: { max?: number }): Promise<WebexMembership[]> {
@@ -544,8 +621,9 @@ export class WebexClient {
544
621
  }
545
622
 
546
623
  async listMemberships(roomId: string, options?: { max?: number }): Promise<WebexMembership[]> {
624
+ const resolvedRoomId = await this.resolveRoomId(roomId)
547
625
  const params = new URLSearchParams()
548
- params.set('roomId', roomId)
626
+ params.set('roomId', resolvedRoomId)
549
627
  if (options?.max) params.set('max', String(options.max))
550
628
  const data = await this.request<{ items: WebexMembership[] }>('GET', `/memberships?${params}`)
551
629
  return data.items
@@ -556,12 +634,14 @@ export class WebexClient {
556
634
  file: { content: Blob; filename: string },
557
635
  options?: { text?: string; markdown?: boolean; parentId?: string },
558
636
  ): Promise<WebexMessage> {
637
+ const resolvedRoomId = await this.resolveRoomId(roomId)
638
+ const resolvedParentId = options?.parentId ? this.resolveMessageId(options.parentId) : undefined
559
639
  const form = new FormData()
560
- form.set('roomId', roomId)
640
+ form.set('roomId', resolvedRoomId)
561
641
  if (options?.text) {
562
642
  form.set(options.markdown ? 'markdown' : 'text', options.text)
563
643
  }
564
- if (options?.parentId) form.set('parentId', options.parentId)
644
+ if (resolvedParentId) form.set('parentId', resolvedParentId)
565
645
  form.set('files', file.content, file.filename)
566
646
 
567
647
  const response = await fetch(`${BASE_URL}/messages`, {
@@ -577,6 +657,80 @@ export class WebexClient {
577
657
  return response.json() as Promise<WebexMessage>
578
658
  }
579
659
 
660
+ private async lookupRoomId(uuid: string, fallback: string): Promise<string> {
661
+ try {
662
+ // Page through every room the account belongs to, stopping as soon as the
663
+ // trailing UUID matches because room titles are not stable identifiers.
664
+ for await (const room of this.iterateSpaces({ max: 1000 })) {
665
+ if (decodeWebexId(room.id)?.uuid === uuid) {
666
+ this.clusteredRoomIds.set(uuid, room.id)
667
+ return room.id
668
+ }
669
+ }
670
+ } catch {
671
+ // Network/auth failure: fail open to the un-corrected id rather than block the call.
672
+ return fallback
673
+ }
674
+
675
+ console.warn(
676
+ `${this.roomResolutionWarningPrefix} Could not resolve clustered room id for ${uuid}; falling back to the un-clustered id. ` +
677
+ 'Room-scoped calls may fail if this room lives on a non-default Webex cluster.',
678
+ )
679
+ return fallback
680
+ }
681
+
682
+ private resolveMessageOptions(options?: {
683
+ markdown?: boolean
684
+ parentId?: string
685
+ files?: string[]
686
+ }): { markdown?: boolean; parentId?: string; files?: string[] } | undefined {
687
+ if (!options?.parentId) return options
688
+ return { ...options, parentId: this.resolveMessageId(options.parentId) }
689
+ }
690
+
691
+ private async resolveListMessageOptions(options?: {
692
+ max?: number
693
+ mentionedPeople?: string
694
+ parentId?: string
695
+ }): Promise<{ max?: number; mentionedPeople?: string; parentId?: string } | undefined> {
696
+ if (!options) return undefined
697
+ const resolved = { ...options }
698
+ if (options.mentionedPeople) {
699
+ resolved.mentionedPeople = await this.resolveMentionedPeople(options.mentionedPeople)
700
+ }
701
+ if (options.parentId) {
702
+ resolved.parentId = this.resolveMessageId(options.parentId)
703
+ }
704
+ return resolved
705
+ }
706
+
707
+ private async resolveMentionedPeople(mentionedPeople: string): Promise<string> {
708
+ if (mentionedPeople === 'me') return mentionedPeople
709
+ return this.resolvePersonId(mentionedPeople)
710
+ }
711
+
712
+ private resolveMessageId(messageId: string): string {
713
+ if (!messageId || decodeWebexId(messageId)) return messageId
714
+ // A lone message UUID does not identify its room cluster, so cluster correction
715
+ // is not possible without the room context.
716
+ if (looksLikeUuid(messageId)) return toRestId(messageId, 'MESSAGE')
717
+ return messageId
718
+ }
719
+
720
+ private toMessageRef(messageId: string): string {
721
+ return decodeWebexId(messageId)?.uuid ?? messageId
722
+ }
723
+
724
+ private normalizeMessageId(messageId: string): string {
725
+ if (!messageId || decodeWebexId(messageId)) return messageId
726
+ return toRestId(messageId, 'MESSAGE')
727
+ }
728
+
729
+ private normalizePersonId(personId: string): string {
730
+ if (!personId || decodeWebexId(personId)) return personId
731
+ return toRestId(personId, 'PEOPLE')
732
+ }
733
+
580
734
  async downloadContent(contentRef: string): Promise<{ data: ArrayBuffer; filename: string; contentType: string }> {
581
735
  const url = this.resolveContentUrl(contentRef)
582
736
  const response = await fetch(url, {
@@ -638,6 +792,14 @@ function sanitizeFilename(name: string | undefined): string | undefined {
638
792
  return base
639
793
  }
640
794
 
795
+ function looksLikeUuid(value: string): boolean {
796
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
797
+ }
798
+
799
+ function looksLikeEmail(value: string): boolean {
800
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
801
+ }
802
+
641
803
  interface InternalActivity {
642
804
  id: string
643
805
  verb: string
@@ -9,6 +9,8 @@ import { WebexTokenExtractor } from '../token-extractor'
9
9
  import { WebexError } from '../types'
10
10
  import { extractAction, loginAction, logoutAction, oauthAction, statusAction } from './auth'
11
11
 
12
+ const personId = Buffer.from('ciscospark://us/PEOPLE/person-1').toString('base64url')
13
+
12
14
  let promptQueue: string[] = []
13
15
  mock.module('node:readline/promises', () => ({
14
16
  createInterface: () => ({
@@ -33,7 +35,7 @@ describe('auth commands', () => {
33
35
  let originalStdinTTY: boolean | undefined
34
36
  let originalStdoutTTY: boolean | undefined
35
37
  const mockPerson = {
36
- id: 'person-1',
38
+ id: personId,
37
39
  displayName: 'Test User',
38
40
  emails: ['test@example.com'],
39
41
  orgId: 'org-1',
@@ -12,6 +12,7 @@ import { info, debug, error as stderrError } from '@/shared/utils/stderr'
12
12
  import { getWebexAppCredentials } from '../app-config'
13
13
  import { WebexClient } from '../client'
14
14
  import { WebexCredentialManager } from '../credential-manager'
15
+ import { toRef } from '../id-normalizer'
15
16
  import { loginWithPassword, WEB_CLIENT_ID, WEB_CLIENT_SECRET } from '../password-login'
16
17
  import { WebexTokenExtractor } from '../token-extractor'
17
18
  import { WebexError } from '../types'
@@ -21,6 +22,10 @@ interface ResolvedCredentials {
21
22
  clientSecret: string
22
23
  }
23
24
 
25
+ function formatAuthUser(person: { id: string; displayName: string; emails: string[] }) {
26
+ return { id: person.id, ref: toRef(person.id), displayName: person.displayName, emails: person.emails }
27
+ }
28
+
24
29
  async function openBrowser(url: string): Promise<void> {
25
30
  const { exec } = await import('node:child_process')
26
31
  const command =
@@ -170,7 +175,7 @@ async function loginWithToken(credManager: WebexCredentialManager, token: string
170
175
  console.log(
171
176
  formatOutput(
172
177
  {
173
- user: { id: person.id, displayName: person.displayName, emails: person.emails },
178
+ user: formatAuthUser(person),
174
179
  authenticated: true,
175
180
  },
176
181
  pretty,
@@ -205,7 +210,7 @@ async function loginWithEmailPassword(
205
210
  formatOutput(
206
211
  {
207
212
  authenticated: true,
208
- user: { id: person.id, displayName: person.displayName, emails: person.emails },
213
+ user: formatAuthUser(person),
209
214
  },
210
215
  options.pretty,
211
216
  ),
@@ -259,7 +264,7 @@ export async function oauthAction(options: OAuthOptions): Promise<void> {
259
264
  console.log(
260
265
  formatOutput(
261
266
  {
262
- user: { id: person.id, displayName: person.displayName, emails: person.emails },
267
+ user: formatAuthUser(person),
263
268
  authenticated: true,
264
269
  },
265
270
  options.pretty,
@@ -308,7 +313,7 @@ async function finishDeviceGrant(credManager: WebexCredentialManager, options: O
308
313
  formatOutput(
309
314
  {
310
315
  authenticated: true,
311
- user: { id: person.id, displayName: person.displayName, emails: person.emails },
316
+ user: formatAuthUser(person),
312
317
  },
313
318
  options.pretty,
314
319
  ),
@@ -364,7 +369,7 @@ export async function statusAction(options: { pretty?: boolean }): Promise<void>
364
369
  formatOutput(
365
370
  {
366
371
  authenticated: true,
367
- user: { id: person.id, displayName: person.displayName, emails: person.emails },
372
+ user: formatAuthUser(person),
368
373
  },
369
374
  options.pretty,
370
375
  ),
@@ -436,11 +441,11 @@ export async function extractAction(options: {
436
441
  tokenType: 'extracted',
437
442
  })
438
443
 
439
- let person: { id: string; displayName: string; emails: string[] } | null = null
444
+ let person: ReturnType<typeof formatAuthUser> | null = null
440
445
  try {
441
446
  const result = await client.testAuth()
442
447
  if (result.id) {
443
- person = { id: result.id, displayName: result.displayName, emails: result.emails }
448
+ person = formatAuthUser(result)
444
449
  }
445
450
  } catch (authError) {
446
451
  const isAuthFailure =
@@ -3,20 +3,26 @@ import { afterEach, beforeEach, describe, expect, spyOn, it } from 'bun:test'
3
3
  import { WebexClient } from '../client'
4
4
  import { WebexError } from '../types'
5
5
 
6
+ const restId = (type: string, ref: string) => Buffer.from(`ciscospark://us/${type}/${ref}`).toString('base64url')
7
+ const member1Id = restId('MEMBERSHIP', 'person-1:room-1')
8
+ const member2Id = restId('MEMBERSHIP', 'person-2:room-1')
9
+ const person1Id = restId('PEOPLE', 'person-1')
10
+ const person2Id = restId('PEOPLE', 'person-2')
11
+
6
12
  const mockMembers = [
7
13
  {
8
- id: 'mem-1',
14
+ id: member1Id,
9
15
  roomId: 'room-1',
10
- personId: 'person-1',
16
+ personId: person1Id,
11
17
  personEmail: 'alice@example.com',
12
18
  personDisplayName: 'Alice',
13
19
  isModerator: true,
14
20
  created: '2024-01-01T00:00:00.000Z',
15
21
  },
16
22
  {
17
- id: 'mem-2',
23
+ id: member2Id,
18
24
  roomId: 'room-1',
19
- personId: 'person-2',
25
+ personId: person2Id,
20
26
  personEmail: 'bob@example.com',
21
27
  personDisplayName: 'Bob',
22
28
  isModerator: false,
@@ -66,16 +72,20 @@ describe('member commands', () => {
66
72
  expect(consoleSpy).toHaveBeenCalledWith(
67
73
  JSON.stringify([
68
74
  {
69
- id: 'mem-1',
70
- personId: 'person-1',
75
+ id: member1Id,
76
+ ref: 'person-1:room-1',
77
+ personId: person1Id,
78
+ personRef: 'person-1',
71
79
  personEmail: 'alice@example.com',
72
80
  personDisplayName: 'Alice',
73
81
  isModerator: true,
74
82
  created: '2024-01-01T00:00:00.000Z',
75
83
  },
76
84
  {
77
- id: 'mem-2',
78
- personId: 'person-2',
85
+ id: member2Id,
86
+ ref: 'person-2:room-1',
87
+ personId: person2Id,
88
+ personRef: 'person-2',
79
89
  personEmail: 'bob@example.com',
80
90
  personDisplayName: 'Bob',
81
91
  isModerator: false,
@@ -4,6 +4,7 @@ import { handleError } from '@/shared/utils/error-handler'
4
4
  import { formatOutput } from '@/shared/utils/output'
5
5
 
6
6
  import { WebexClient } from '../client'
7
+ import { toRef } from '../id-normalizer'
7
8
 
8
9
  export async function listAction(spaceId: string, options: { limit?: number; pretty?: boolean }): Promise<void> {
9
10
  try {
@@ -12,7 +13,9 @@ export async function listAction(spaceId: string, options: { limit?: number; pre
12
13
 
13
14
  const output = members.map((m) => ({
14
15
  id: m.id,
16
+ ref: toRef(m.id),
15
17
  personId: m.personId,
18
+ personRef: toRef(m.personId),
16
19
  personEmail: m.personEmail,
17
20
  personDisplayName: m.personDisplayName,
18
21
  isModerator: m.isModerator,
@@ -1,26 +1,32 @@
1
1
  import { afterEach, beforeEach, expect, spyOn, it } from 'bun:test'
2
2
 
3
3
  import { WebexClient } from '../client'
4
+ import { toRestId } from '../id-normalizer'
4
5
  import { WebexError } from '../types'
5
6
 
7
+ const messageId = toRestId('msg_123', 'MESSAGE')
8
+ const message2Id = toRestId('msg_124', 'MESSAGE')
9
+ const roomId = toRestId('space_456', 'ROOM')
10
+ const personId = toRestId('person_789', 'PEOPLE')
11
+
6
12
  const mockMessage = {
7
- id: 'msg_123',
8
- roomId: 'space_456',
13
+ id: messageId,
14
+ roomId,
9
15
  roomType: 'group' as const,
10
16
  text: 'Hello world',
11
17
  html: '<p>Hello <a href="https://example.com">world</a></p>',
12
- personId: 'person_789',
18
+ personId,
13
19
  personEmail: 'user@example.com',
14
20
  created: '2025-01-29T10:00:00.000Z',
15
21
  }
16
22
 
17
23
  const mockMessage2 = {
18
- id: 'msg_124',
19
- roomId: 'space_456',
24
+ id: message2Id,
25
+ roomId,
20
26
  roomType: 'group' as const,
21
27
  text: 'Second message',
22
28
  html: '<p>Second message</p>',
23
- personId: 'person_789',
29
+ personId,
24
30
  personEmail: 'user@example.com',
25
31
  created: '2025-01-29T10:01:00.000Z',
26
32
  }
@@ -78,10 +84,12 @@ it('calls sendMessage with correct args and outputs result', async () => {
78
84
  markdown: undefined,
79
85
  })
80
86
  expect(consoleLogSpy).toHaveBeenCalled()
81
- const output = consoleLogSpy.mock.calls[0][0]
82
- expect(output).toContain('msg_123')
83
- expect(output).toContain('space_456')
84
- expect(output).toContain('user@example.com')
87
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
88
+ expect(output.id).toBe(messageId)
89
+ expect(output.ref).toBe('msg_123')
90
+ expect(output.roomId).toBe(roomId)
91
+ expect(output.roomRef).toBe('space_456')
92
+ expect(output.personEmail).toBe('user@example.com')
85
93
  expect(mockDispose).toHaveBeenCalled()
86
94
  })
87
95
 
@@ -115,8 +123,8 @@ it('calls sendDirectMessage with email and text', async () => {
115
123
  markdown: undefined,
116
124
  })
117
125
  expect(consoleLogSpy).toHaveBeenCalled()
118
- const output = consoleLogSpy.mock.calls[0][0]
119
- expect(output).toContain('msg_123')
126
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
127
+ expect(output.ref).toBe('msg_123')
120
128
  })
121
129
 
122
130
  it('passes markdown option to sendDirectMessage when --markdown flag is set', async () => {
@@ -132,10 +140,10 @@ it('calls listMessages with limit and outputs array', async () => {
132
140
 
133
141
  expect(mockListMessages).toHaveBeenCalledWith('space_456', { max: 50 })
134
142
  expect(consoleLogSpy).toHaveBeenCalled()
135
- const output = consoleLogSpy.mock.calls[0][0]
136
- expect(output).toContain('msg_123')
137
- expect(output).toContain('msg_124')
138
- expect(output).toContain('https://example.com')
143
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
144
+ expect(output[0].ref).toBe('msg_123')
145
+ expect(output[1].ref).toBe('msg_124')
146
+ expect(output[0].html).toContain('https://example.com')
139
147
  })
140
148
 
141
149
  it('calls getMessage with correct id and outputs result', async () => {
@@ -143,10 +151,10 @@ it('calls getMessage with correct id and outputs result', async () => {
143
151
 
144
152
  expect(mockGetMessage).toHaveBeenCalledWith('msg_123')
145
153
  expect(consoleLogSpy).toHaveBeenCalled()
146
- const output = consoleLogSpy.mock.calls[0][0]
147
- expect(output).toContain('msg_123')
148
- expect(output).toContain('user@example.com')
149
- expect(output).toContain('https://example.com')
154
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
155
+ expect(output.ref).toBe('msg_123')
156
+ expect(output.personEmail).toBe('user@example.com')
157
+ expect(output.html).toContain('https://example.com')
150
158
  })
151
159
 
152
160
  it('calls deleteMessage and outputs deleted id when --force flag is set', async () => {
@@ -178,9 +186,9 @@ it('calls editMessage with roomId in args and outputs result', async () => {
178
186
  markdown: undefined,
179
187
  })
180
188
  expect(consoleLogSpy).toHaveBeenCalled()
181
- const output = consoleLogSpy.mock.calls[0][0]
182
- expect(output).toContain('msg_123')
183
- expect(output).toContain('Updated message')
189
+ const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string)
190
+ expect(output.ref).toBe('msg_123')
191
+ expect(output.text).toBe('Updated message')
184
192
  expect(mockDispose).toHaveBeenCalled()
185
193
  })
186
194