agent-messenger 2.20.4 → 2.21.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 (115) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +8 -5
  3. package/bun.lock +2 -2
  4. package/dist/package.json +10 -2
  5. package/dist/src/cli.d.ts.map +1 -1
  6. package/dist/src/cli.js +3 -0
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/platforms/webexbot/cli.d.ts +5 -0
  9. package/dist/src/platforms/webexbot/cli.d.ts.map +1 -0
  10. package/dist/src/platforms/webexbot/cli.js +30 -0
  11. package/dist/src/platforms/webexbot/cli.js.map +1 -0
  12. package/dist/src/platforms/webexbot/client.d.ts +41 -0
  13. package/dist/src/platforms/webexbot/client.d.ts.map +1 -0
  14. package/dist/src/platforms/webexbot/client.js +66 -0
  15. package/dist/src/platforms/webexbot/client.js.map +1 -0
  16. package/dist/src/platforms/webexbot/commands/auth.d.ts +28 -0
  17. package/dist/src/platforms/webexbot/commands/auth.d.ts.map +1 -0
  18. package/dist/src/platforms/webexbot/commands/auth.js +166 -0
  19. package/dist/src/platforms/webexbot/commands/auth.js.map +1 -0
  20. package/dist/src/platforms/webexbot/commands/index.d.ts +7 -0
  21. package/dist/src/platforms/webexbot/commands/index.d.ts.map +1 -0
  22. package/dist/src/platforms/webexbot/commands/index.js +7 -0
  23. package/dist/src/platforms/webexbot/commands/index.js.map +1 -0
  24. package/dist/src/platforms/webexbot/commands/listen.d.ts +12 -0
  25. package/dist/src/platforms/webexbot/commands/listen.d.ts.map +1 -0
  26. package/dist/src/platforms/webexbot/commands/listen.js +85 -0
  27. package/dist/src/platforms/webexbot/commands/listen.js.map +1 -0
  28. package/dist/src/platforms/webexbot/commands/member.d.ts +19 -0
  29. package/dist/src/platforms/webexbot/commands/member.d.ts.map +1 -0
  30. package/dist/src/platforms/webexbot/commands/member.js +33 -0
  31. package/dist/src/platforms/webexbot/commands/member.js.map +1 -0
  32. package/dist/src/platforms/webexbot/commands/message.d.ts +37 -0
  33. package/dist/src/platforms/webexbot/commands/message.d.ts.map +1 -0
  34. package/dist/src/platforms/webexbot/commands/message.js +142 -0
  35. package/dist/src/platforms/webexbot/commands/message.js.map +1 -0
  36. package/dist/src/platforms/webexbot/commands/shared.d.ts +9 -0
  37. package/dist/src/platforms/webexbot/commands/shared.d.ts.map +1 -0
  38. package/dist/src/platforms/webexbot/commands/shared.js +13 -0
  39. package/dist/src/platforms/webexbot/commands/shared.js.map +1 -0
  40. package/dist/src/platforms/webexbot/commands/space.d.ts +28 -0
  41. package/dist/src/platforms/webexbot/commands/space.d.ts.map +1 -0
  42. package/dist/src/platforms/webexbot/commands/space.js +61 -0
  43. package/dist/src/platforms/webexbot/commands/space.js.map +1 -0
  44. package/dist/src/platforms/webexbot/commands/whoami.d.ts +16 -0
  45. package/dist/src/platforms/webexbot/commands/whoami.d.ts.map +1 -0
  46. package/dist/src/platforms/webexbot/commands/whoami.js +29 -0
  47. package/dist/src/platforms/webexbot/commands/whoami.js.map +1 -0
  48. package/dist/src/platforms/webexbot/credential-manager.d.ts +17 -0
  49. package/dist/src/platforms/webexbot/credential-manager.d.ts.map +1 -0
  50. package/dist/src/platforms/webexbot/credential-manager.js +120 -0
  51. package/dist/src/platforms/webexbot/credential-manager.js.map +1 -0
  52. package/dist/src/platforms/webexbot/index.d.ts +9 -0
  53. package/dist/src/platforms/webexbot/index.d.ts.map +1 -0
  54. package/dist/src/platforms/webexbot/index.js +6 -0
  55. package/dist/src/platforms/webexbot/index.js.map +1 -0
  56. package/dist/src/platforms/webexbot/listener.d.ts +44 -0
  57. package/dist/src/platforms/webexbot/listener.d.ts.map +1 -0
  58. package/dist/src/platforms/webexbot/listener.js +214 -0
  59. package/dist/src/platforms/webexbot/listener.js.map +1 -0
  60. package/dist/src/platforms/webexbot/types.d.ts +60 -0
  61. package/dist/src/platforms/webexbot/types.d.ts.map +1 -0
  62. package/dist/src/platforms/webexbot/types.js +28 -0
  63. package/dist/src/platforms/webexbot/types.js.map +1 -0
  64. package/dist/src/platforms/webexbot/wdm-discovery.d.ts +4 -0
  65. package/dist/src/platforms/webexbot/wdm-discovery.d.ts.map +1 -0
  66. package/dist/src/platforms/webexbot/wdm-discovery.js +36 -0
  67. package/dist/src/platforms/webexbot/wdm-discovery.js.map +1 -0
  68. package/docs/content/docs/cli/meta.json +1 -0
  69. package/docs/content/docs/cli/webexbot.mdx +290 -0
  70. package/docs/content/docs/sdk/meta.json +1 -0
  71. package/docs/content/docs/sdk/webexbot.mdx +340 -0
  72. package/docs/src/app/page.tsx +115 -19
  73. package/package.json +10 -2
  74. package/skills/agent-channeltalk/SKILL.md +1 -1
  75. package/skills/agent-channeltalkbot/SKILL.md +1 -1
  76. package/skills/agent-discord/SKILL.md +1 -1
  77. package/skills/agent-discordbot/SKILL.md +1 -1
  78. package/skills/agent-instagram/SKILL.md +1 -1
  79. package/skills/agent-kakaotalk/SKILL.md +1 -1
  80. package/skills/agent-line/SKILL.md +1 -1
  81. package/skills/agent-slack/SKILL.md +1 -1
  82. package/skills/agent-slackbot/SKILL.md +1 -1
  83. package/skills/agent-teams/SKILL.md +1 -1
  84. package/skills/agent-telegram/SKILL.md +1 -1
  85. package/skills/agent-telegrambot/SKILL.md +1 -1
  86. package/skills/agent-webex/SKILL.md +1 -1
  87. package/skills/agent-webexbot/SKILL.md +361 -0
  88. package/skills/agent-webexbot/references/authentication.md +225 -0
  89. package/skills/agent-webexbot/references/common-patterns.md +590 -0
  90. package/skills/agent-wechatbot/SKILL.md +1 -1
  91. package/skills/agent-whatsapp/SKILL.md +1 -1
  92. package/skills/agent-whatsappbot/SKILL.md +1 -1
  93. package/src/cli.ts +4 -0
  94. package/src/platforms/webex/typings/webex-message-handler.d.ts +360 -29
  95. package/src/platforms/webexbot/cli.ts +42 -0
  96. package/src/platforms/webexbot/client.ts +87 -0
  97. package/src/platforms/webexbot/commands/auth.test.ts +185 -0
  98. package/src/platforms/webexbot/commands/auth.ts +210 -0
  99. package/src/platforms/webexbot/commands/index.ts +6 -0
  100. package/src/platforms/webexbot/commands/listen.test.ts +20 -0
  101. package/src/platforms/webexbot/commands/listen.ts +104 -0
  102. package/src/platforms/webexbot/commands/member.ts +51 -0
  103. package/src/platforms/webexbot/commands/message.ts +197 -0
  104. package/src/platforms/webexbot/commands/shared.ts +22 -0
  105. package/src/platforms/webexbot/commands/space.ts +88 -0
  106. package/src/platforms/webexbot/commands/whoami.ts +43 -0
  107. package/src/platforms/webexbot/credential-manager.test.ts +182 -0
  108. package/src/platforms/webexbot/credential-manager.ts +149 -0
  109. package/src/platforms/webexbot/index.ts +8 -0
  110. package/src/platforms/webexbot/listener.test.ts +234 -0
  111. package/src/platforms/webexbot/listener.ts +255 -0
  112. package/src/platforms/webexbot/types.test.ts +87 -0
  113. package/src/platforms/webexbot/types.ts +72 -0
  114. package/src/platforms/webexbot/wdm-discovery.test.ts +97 -0
  115. package/src/platforms/webexbot/wdm-discovery.ts +43 -0
