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
@@ -4,12 +4,15 @@ 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
  import type { WebexMessage } from '../types'
8
9
 
9
10
  function formatMessageOutput(message: WebexMessage) {
10
11
  return {
11
12
  id: message.id,
13
+ ref: toRef(message.id),
12
14
  roomId: message.roomId,
15
+ roomRef: toRef(message.roomId),
13
16
  text: message.text,
14
17
  html: message.html,
15
18
  personEmail: message.personEmail,
@@ -3,32 +3,38 @@ 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 space1Id = restId('ROOM', 'space-1')
8
+ const space2Id = restId('ROOM', 'space-2')
9
+ const member1Id = restId('MEMBERSHIP', 'person-1:space-1')
10
+ const person1Id = restId('PEOPLE', 'person-1')
11
+
6
12
  const mockSpaces = [
7
13
  {
8
- id: 'space-1',
14
+ id: space1Id,
9
15
  title: 'General',
10
16
  type: 'group',
11
17
  isLocked: false,
12
18
  lastActivity: '2024-01-15T00:00:00.000Z',
13
19
  created: '2024-01-01T00:00:00.000Z',
14
- creatorId: 'person-1',
20
+ creatorId: person1Id,
15
21
  },
16
22
  {
17
- id: 'space-2',
23
+ id: space2Id,
18
24
  title: 'Random',
19
25
  type: 'group',
20
26
  isLocked: false,
21
27
  lastActivity: '2024-01-14T00:00:00.000Z',
22
28
  created: '2024-01-01T00:00:00.000Z',
23
- creatorId: 'person-1',
29
+ creatorId: person1Id,
24
30
  },
25
31
  ]
26
32
 
27
33
  const mockMyMemberships = [
28
34
  {
29
- id: 'mem-1',
30
- roomId: 'space-1',
31
- personId: 'person-1',
35
+ id: member1Id,
36
+ roomId: space1Id,
37
+ personId: person1Id,
32
38
  personEmail: 'alice@example.com',
33
39
  personDisplayName: 'Alice',
34
40
  isModerator: true,
@@ -77,7 +83,8 @@ describe('snapshot command', () => {
77
83
  expect(consoleSpy).toHaveBeenCalled()
78
84
  const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
79
85
  expect(output.spaces).toHaveLength(1)
80
- expect(output.spaces[0].id).toBe('space-1')
86
+ expect(output.spaces[0].id).toBe(space1Id)
87
+ expect(output.spaces[0].ref).toBe('space-1')
81
88
  expect(output.spaces[0].title).toBe('General')
82
89
  expect(output.spaces[0].type).toBeUndefined()
83
90
  expect(output.hint).toBeDefined()
@@ -89,7 +96,8 @@ describe('snapshot command', () => {
89
96
  expect(consoleSpy).toHaveBeenCalled()
90
97
  const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
91
98
  expect(output.spaces).toHaveLength(1)
92
- expect(output.spaces[0].id).toBe('space-1')
99
+ expect(output.spaces[0].id).toBe(space1Id)
100
+ expect(output.spaces[0].ref).toBe('space-1')
93
101
  expect(output.spaces[0].title).toBe('General')
94
102
  expect(output.spaces[0].type).toBe('group')
95
103
  expect(output.spaces[0].lastActivity).toBe('2024-01-15T00:00:00.000Z')
@@ -101,7 +109,7 @@ describe('snapshot command', () => {
101
109
 
102
110
  const output = JSON.parse(consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0])
103
111
  expect(output.spaces).toHaveLength(1)
104
- expect(output.spaces[0].id).toBe('space-1')
112
+ expect(output.spaces[0].id).toBe(space1Id)
105
113
  })
106
114
 
107
115
  it('exits with code 1 when not authenticated', async () => {
@@ -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 snapshotAction(options: { full?: boolean; pretty?: boolean }): Promise<void> {
9
10
  try {
@@ -19,11 +20,12 @@ export async function snapshotAction(options: { full?: boolean; pretty?: boolean
19
20
  spaces: options.full
20
21
  ? spaces.map((s) => ({
21
22
  id: s.id,
23
+ ref: toRef(s.id),
22
24
  title: s.title,
23
25
  type: s.type,
24
26
  lastActivity: s.lastActivity,
25
27
  }))
26
- : spaces.map((s) => ({ id: s.id, title: s.title })),
28
+ : spaces.map((s) => ({ id: s.id, ref: toRef(s.id), title: s.title })),
27
29
  }
28
30
 
29
31
  if (!options.full) {
@@ -3,36 +3,43 @@ 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 space1Id = restId('ROOM', 'space-1')
8
+ const space2Id = restId('ROOM', 'space-2')
9
+ const person1Id = restId('PEOPLE', 'person-1')
10
+ const person2Id = restId('PEOPLE', 'person-2')
11
+ const teamId = restId('TEAM', 'team-abc')
12
+
6
13
  const mockSpaces = [
7
14
  {
8
- id: 'space-1',
15
+ id: space1Id,
9
16
  title: 'General',
10
17
  type: 'group' as const,
11
18
  isLocked: false,
12
19
  lastActivity: '2024-01-02T00:00:00.000Z',
13
20
  created: '2024-01-01T00:00:00.000Z',
14
- creatorId: 'person-1',
21
+ creatorId: person1Id,
15
22
  },
16
23
  {
17
- id: 'space-2',
24
+ id: space2Id,
18
25
  title: 'Direct with Alice',
19
26
  type: 'direct' as const,
20
27
  isLocked: false,
21
28
  lastActivity: '2024-01-03T00:00:00.000Z',
22
29
  created: '2024-01-01T00:00:00.000Z',
23
- creatorId: 'person-2',
30
+ creatorId: person2Id,
24
31
  },
25
32
  ]
26
33
 
27
34
  const mockSpace = {
28
- id: 'space-1',
35
+ id: space1Id,
29
36
  title: 'General',
30
37
  type: 'group' as const,
31
38
  isLocked: false,
32
- teamId: 'team-abc',
39
+ teamId,
33
40
  lastActivity: '2024-01-02T00:00:00.000Z',
34
41
  created: '2024-01-01T00:00:00.000Z',
35
- creatorId: 'person-1',
42
+ creatorId: person1Id,
36
43
  }
37
44
 
38
45
  import { infoAction, listAction } from './space'
@@ -79,14 +86,16 @@ describe('listAction', () => {
79
86
  expect(consoleLogSpy).toHaveBeenCalledWith(
80
87
  JSON.stringify([
81
88
  {
82
- id: 'space-1',
89
+ id: space1Id,
90
+ ref: 'space-1',
83
91
  title: 'General',
84
92
  type: 'group',
85
93
  lastActivity: '2024-01-02T00:00:00.000Z',
86
94
  created: '2024-01-01T00:00:00.000Z',
87
95
  },
88
96
  {
89
- id: 'space-2',
97
+ id: space2Id,
98
+ ref: 'space-2',
90
99
  title: 'Direct with Alice',
91
100
  type: 'direct',
92
101
  lastActivity: '2024-01-03T00:00:00.000Z',
@@ -115,14 +124,16 @@ describe('listAction', () => {
115
124
  JSON.stringify(
116
125
  [
117
126
  {
118
- id: 'space-1',
127
+ id: space1Id,
128
+ ref: 'space-1',
119
129
  title: 'General',
120
130
  type: 'group',
121
131
  lastActivity: '2024-01-02T00:00:00.000Z',
122
132
  created: '2024-01-01T00:00:00.000Z',
123
133
  },
124
134
  {
125
- id: 'space-2',
135
+ id: space2Id,
136
+ ref: 'space-2',
126
137
  title: 'Direct with Alice',
127
138
  type: 'direct',
128
139
  lastActivity: '2024-01-03T00:00:00.000Z',
@@ -152,14 +163,17 @@ describe('infoAction', () => {
152
163
  expect(mockGetSpace).toHaveBeenCalledWith('space-1')
153
164
  expect(consoleLogSpy).toHaveBeenCalledWith(
154
165
  JSON.stringify({
155
- id: 'space-1',
166
+ id: space1Id,
167
+ ref: 'space-1',
156
168
  title: 'General',
157
169
  type: 'group',
158
170
  isLocked: false,
159
- teamId: 'team-abc',
171
+ teamId,
172
+ teamRef: 'team-abc',
160
173
  lastActivity: '2024-01-02T00:00:00.000Z',
161
174
  created: '2024-01-01T00:00:00.000Z',
162
- creatorId: 'person-1',
175
+ creatorId: person1Id,
176
+ creatorRef: 'person-1',
163
177
  }),
164
178
  )
165
179
  })
@@ -171,8 +185,10 @@ describe('infoAction', () => {
171
185
 
172
186
  const output = JSON.parse(consoleLogSpy.mock.calls[0][0] as string) as {
173
187
  teamId: null
188
+ teamRef: null
174
189
  }
175
190
  expect(output.teamId).toBeNull()
191
+ expect(output.teamRef).toBeNull()
176
192
  })
177
193
 
178
194
  it('outputs pretty-printed JSON when pretty is true', async () => {
@@ -181,14 +197,17 @@ describe('infoAction', () => {
181
197
  expect(consoleLogSpy).toHaveBeenCalledWith(
182
198
  JSON.stringify(
183
199
  {
184
- id: 'space-1',
200
+ id: space1Id,
201
+ ref: 'space-1',
185
202
  title: 'General',
186
203
  type: 'group',
187
204
  isLocked: false,
188
- teamId: 'team-abc',
205
+ teamId,
206
+ teamRef: 'team-abc',
189
207
  lastActivity: '2024-01-02T00:00:00.000Z',
190
208
  created: '2024-01-01T00:00:00.000Z',
191
- creatorId: 'person-1',
209
+ creatorId: person1Id,
210
+ creatorRef: 'person-1',
192
211
  },
193
212
  null,
194
213
  2,
@@ -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(options: { type?: string; limit?: number; pretty?: boolean }): Promise<void> {
9
10
  try {
@@ -11,6 +12,7 @@ export async function listAction(options: { type?: string; limit?: number; prett
11
12
  const spaces = await client.listSpaces({ type: options.type, max: options.limit })
12
13
  const output = spaces.map((s) => ({
13
14
  id: s.id,
15
+ ref: toRef(s.id),
14
16
  title: s.title,
15
17
  type: s.type,
16
18
  lastActivity: s.lastActivity,
@@ -28,13 +30,16 @@ export async function infoAction(spaceId: string, options: { pretty?: boolean })
28
30
  const space = await client.getSpace(spaceId)
29
31
  const output = {
30
32
  id: space.id,
33
+ ref: toRef(space.id),
31
34
  title: space.title,
32
35
  type: space.type,
33
36
  isLocked: space.isLocked,
34
37
  teamId: space.teamId || null,
38
+ teamRef: space.teamId ? toRef(space.teamId) : null,
35
39
  lastActivity: space.lastActivity,
36
40
  created: space.created,
37
41
  creatorId: space.creatorId,
42
+ creatorRef: toRef(space.creatorId),
38
43
  }
39
44
  console.log(formatOutput(output, options.pretty))
40
45
  } catch (error) {
@@ -1,18 +1,22 @@
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
  import { whoamiCommand } from './whoami'
6
7
 
8
+ const orgId = Buffer.from('ciscospark://us/ORGANIZATION/org-123').toString('base64url')
9
+ const personId = toRestId('person-123', 'PEOPLE')
10
+
7
11
  const mockUser = {
8
- id: 'person-123',
12
+ id: personId,
9
13
  emails: ['test@example.com'],
10
14
  displayName: 'Test User',
11
15
  nickName: 'Testy',
12
16
  firstName: 'Test',
13
17
  lastName: 'User',
14
18
  avatar: 'https://example.com/avatar.jpg',
15
- orgId: 'org-123',
19
+ orgId,
16
20
  type: 'person' as const,
17
21
  created: '2024-01-01T00:00:00.000Z',
18
22
  }
@@ -58,14 +62,16 @@ it('whoami calls testAuth and outputs user fields', async () => {
58
62
  // then: outputs all expected fields
59
63
  expect(consoleLogSpy).toHaveBeenCalledWith(
60
64
  JSON.stringify({
61
- id: 'person-123',
65
+ id: personId,
66
+ ref: 'person-123',
62
67
  emails: ['test@example.com'],
63
68
  displayName: 'Test User',
64
69
  nickName: 'Testy',
65
70
  firstName: 'Test',
66
71
  lastName: 'User',
67
72
  avatar: 'https://example.com/avatar.jpg',
68
- orgId: 'org-123',
73
+ orgId,
74
+ orgRef: 'org-123',
69
75
  type: 'person',
70
76
  }),
71
77
  )
@@ -80,14 +86,16 @@ it('whoami outputs pretty-printed JSON when --pretty flag is passed', async () =
80
86
  expect(consoleLogSpy).toHaveBeenCalledWith(
81
87
  JSON.stringify(
82
88
  {
83
- id: 'person-123',
89
+ id: personId,
90
+ ref: 'person-123',
84
91
  emails: ['test@example.com'],
85
92
  displayName: 'Test User',
86
93
  nickName: 'Testy',
87
94
  firstName: 'Test',
88
95
  lastName: 'User',
89
96
  avatar: 'https://example.com/avatar.jpg',
90
- orgId: 'org-123',
97
+ orgId,
98
+ orgRef: 'org-123',
91
99
  type: 'person',
92
100
  },
93
101
  null,
@@ -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 whoamiAction(options: { pretty?: boolean }): Promise<void> {
9
10
  try {
@@ -12,6 +13,7 @@ export async function whoamiAction(options: { pretty?: boolean }): Promise<void>
12
13
 
13
14
  const output = {
14
15
  id: user.id,
16
+ ref: toRef(user.id),
15
17
  emails: user.emails,
16
18
  displayName: user.displayName,
17
19
  nickName: user.nickName,
@@ -19,6 +21,7 @@ export async function whoamiAction(options: { pretty?: boolean }): Promise<void>
19
21
  lastName: user.lastName,
20
22
  avatar: user.avatar,
21
23
  orgId: user.orgId,
24
+ orgRef: toRef(user.orgId),
22
25
  type: user.type,
23
26
  }
24
27
  console.log(formatOutput(output, options.pretty))
@@ -17,6 +17,7 @@ import {
17
17
  normalizeMessage,
18
18
  normalizeRoomActivity,
19
19
  toRestId,
20
+ toRef,
20
21
  } from './id-normalizer'
21
22
 
22
23
  const RAW: MercuryActivity = {
@@ -28,6 +29,8 @@ const RAW: MercuryActivity = {
28
29
  published: '2024-01-01T00:00:00Z',
29
30
  }
30
31
 
32
+ const restId = (type: string, ref: string) => Buffer.from(`ciscospark://us/${type}/${ref}`).toString('base64url')
33
+
31
34
  describe('toRestId / fromRestId', () => {
32
35
  it('encodes a uuid into a ciscospark REST id round-trippable to the uuid', () => {
33
36
  const restId = toRestId('abc-123', 'PEOPLE')
@@ -58,6 +61,40 @@ describe('toRestId / fromRestId', () => {
58
61
  })
59
62
  })
60
63
 
64
+ describe('toRef', () => {
65
+ it('returns a room uuid ref', () => {
66
+ const uuid = '12345678-1234-1234-1234-1234567890ab'
67
+
68
+ expect(toRef(toRestId(uuid, 'ROOM'))).toBe(uuid)
69
+ })
70
+
71
+ it('returns a person uuid ref', () => {
72
+ const uuid = '22222222-2222-2222-2222-222222222222'
73
+
74
+ expect(toRef(toRestId(uuid, 'PEOPLE'))).toBe(uuid)
75
+ })
76
+
77
+ it('returns a legacy person email ref', () => {
78
+ expect(toRef(toRestId('legacy@example.com', 'PEOPLE'))).toBe('legacy@example.com')
79
+ })
80
+
81
+ it('returns an organization uuid ref', () => {
82
+ const uuid = '33333333-3333-3333-3333-333333333333'
83
+
84
+ expect(toRef(restId('ORGANIZATION', uuid))).toBe(uuid)
85
+ })
86
+
87
+ it('returns a membership person-room pair ref', () => {
88
+ const membershipRef = '44444444-4444-4444-4444-444444444444:55555555-5555-5555-5555-555555555555'
89
+
90
+ expect(toRef(restId('MEMBERSHIP', membershipRef))).toBe(membershipRef)
91
+ })
92
+
93
+ it('returns empty input unchanged', () => {
94
+ expect(toRef('')).toBe('')
95
+ })
96
+ })
97
+
61
98
  describe('normalizeMessage', () => {
62
99
  const message: DecryptedMessage = {
63
100
  id: 'msg-uuid',
@@ -13,6 +13,27 @@ export { fromRestId }
13
13
  // (the resource type behind GET /v1/attachment/actions).
14
14
  export type WebexRestIdType = 'MESSAGE' | 'PEOPLE' | 'ROOM' | 'ATTACHMENT_ACTION'
15
15
 
16
+ export interface DecodedWebexId {
17
+ cluster: string
18
+ type: string
19
+ uuid: string
20
+ }
21
+
22
+ export function toRef(id: string): string {
23
+ if (!id) return id
24
+ return fromRestId(id)
25
+ }
26
+
27
+ // Webex REST ids are base64(url) of `ciscospark://<cluster>/<TYPE>/<uuid>`; room
28
+ // cluster correction needs all three parts, not just the trailing ref value.
29
+ export function decodeWebexId(restId: string): DecodedWebexId | null {
30
+ if (!restId) return null
31
+ const decoded = Buffer.from(restId, 'base64').toString('utf-8')
32
+ const match = decoded.match(/^ciscospark:\/\/([^/]+)\/([^/]+)\/(.+)$/)
33
+ if (!match) return null
34
+ return { cluster: match[1], type: match[2], uuid: match[3] }
35
+ }
36
+
16
37
  /**
17
38
  * Encode a raw Mercury UUID as a Webex REST ID. Empty input is returned unchanged
18
39
  * so an absent ID never becomes a bogus `ciscospark://us/{TYPE}/` value.
@@ -1,7 +1,7 @@
1
1
  export { WebexClient } from './client'
2
2
  export { WebexCredentialManager } from './credential-manager'
3
- export { fromRestId, toRestId } from './id-normalizer'
4
- export type { WebexRestIdType } from './id-normalizer'
3
+ export { decodeWebexId, fromRestId, toRef, toRestId } from './id-normalizer'
4
+ export type { DecodedWebexId, WebexRestIdType } from './id-normalizer'
5
5
  export { WebexListener } from './listener'
6
6
  export type { WebexListenerClient, WebexListenerEventMap, WebexListenerOptions } from './listener'
7
7
  export { loginWithPassword } from './password-login'
@@ -2,33 +2,10 @@ import { WebexClient } from '../webex/client'
2
2
  import type { WebexMembership, WebexMessage, WebexPerson, WebexSpace } from '../webex/types'
3
3
  import { WebexBotError } from './types'
4
4
 
5
- interface DecodedWebexId {
6
- cluster: string
7
- type: string
8
- uuid: string
9
- }
10
-
11
- // Webex REST ids are base64(url) of `ciscospark://<cluster>/<TYPE>/<uuid>`; the
12
- // cluster correction needs all three parts, not just the <uuid> `fromRestId` returns.
13
- function decodeWebexId(restId: string): DecodedWebexId | null {
14
- if (!restId) return null
15
- const decoded = Buffer.from(restId, 'base64').toString('utf-8')
16
- const match = decoded.match(/^ciscospark:\/\/([^/]+)\/([^/]+)\/(.+)$/)
17
- if (!match) return null
18
- return { cluster: match[1], type: match[2], uuid: match[3] }
19
- }
20
-
21
5
  export class WebexBotClient {
22
- private client = new WebexClient()
6
+ private client = new WebexClient({ roomResolutionWarningPrefix: '[webexbot]' })
23
7
  private token: string | null = null
24
8
 
25
- // The listener flattens room ids to `ciscospark://us/ROOM/<uuid>`, but team/group
26
- // rooms live on `ciscospark://urn:TEAM:<cluster>/ROOM/<uuid>` — a cluster the bare
27
- // uuid cannot recover. Cache the real clustered id per uuid and dedupe concurrent
28
- // lookups so a burst of calls triggers a single `listSpaces`.
29
- private clusteredRoomIds = new Map<string, string>()
30
- private roomIdLookups = new Map<string, Promise<string>>()
31
-
32
9
  async login(credentials?: { token: string }): Promise<this> {
33
10
  if (credentials) {
34
11
  if (!credentials.token) {
@@ -64,7 +41,7 @@ export class WebexBotClient {
64
41
  }
65
42
 
66
43
  async getSpace(spaceId: string): Promise<WebexSpace> {
67
- return this.client.getSpace(await this.resolveRoomId(spaceId))
44
+ return this.client.getSpace(spaceId)
68
45
  }
69
46
 
70
47
  async sendMessage(
@@ -72,7 +49,7 @@ export class WebexBotClient {
72
49
  text: string,
73
50
  options?: { markdown?: boolean; parentId?: string; files?: string[] },
74
51
  ): Promise<WebexMessage> {
75
- return this.client.sendMessage(await this.resolveRoomId(roomId), text, options)
52
+ return this.client.sendMessage(roomId, text, options)
76
53
  }
77
54
 
78
55
  async sendDirectMessage(personEmail: string, text: string, options?: { markdown?: boolean }): Promise<WebexMessage> {
@@ -80,14 +57,14 @@ export class WebexBotClient {
80
57
  }
81
58
 
82
59
  async listMessages(roomId: string, options?: { max?: number; parentId?: string }): Promise<WebexMessage[]> {
83
- const resolvedRoomId = await this.resolveRoomId(roomId)
60
+ const resolvedRoomId = await this.client.resolveRoomId(roomId)
84
61
  const space = await this.client.getSpace(resolvedRoomId)
85
62
  const messageOptions = space.type === 'group' ? { ...options, mentionedPeople: 'me' } : options
86
63
  return this.client.listMessages(resolvedRoomId, messageOptions)
87
64
  }
88
65
 
89
66
  async listReplies(roomId: string, parentId: string, options?: { max?: number }): Promise<WebexMessage[]> {
90
- return this.client.listMessages(await this.resolveRoomId(roomId), { ...options, parentId })
67
+ return this.client.listMessages(roomId, { ...options, parentId })
91
68
  }
92
69
 
93
70
  async getMessage(messageId: string): Promise<WebexMessage> {
@@ -108,7 +85,7 @@ export class WebexBotClient {
108
85
  text: string,
109
86
  options?: { markdown?: boolean },
110
87
  ): Promise<WebexMessage> {
111
- return this.client.editMessage(messageId, await this.resolveRoomId(roomId), text, options)
88
+ return this.client.editMessage(messageId, roomId, text, options)
112
89
  }
113
90
 
114
91
  async listPeople(options?: { email?: string; displayName?: string; max?: number }): Promise<WebexPerson[]> {
@@ -124,7 +101,7 @@ export class WebexBotClient {
124
101
  }
125
102
 
126
103
  async listMemberships(roomId: string, options?: { max?: number }): Promise<WebexMembership[]> {
127
- return this.client.listMemberships(await this.resolveRoomId(roomId), options)
104
+ return this.client.listMemberships(roomId, options)
128
105
  }
129
106
 
130
107
  async uploadFile(
@@ -132,53 +109,10 @@ export class WebexBotClient {
132
109
  file: { content: Blob; filename: string },
133
110
  options?: { text?: string; markdown?: boolean; parentId?: string },
134
111
  ): Promise<WebexMessage> {
135
- return this.client.uploadFile(await this.resolveRoomId(roomId), file, options)
112
+ return this.client.uploadFile(roomId, file, options)
136
113
  }
137
114
 
138
115
  async downloadContent(contentRef: string): Promise<{ data: ArrayBuffer; filename: string; contentType: string }> {
139
116
  return this.client.downloadContent(contentRef)
140
117
  }
141
-
142
- private async resolveRoomId(roomId: string): Promise<string> {
143
- const decoded = decodeWebexId(roomId)
144
- // Already cluster-qualified or undecodable: nothing to correct.
145
- if (!decoded || decoded.cluster.startsWith('urn:')) return roomId
146
-
147
- const { uuid } = decoded
148
- const cached = this.clusteredRoomIds.get(uuid)
149
- if (cached) return cached
150
-
151
- const inFlight = this.roomIdLookups.get(uuid)
152
- if (inFlight) return inFlight
153
-
154
- const lookup = this.lookupRoomId(uuid, roomId)
155
- this.roomIdLookups.set(uuid, lookup)
156
- try {
157
- return await lookup
158
- } finally {
159
- this.roomIdLookups.delete(uuid)
160
- }
161
- }
162
-
163
- private async lookupRoomId(uuid: string, fallback: string): Promise<string> {
164
- try {
165
- // Page through every room the bot belongs to (largest page size, following
166
- // `Link` pages), stopping as soon as the trailing UUID matches.
167
- for await (const room of this.client.iterateSpaces({ max: 1000 })) {
168
- if (decodeWebexId(room.id)?.uuid === uuid) {
169
- this.clusteredRoomIds.set(uuid, room.id)
170
- return room.id
171
- }
172
- }
173
- } catch {
174
- // Network/auth failure: fail open to the un-corrected id rather than block the call.
175
- return fallback
176
- }
177
-
178
- console.warn(
179
- `[webexbot] Could not resolve clustered room id for ${uuid}; falling back to the un-clustered id. ` +
180
- 'Room-scoped calls may fail if this room lives on a non-default Webex cluster.',
181
- )
182
- return fallback
183
- }
184
118
  }
@@ -5,12 +5,15 @@ import { Command } from 'commander'
5
5
 
6
6
  import { cliOutput } from '@/shared/utils/cli-output'
7
7
 
8
+ import { toRef } from '../../webex/id-normalizer'
8
9
  import type { BotOption } from './shared'
9
10
  import { getClient } from './shared'
10
11
 
11
12
  interface FileResult {
12
13
  id?: string
14
+ ref?: string
13
15
  roomId?: string
16
+ roomRef?: string
14
17
  files?: string[]
15
18
  created?: string
16
19
  downloaded?: string
@@ -37,7 +40,9 @@ export async function uploadAction(
37
40
 
38
41
  return {
39
42
  id: message.id,
43
+ ref: toRef(message.id),
40
44
  roomId: message.roomId,
45
+ roomRef: toRef(message.roomId),
41
46
  files: message.files,
42
47
  created: message.created,
43
48
  }
@@ -2,13 +2,16 @@ import { Command } from 'commander'
2
2
 
3
3
  import { cliOutput } from '@/shared/utils/cli-output'
4
4
 
5
+ import { toRef } from '../../webex/id-normalizer'
5
6
  import type { BotOption } from './shared'
6
7
  import { getClient } from './shared'
7
8
 
8
9
  interface MemberResult {
9
10
  members?: Array<{
10
11
  id: string
12
+ ref: string
11
13
  personId: string
14
+ personRef: string
12
15
  personEmail: string
13
16
  personDisplayName: string
14
17
  isModerator: boolean
@@ -26,7 +29,9 @@ export async function listAction(space: string, options: BotOption & { max?: str
26
29
  return {
27
30
  members: members.map((m) => ({
28
31
  id: m.id,
32
+ ref: toRef(m.id),
29
33
  personId: m.personId,
34
+ personRef: toRef(m.personId),
30
35
  personEmail: m.personEmail,
31
36
  personDisplayName: m.personDisplayName,
32
37
  isModerator: m.isModerator,