@@ -0,0 +1,234 @@
1
+ import { describe, expect, it, mock } from 'bun:test'
2
+ import { EventEmitter } from 'events'
3
+
4
+ import type {
5
+ DecryptedMessage,
6
+ HandlerStatus,
7
+ MercuryActivity,
8
+ WebexMessageHandlerConfig,
9
+ WebexMessageHandlerEvents,
10
+ } from 'webex-message-handler'
11
+
12
+ import { WebexBotListener } from './listener'
13
+
14
+ const STATUS: HandlerStatus = {
15
+ status: 'connected',
16
+ webSocketOpen: true,
17
+ kmsInitialized: true,
18
+ deviceRegistered: true,
19
+ reconnectAttempt: 0,
20
+ }
21
+
22
+ const RAW_ACTIVITY: MercuryActivity = {
23
+ id: 'activity-123',
24
+ verb: 'post',
25
+ actor: { id: 'person-123', objectType: 'person', emailAddress: 'user@example.com' },
26
+ object: { id: 'object-123', objectType: 'comment', displayName: 'hello' },
27
+ target: { id: 'room-123', objectType: 'conversation' },
28
+ published: '2024-01-01T00:00:00Z',
29
+ }
30
+
31
+ const MESSAGE: DecryptedMessage = {
32
+ id: 'message-123',
33
+ roomId: 'room-123',
34
+ personId: 'person-123',
35
+ personEmail: 'user@example.com',
36
+ text: 'hello',
37
+ created: '2024-01-01T00:00:00Z',
38
+ mentionedPeople: [],
39
+ mentionedGroups: [],
40
+ files: [],
41
+ raw: RAW_ACTIVITY,
42
+ }
43
+
44
+ class FakeWebexMessageHandler extends EventEmitter {
45
+ connect = mock(() => Promise.resolve())
46
+ disconnect = mock(() => Promise.resolve())
47
+ connected = true
48
+
49
+ status(): HandlerStatus {
50
+ return STATUS
51
+ }
52
+
53
+ override on<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this {
54
+ return super.on(event, listener)
55
+ }
56
+
57
+ override off<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this {
58
+ return super.off(event, listener)
59
+ }
60
+
61
+ override once<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this {
62
+ return super.once(event, listener)
63
+ }
64
+ }
65
+
66
+ describe('WebexBotListener', () => {
67
+ it('bridges handler message events and webex_event', async () => {
68
+ const handler = new FakeWebexMessageHandler()
69
+ const client = { getToken: () => 'token123' }
70
+ const listener = new WebexBotListener(client, {
71
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
72
+ })
73
+ const messageCreated = mock((_event: DecryptedMessage) => undefined)
74
+ const webexEvent = mock((_event: DecryptedMessage) => undefined)
75
+ listener.on('message_created', messageCreated)
76
+ listener.on('webex_event', webexEvent)
77
+
78
+ await listener.start()
79
+ handler.emit('message:created', MESSAGE)
80
+
81
+ expect(messageCreated).toHaveBeenCalledWith(MESSAGE)
82
+ expect(webexEvent).toHaveBeenCalledWith(MESSAGE)
83
+ })
84
+
85
+ it('stop calls handler disconnect', async () => {
86
+ const handler = new FakeWebexMessageHandler()
87
+ const client = { getToken: () => 'token123' }
88
+ const listener = new WebexBotListener(client, {
89
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
90
+ })
91
+
92
+ await listener.start()
93
+ await listener.stop()
94
+
95
+ expect(handler.disconnect).toHaveBeenCalled()
96
+ })
97
+
98
+ it('start is idempotent', async () => {
99
+ const handler = new FakeWebexMessageHandler()
100
+ const client = { getToken: () => 'token123' }
101
+ const listener = new WebexBotListener(client, {
102
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
103
+ })
104
+
105
+ await listener.start()
106
+ await listener.start()
107
+
108
+ expect(handler.connect).toHaveBeenCalledTimes(1)
109
+ })
110
+
111
+ it('start rethrows and resets state when connect fails, allowing retry', async () => {
112
+ const failing = new FakeWebexMessageHandler()
113
+ failing.connect = mock(() => Promise.reject(new Error('device registration failed')))
114
+ const ok = new FakeWebexMessageHandler()
115
+ const handlers = [failing, ok]
116
+ const client = { getToken: () => 'token123' }
117
+ const listener = new WebexBotListener(client, {
118
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handlers.shift()!,
119
+ })
120
+
121
+ await expect(listener.start()).rejects.toThrow('device registration failed')
122
+ expect(failing.disconnect).toHaveBeenCalled()
123
+
124
+ await listener.start()
125
+ expect(ok.connect).toHaveBeenCalledTimes(1)
126
+ })
127
+
128
+ it('does not throw when handler emits error with no error listener', async () => {
129
+ const handler = new FakeWebexMessageHandler()
130
+ const client = { getToken: () => 'token123' }
131
+ const listener = new WebexBotListener(client, {
132
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
133
+ })
134
+ listener.on('message_created', () => undefined)
135
+
136
+ await listener.start()
137
+
138
+ expect(() => handler.emit('error', new Error('boom'))).not.toThrow()
139
+ })
140
+
141
+ it('ignores stale handler events after stop', async () => {
142
+ const handler = new FakeWebexMessageHandler()
143
+ const client = { getToken: () => 'token123' }
144
+ const listener = new WebexBotListener(client, {
145
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
146
+ })
147
+ const messageCreated = mock((_event: DecryptedMessage) => undefined)
148
+ listener.on('message_created', messageCreated)
149
+
150
+ await listener.start()
151
+ await listener.stop()
152
+ handler.emit('message:created', MESSAGE)
153
+
154
+ expect(messageCreated).not.toHaveBeenCalled()
155
+ })
156
+
157
+ it('start-stop-start does not cross-talk between handlers', async () => {
158
+ const first = new FakeWebexMessageHandler()
159
+ const second = new FakeWebexMessageHandler()
160
+ const handlers = [first, second]
161
+ const client = { getToken: () => 'token123' }
162
+ const listener = new WebexBotListener(client, {
163
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handlers.shift()!,
164
+ })
165
+ const messageCreated = mock((_event: DecryptedMessage) => undefined)
166
+ listener.on('message_created', messageCreated)
167
+
168
+ await listener.start()
169
+ await listener.stop()
170
+ await listener.start()
171
+
172
+ first.emit('message:created', MESSAGE)
173
+ expect(messageCreated).not.toHaveBeenCalled()
174
+
175
+ second.emit('message:created', MESSAGE)
176
+ expect(messageCreated).toHaveBeenCalledTimes(1)
177
+ })
178
+
179
+ it('preserves disconnected reason', async () => {
180
+ const handler = new FakeWebexMessageHandler()
181
+ const client = { getToken: () => 'token123' }
182
+ const listener = new WebexBotListener(client, {
183
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
184
+ })
185
+ const disconnected = mock((_reason: string) => undefined)
186
+ listener.on('disconnected', disconnected)
187
+
188
+ await listener.start()
189
+ handler.emit('disconnected', 'network lost')
190
+
191
+ expect(disconnected).toHaveBeenCalledWith('network lost')
192
+ })
193
+
194
+ it('concurrent start() calls share the same connect failure', async () => {
195
+ const handler = new FakeWebexMessageHandler()
196
+ handler.connect = mock(() => Promise.reject(new Error('connect failed')))
197
+ const client = { getToken: () => 'token123' }
198
+ const listener = new WebexBotListener(client, {
199
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
200
+ })
201
+
202
+ const first = listener.start()
203
+ const second = listener.start()
204
+ const firstResult = first.then(
205
+ () => 'ok',
206
+ (e: Error) => e.message,
207
+ )
208
+ const secondResult = second.then(
209
+ () => 'ok',
210
+ (e: Error) => e.message,
211
+ )
212
+
213
+ expect(await firstResult).toBe('connect failed')
214
+ expect(await secondResult).toBe('connect failed')
215
+ expect(handler.connect).toHaveBeenCalledTimes(1)
216
+ })
217
+
218
+ it('disconnects a handler whose connect resolves after stop', async () => {
219
+ const handler = new FakeWebexMessageHandler()
220
+ let resolveConnect: () => void = () => undefined
221
+ handler.connect = mock(() => new Promise<void>((resolve) => (resolveConnect = resolve)))
222
+ const client = { getToken: () => 'token123' }
223
+ const listener = new WebexBotListener(client, {
224
+ _handlerFactory: (_config: WebexMessageHandlerConfig) => handler,
225
+ })
226
+
227
+ const starting = listener.start()
228
+ const stopping = listener.stop()
229
+ resolveConnect()
230
+ await Promise.all([starting, stopping])
231
+
232
+ expect(handler.disconnect).toHaveBeenCalled()
233
+ })
234
+ })
@@ -0,0 +1,255 @@
1
+ import { EventEmitter } from 'events'
2
+
3
+ import { WebexMessageHandler } from 'webex-message-handler'
4
+ import type {
5
+ AttachmentAction,
6
+ DecryptedMessage,
7
+ DeletedMessage,
8
+ HandlerStatus,
9
+ InjectedWebSocket,
10
+ MembershipActivity,
11
+ RoomActivity,
12
+ WebexMessageHandlerConfig,
13
+ WebexMessageHandlerEvents,
14
+ } from 'webex-message-handler'
15
+ import WebSocket from 'ws'
16
+
17
+ import type { WebexBotClient } from './client'
18
+ import type { WebexBotListenerEventMap } from './types'
19
+ import { createWdmRewriteFetch, discoverWdmDevicesUrl } from './wdm-discovery'
20
+
21
+ type EventKey = keyof WebexBotListenerEventMap
22
+ type WebexBotClientLike = Pick<WebexBotClient, 'getToken'>
23
+
24
+ interface WebexMessageHandlerLike {
25
+ connect(): Promise<void>
26
+ disconnect(): Promise<void>
27
+ status(): HandlerStatus
28
+ get connected(): boolean
29
+ on<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this
30
+ off<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this
31
+ once<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this
32
+ }
33
+
34
+ export interface WebexBotListenerOptions {
35
+ ignoreSelfMessages?: boolean
36
+ pingInterval?: number
37
+ pongTimeout?: number
38
+ reconnectBackoffMax?: number
39
+ maxReconnectAttempts?: number
40
+ _handlerFactory?: (config: WebexMessageHandlerConfig) => WebexMessageHandlerLike
41
+ }
42
+
43
+ export class WebexBotListener {
44
+ private client: WebexBotClientLike
45
+ private options: WebexBotListenerOptions
46
+ private running = false
47
+ private emitter = new EventEmitter()
48
+ private handler: WebexMessageHandlerLike | null = null
49
+ private generation = 0
50
+ private detachHandler: (() => void) | null = null
51
+ private startPromise: Promise<void> | null = null
52
+
53
+ constructor(client: WebexBotClientLike, options: WebexBotListenerOptions = {}) {
54
+ this.client = client
55
+ this.options = options
56
+ }
57
+
58
+ async start(): Promise<void> {
59
+ if (this.startPromise) return this.startPromise
60
+ if (this.running) return
61
+
62
+ this.running = true
63
+ const generation = ++this.generation
64
+
65
+ let startPromise!: Promise<void>
66
+ startPromise = (async () => {
67
+ let handler: WebexMessageHandlerLike | null = null
68
+ try {
69
+ handler = await this.createHandler()
70
+
71
+ if (!this.running || this.generation !== generation) {
72
+ await handler.disconnect().catch(() => undefined)
73
+ return
74
+ }
75
+ this.handler = handler
76
+ this.detachHandler = this.wireHandler(handler, generation)
77
+
78
+ await handler.connect()
79
+
80
+ if (!this.running || this.handler !== handler || this.generation !== generation) {
81
+ await handler.disconnect().catch(() => undefined)
82
+ return
83
+ }
84
+ } catch (error) {
85
+ if (this.handler === handler && this.generation === generation) {
86
+ this.detachHandler?.()
87
+ this.detachHandler = null
88
+ this.handler = null
89
+ }
90
+ if (this.generation === generation) {
91
+ this.running = false
92
+ }
93
+ await handler?.disconnect().catch(() => undefined)
94
+ throw error
95
+ } finally {
96
+ if (this.startPromise === startPromise) {
97
+ this.startPromise = null
98
+ }
99
+ }
100
+ })()
101
+
102
+ this.startPromise = startPromise
103
+ return startPromise
104
+ }
105
+
106
+ async stop(): Promise<void> {
107
+ const pendingStart = this.startPromise
108
+ this.startPromise = null
109
+
110
+ this.running = false
111
+ this.generation++
112
+
113
+ const detach = this.detachHandler
114
+ this.detachHandler = null
115
+ detach?.()
116
+
117
+ const handler = this.handler
118
+ this.handler = null
119
+ if (handler) {
120
+ await handler.disconnect().catch(() => undefined)
121
+ }
122
+
123
+ if (pendingStart) {
124
+ await pendingStart.catch(() => undefined)
125
+ }
126
+ }
127
+
128
+ on<K extends EventKey>(event: K, listener: (...args: WebexBotListenerEventMap[K]) => void): this {
129
+ this.emitter.on(event, listener as (...args: any[]) => void)
130
+ return this
131
+ }
132
+
133
+ off<K extends EventKey>(event: K, listener: (...args: WebexBotListenerEventMap[K]) => void): this {
134
+ this.emitter.off(event, listener as (...args: any[]) => void)
135
+ return this
136
+ }
137
+
138
+ once<K extends EventKey>(event: K, listener: (...args: WebexBotListenerEventMap[K]) => void): this {
139
+ this.emitter.once(event, listener as (...args: any[]) => void)
140
+ return this
141
+ }
142
+
143
+ private isCurrent(handler: WebexMessageHandlerLike, generation: number): boolean {
144
+ return this.running && this.handler === handler && this.generation === generation
145
+ }
146
+
147
+ // Node's EventEmitter throws synchronously if an 'error' event is emitted with
148
+ // no registered listener. Guard it so SDK consumers that only subscribe to
149
+ // message events are never crashed by a transient connection error.
150
+ private emitError(error: Error): void {
151
+ if (this.emitter.listenerCount('error') > 0) {
152
+ this.emitter.emit('error', error)
153
+ }
154
+ }
155
+
156
+ private async createHandler(): Promise<WebexMessageHandlerLike> {
157
+ const token = this.client.getToken()
158
+ const config: WebexMessageHandlerConfig = { token }
159
+ if (this.options.ignoreSelfMessages !== undefined) config.ignoreSelfMessages = this.options.ignoreSelfMessages
160
+ if (this.options.pingInterval !== undefined) config.pingInterval = this.options.pingInterval
161
+ if (this.options.pongTimeout !== undefined) config.pongTimeout = this.options.pongTimeout
162
+ if (this.options.reconnectBackoffMax !== undefined) config.reconnectBackoffMax = this.options.reconnectBackoffMax
163
+ if (this.options.maxReconnectAttempts !== undefined) config.maxReconnectAttempts = this.options.maxReconnectAttempts
164
+
165
+ if (this.options._handlerFactory) {
166
+ return this.options._handlerFactory(config)
167
+ }
168
+
169
+ const wdmDevicesUrl = await discoverWdmDevicesUrl(token)
170
+ config.mode = 'injected'
171
+ config.fetch = createWdmRewriteFetch(wdmDevicesUrl)
172
+ config.webSocketFactory = (url: string) => new WebSocket(url) as unknown as InjectedWebSocket
173
+ return new WebexMessageHandler(config)
174
+ }
175
+
176
+ private wireHandler(handler: WebexMessageHandlerLike, generation: number): () => void {
177
+ const onMessageCreated = (event: DecryptedMessage) => {
178
+ if (!this.isCurrent(handler, generation)) return
179
+ this.emitter.emit('message_created', event)
180
+ this.emitter.emit('webex_event', event)
181
+ }
182
+ const onMessageUpdated = (event: DecryptedMessage) => {
183
+ if (!this.isCurrent(handler, generation)) return
184
+ this.emitter.emit('message_updated', event)
185
+ this.emitter.emit('webex_event', event)
186
+ }
187
+ const onMessageDeleted = (event: DeletedMessage) => {
188
+ if (!this.isCurrent(handler, generation)) return
189
+ this.emitter.emit('message_deleted', event)
190
+ this.emitter.emit('webex_event', event)
191
+ }
192
+ const onMembershipCreated = (event: MembershipActivity) => {
193
+ if (!this.isCurrent(handler, generation)) return
194
+ this.emitter.emit('membership_created', event)
195
+ this.emitter.emit('webex_event', event)
196
+ }
197
+ const onAttachmentAction = (event: AttachmentAction) => {
198
+ if (!this.isCurrent(handler, generation)) return
199
+ this.emitter.emit('attachment_action', event)
200
+ this.emitter.emit('webex_event', event)
201
+ }
202
+ const onRoomCreated = (event: RoomActivity) => {
203
+ if (!this.isCurrent(handler, generation)) return
204
+ this.emitter.emit('room_created', event)
205
+ this.emitter.emit('webex_event', event)
206
+ }
207
+ const onRoomUpdated = (event: RoomActivity) => {
208
+ if (!this.isCurrent(handler, generation)) return
209
+ this.emitter.emit('room_updated', event)
210
+ this.emitter.emit('webex_event', event)
211
+ }
212
+ const onConnected = () => {
213
+ if (!this.isCurrent(handler, generation)) return
214
+ this.emitter.emit('connected', { connected: handler.connected, status: handler.status() })
215
+ }
216
+ const onReconnecting = (attempt: number) => {
217
+ if (!this.isCurrent(handler, generation)) return
218
+ this.emitter.emit('reconnecting', attempt)
219
+ }
220
+ const onDisconnected = (reason: string) => {
221
+ if (!this.isCurrent(handler, generation)) return
222
+ this.emitter.emit('disconnected', reason)
223
+ }
224
+ const onError = (error: Error) => {
225
+ if (!this.isCurrent(handler, generation)) return
226
+ this.emitError(error)
227
+ }
228
+
229
+ handler.on('message:created', onMessageCreated)
230
+ handler.on('message:updated', onMessageUpdated)
231
+ handler.on('message:deleted', onMessageDeleted)
232
+ handler.on('membership:created', onMembershipCreated)
233
+ handler.on('attachmentAction:created', onAttachmentAction)
234
+ handler.on('room:created', onRoomCreated)
235
+ handler.on('room:updated', onRoomUpdated)
236
+ handler.on('connected', onConnected)
237
+ handler.on('reconnecting', onReconnecting)
238
+ handler.on('disconnected', onDisconnected)
239
+ handler.on('error', onError)
240
+
241
+ return () => {
242
+ handler.off('message:created', onMessageCreated)
243
+ handler.off('message:updated', onMessageUpdated)
244
+ handler.off('message:deleted', onMessageDeleted)
245
+ handler.off('membership:created', onMembershipCreated)
246
+ handler.off('attachmentAction:created', onAttachmentAction)
247
+ handler.off('room:created', onRoomCreated)
248
+ handler.off('room:updated', onRoomUpdated)
249
+ handler.off('connected', onConnected)
250
+ handler.off('reconnecting', onReconnecting)
251
+ handler.off('disconnected', onDisconnected)
252
+ handler.off('error', onError)
253
+ }
254
+ }
255
+ }
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { WebexBotConfigSchema, WebexBotCredentialsSchema, WebexBotEntrySchema, WebexBotError } from './types'
4
+
5
+ describe('WebexBotError', () => {
6
+ it('creates error with message and code', () => {
7
+ const error = new WebexBotError('Test error', 'TEST_CODE')
8
+ expect(error.message).toBe('Test error')
9
+ expect(error.code).toBe('TEST_CODE')
10
+ expect(error.name).toBe('WebexBotError')
11
+ })
12
+ })
13
+
14
+ describe('WebexBotEntrySchema', () => {
15
+ it('validates correct bot entry', () => {
16
+ const data = {
17
+ bot_id: 'bot123',
18
+ bot_name: 'My Bot',
19
+ token: 'token123',
20
+ }
21
+
22
+ const result = WebexBotEntrySchema.safeParse(data)
23
+
24
+ expect(result.success).toBe(true)
25
+ })
26
+
27
+ it('rejects missing token', () => {
28
+ const data = {
29
+ bot_id: 'bot123',
30
+ bot_name: 'My Bot',
31
+ }
32
+
33
+ const result = WebexBotEntrySchema.safeParse(data)
34
+
35
+ expect(result.success).toBe(false)
36
+ })
37
+ })
38
+
39
+ describe('WebexBotConfigSchema', () => {
40
+ it('validates correct config with bots', () => {
41
+ const data = {
42
+ current: { bot_id: 'bot123' },
43
+ bots: {
44
+ bot123: {
45
+ bot_id: 'bot123',
46
+ bot_name: 'My Bot',
47
+ token: 'token123',
48
+ },
49
+ },
50
+ }
51
+
52
+ const result = WebexBotConfigSchema.safeParse(data)
53
+
54
+ expect(result.success).toBe(true)
55
+ })
56
+
57
+ it('validates config with null current', () => {
58
+ const result = WebexBotConfigSchema.safeParse({ current: null, bots: {} })
59
+
60
+ expect(result.success).toBe(true)
61
+ })
62
+ })
63
+
64
+ describe('WebexBotCredentialsSchema', () => {
65
+ it('validates credentials with required fields', () => {
66
+ const data = {
67
+ token: 'token123',
68
+ bot_id: 'bot123',
69
+ bot_name: 'My Bot',
70
+ }
71
+
72
+ const result = WebexBotCredentialsSchema.safeParse(data)
73
+
74
+ expect(result.success).toBe(true)
75
+ })
76
+
77
+ it('rejects missing bot_id', () => {
78
+ const data = {
79
+ token: 'token123',
80
+ bot_name: 'My Bot',
81
+ }
82
+
83
+ const result = WebexBotCredentialsSchema.safeParse(data)
84
+
85
+ expect(result.success).toBe(false)
86
+ })
87
+ })
@@ -0,0 +1,72 @@
1
+ import type {
2
+ AttachmentAction,
3
+ DecryptedMessage,
4
+ DeletedMessage,
5
+ HandlerStatus,
6
+ MembershipActivity,
7
+ RoomActivity,
8
+ } from 'webex-message-handler'
9
+ import { z } from 'zod'
10
+
11
+ export interface WebexBotEntry {
12
+ bot_id: string
13
+ bot_name: string
14
+ token: string
15
+ }
16
+
17
+ export interface WebexBotConfig {
18
+ current: { bot_id: string } | null
19
+ bots: Record<string, WebexBotEntry>
20
+ }
21
+
22
+ export interface WebexBotCredentials {
23
+ token: string
24
+ bot_id: string
25
+ bot_name: string
26
+ }
27
+
28
+ export class WebexBotError extends Error {
29
+ code: string
30
+
31
+ constructor(message: string, code: string) {
32
+ super(message)
33
+ this.name = 'WebexBotError'
34
+ this.code = code
35
+ }
36
+ }
37
+
38
+ export const WebexBotEntrySchema = z.object({
39
+ bot_id: z.string(),
40
+ bot_name: z.string(),
41
+ token: z.string(),
42
+ })
43
+
44
+ export const WebexBotConfigSchema = z.object({
45
+ current: z
46
+ .object({
47
+ bot_id: z.string(),
48
+ })
49
+ .nullable(),
50
+ bots: z.record(z.string(), WebexBotEntrySchema),
51
+ })
52
+
53
+ export const WebexBotCredentialsSchema = z.object({
54
+ token: z.string(),
55
+ bot_id: z.string(),
56
+ bot_name: z.string(),
57
+ })
58
+
59
+ export interface WebexBotListenerEventMap {
60
+ message_created: [event: DecryptedMessage]
61
+ message_updated: [event: DecryptedMessage]
62
+ message_deleted: [event: DeletedMessage]
63
+ membership_created: [event: MembershipActivity]
64
+ attachment_action: [event: AttachmentAction]
65
+ room_created: [event: RoomActivity]
66
+ room_updated: [event: RoomActivity]
67
+ webex_event: [event: DecryptedMessage | DeletedMessage | MembershipActivity | AttachmentAction | RoomActivity]
68
+ connected: [info: { connected: boolean; status: HandlerStatus }]
69
+ reconnecting: [attempt: number]
70
+ disconnected: [reason: string]
71
+ error: [error: Error]
72
+ }