agent-messenger 2.20.5 → 2.22.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -5
- package/dist/package.json +9 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/webex/client.d.ts +19 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -1
- package/dist/src/platforms/webex/client.js +81 -1
- package/dist/src/platforms/webex/client.js.map +1 -1
- package/dist/src/platforms/webexbot/cli.d.ts +5 -0
- package/dist/src/platforms/webexbot/cli.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/cli.js +33 -0
- package/dist/src/platforms/webexbot/cli.js.map +1 -0
- package/dist/src/platforms/webexbot/client.d.ts +61 -0
- package/dist/src/platforms/webexbot/client.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/client.js +80 -0
- package/dist/src/platforms/webexbot/client.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/auth.d.ts +28 -0
- package/dist/src/platforms/webexbot/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/auth.js +166 -0
- package/dist/src/platforms/webexbot/commands/auth.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/file.d.ts +22 -0
- package/dist/src/platforms/webexbot/commands/file.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/file.js +64 -0
- package/dist/src/platforms/webexbot/commands/file.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/index.d.ts +10 -0
- package/dist/src/platforms/webexbot/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/index.js +10 -0
- package/dist/src/platforms/webexbot/commands/index.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/listen.d.ts +12 -0
- package/dist/src/platforms/webexbot/commands/listen.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/listen.js +85 -0
- package/dist/src/platforms/webexbot/commands/listen.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/member.d.ts +19 -0
- package/dist/src/platforms/webexbot/commands/member.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/member.js +33 -0
- package/dist/src/platforms/webexbot/commands/member.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/message.d.ts +44 -0
- package/dist/src/platforms/webexbot/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/message.js +193 -0
- package/dist/src/platforms/webexbot/commands/message.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/shared.d.ts +9 -0
- package/dist/src/platforms/webexbot/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/shared.js +13 -0
- package/dist/src/platforms/webexbot/commands/shared.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/snapshot.d.ts +24 -0
- package/dist/src/platforms/webexbot/commands/snapshot.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/snapshot.js +37 -0
- package/dist/src/platforms/webexbot/commands/snapshot.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/space.d.ts +28 -0
- package/dist/src/platforms/webexbot/commands/space.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/space.js +61 -0
- package/dist/src/platforms/webexbot/commands/space.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/user.d.ts +30 -0
- package/dist/src/platforms/webexbot/commands/user.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/user.js +66 -0
- package/dist/src/platforms/webexbot/commands/user.js.map +1 -0
- package/dist/src/platforms/webexbot/commands/whoami.d.ts +16 -0
- package/dist/src/platforms/webexbot/commands/whoami.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/commands/whoami.js +29 -0
- package/dist/src/platforms/webexbot/commands/whoami.js.map +1 -0
- package/dist/src/platforms/webexbot/credential-manager.d.ts +17 -0
- package/dist/src/platforms/webexbot/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/credential-manager.js +120 -0
- package/dist/src/platforms/webexbot/credential-manager.js.map +1 -0
- package/dist/src/platforms/webexbot/index.d.ts +9 -0
- package/dist/src/platforms/webexbot/index.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/index.js +6 -0
- package/dist/src/platforms/webexbot/index.js.map +1 -0
- package/dist/src/platforms/webexbot/listener.d.ts +44 -0
- package/dist/src/platforms/webexbot/listener.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/listener.js +214 -0
- package/dist/src/platforms/webexbot/listener.js.map +1 -0
- package/dist/src/platforms/webexbot/types.d.ts +60 -0
- package/dist/src/platforms/webexbot/types.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/types.js +28 -0
- package/dist/src/platforms/webexbot/types.js.map +1 -0
- package/dist/src/platforms/webexbot/wdm-discovery.d.ts +4 -0
- package/dist/src/platforms/webexbot/wdm-discovery.d.ts.map +1 -0
- package/dist/src/platforms/webexbot/wdm-discovery.js +36 -0
- package/dist/src/platforms/webexbot/wdm-discovery.js.map +1 -0
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/cli/webexbot.mdx +292 -0
- package/docs/content/docs/sdk/meta.json +1 -0
- package/docs/content/docs/sdk/webexbot.mdx +342 -0
- package/docs/src/app/page.tsx +115 -19
- package/package.json +9 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +1 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +414 -0
- package/skills/agent-webexbot/references/authentication.md +225 -0
- package/skills/agent-webexbot/references/common-patterns.md +708 -0
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/cli.ts +4 -0
- package/src/platforms/webex/client.test.ts +10 -0
- package/src/platforms/webex/client.ts +97 -3
- package/src/platforms/webex/typings/webex-message-handler.d.ts +360 -29
- package/src/platforms/webexbot/cli.ts +48 -0
- package/src/platforms/webexbot/client.test.ts +198 -0
- package/src/platforms/webexbot/client.ts +113 -0
- package/src/platforms/webexbot/commands/auth.test.ts +185 -0
- package/src/platforms/webexbot/commands/auth.ts +210 -0
- package/src/platforms/webexbot/commands/file.ts +104 -0
- package/src/platforms/webexbot/commands/index.ts +9 -0
- package/src/platforms/webexbot/commands/listen.test.ts +20 -0
- package/src/platforms/webexbot/commands/listen.ts +104 -0
- package/src/platforms/webexbot/commands/member.ts +51 -0
- package/src/platforms/webexbot/commands/message.ts +263 -0
- package/src/platforms/webexbot/commands/shared.ts +22 -0
- package/src/platforms/webexbot/commands/snapshot.ts +60 -0
- package/src/platforms/webexbot/commands/space.ts +88 -0
- package/src/platforms/webexbot/commands/user.test.ts +77 -0
- package/src/platforms/webexbot/commands/user.ts +98 -0
- package/src/platforms/webexbot/commands/whoami.ts +43 -0
- package/src/platforms/webexbot/credential-manager.test.ts +182 -0
- package/src/platforms/webexbot/credential-manager.ts +149 -0
- package/src/platforms/webexbot/index.ts +8 -0
- package/src/platforms/webexbot/listener.test.ts +234 -0
- package/src/platforms/webexbot/listener.ts +255 -0
- package/src/platforms/webexbot/types.test.ts +87 -0
- package/src/platforms/webexbot/types.ts +72 -0
- package/src/platforms/webexbot/wdm-discovery.test.ts +97 -0
- package/src/platforms/webexbot/wdm-discovery.ts +43 -0
package/src/cli.ts
CHANGED
|
@@ -76,6 +76,10 @@ program.command('webex', 'Interact with Cisco Webex', {
|
|
|
76
76
|
executableFile: join(__dirname, 'platforms', 'webex', `cli${ext}`),
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
+
program.command('webexbot', 'Interact with Cisco Webex using bot tokens', {
|
|
80
|
+
executableFile: join(__dirname, 'platforms', 'webexbot', `cli${ext}`),
|
|
81
|
+
})
|
|
82
|
+
|
|
79
83
|
program.command('tui', 'Launch unified messenger TUI', {
|
|
80
84
|
executableFile: join(__dirname, 'tui', `cli${ext}`),
|
|
81
85
|
})
|
|
@@ -274,6 +274,16 @@ describe('WebexClient', () => {
|
|
|
274
274
|
|
|
275
275
|
expect(fetchCalls[0].url).toContain('max=10')
|
|
276
276
|
})
|
|
277
|
+
|
|
278
|
+
it('passes mentionedPeople when requested', async () => {
|
|
279
|
+
mockResponse({ items: [] })
|
|
280
|
+
|
|
281
|
+
const client = await new WebexClient().login({ token: 'test-token' })
|
|
282
|
+
await client.listMessages('room1', { max: 10, mentionedPeople: 'me' })
|
|
283
|
+
|
|
284
|
+
const url = new URL(fetchCalls[0].url)
|
|
285
|
+
expect(url.searchParams.get('mentionedPeople')).toBe('me')
|
|
286
|
+
})
|
|
277
287
|
})
|
|
278
288
|
|
|
279
289
|
describe('getMessage', () => {
|
|
@@ -6,6 +6,7 @@ import type { WebexConfig, WebexMembership, WebexMessage, WebexPerson, WebexSpac
|
|
|
6
6
|
import { WebexError } from './types'
|
|
7
7
|
|
|
8
8
|
const BASE_URL = 'https://webexapis.com/v1'
|
|
9
|
+
const CONTENT_HOST = 'webexapis.com'
|
|
9
10
|
const MAX_RETRIES = 3
|
|
10
11
|
const BASE_BACKOFF_MS = 100
|
|
11
12
|
|
|
@@ -223,11 +224,19 @@ export class WebexClient {
|
|
|
223
224
|
return this.request<WebexSpace>('GET', `/rooms/${spaceId}`)
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
async sendMessage(
|
|
227
|
+
async sendMessage(
|
|
228
|
+
roomId: string,
|
|
229
|
+
text: string,
|
|
230
|
+
options?: { markdown?: boolean; parentId?: string; files?: string[] },
|
|
231
|
+
): Promise<WebexMessage> {
|
|
227
232
|
if (this.useInternalAPI) {
|
|
228
233
|
return this.sendMessageInternal(roomId, text, options)
|
|
229
234
|
}
|
|
230
|
-
const body
|
|
235
|
+
const body: Record<string, unknown> = { roomId }
|
|
236
|
+
if (options?.markdown) body.markdown = text
|
|
237
|
+
else body.text = text
|
|
238
|
+
if (options?.parentId) body.parentId = options.parentId
|
|
239
|
+
if (options?.files?.length) body.files = options.files
|
|
231
240
|
return this.request<WebexMessage>('POST', '/messages', body)
|
|
232
241
|
}
|
|
233
242
|
|
|
@@ -387,7 +396,10 @@ export class WebexClient {
|
|
|
387
396
|
return null
|
|
388
397
|
}
|
|
389
398
|
|
|
390
|
-
async listMessages(
|
|
399
|
+
async listMessages(
|
|
400
|
+
roomId: string,
|
|
401
|
+
options?: { max?: number; mentionedPeople?: string; parentId?: string },
|
|
402
|
+
): Promise<WebexMessage[]> {
|
|
391
403
|
if (this.useInternalAPI) {
|
|
392
404
|
const convUuid = this.decodeConvUuid(roomId)
|
|
393
405
|
const max = options?.max ?? 50
|
|
@@ -400,6 +412,8 @@ export class WebexClient {
|
|
|
400
412
|
const params = new URLSearchParams()
|
|
401
413
|
params.set('roomId', roomId)
|
|
402
414
|
params.set('max', String(options?.max ?? 50))
|
|
415
|
+
if (options?.mentionedPeople) params.set('mentionedPeople', options.mentionedPeople)
|
|
416
|
+
if (options?.parentId) params.set('parentId', options.parentId)
|
|
403
417
|
const data = await this.request<{ items: WebexMessage[] }>('GET', `/messages?${params}`)
|
|
404
418
|
return data.items
|
|
405
419
|
}
|
|
@@ -493,6 +507,10 @@ export class WebexClient {
|
|
|
493
507
|
return data.items
|
|
494
508
|
}
|
|
495
509
|
|
|
510
|
+
async getPerson(personId: string): Promise<WebexPerson> {
|
|
511
|
+
return this.request<WebexPerson>('GET', `/people/${personId}`)
|
|
512
|
+
}
|
|
513
|
+
|
|
496
514
|
async listMyMemberships(options?: { max?: number }): Promise<WebexMembership[]> {
|
|
497
515
|
const params = new URLSearchParams()
|
|
498
516
|
params.set('max', String(options?.max ?? 100))
|
|
@@ -507,6 +525,82 @@ export class WebexClient {
|
|
|
507
525
|
const data = await this.request<{ items: WebexMembership[] }>('GET', `/memberships?${params}`)
|
|
508
526
|
return data.items
|
|
509
527
|
}
|
|
528
|
+
|
|
529
|
+
async uploadFile(
|
|
530
|
+
roomId: string,
|
|
531
|
+
file: { content: Blob; filename: string },
|
|
532
|
+
options?: { text?: string; markdown?: boolean; parentId?: string },
|
|
533
|
+
): Promise<WebexMessage> {
|
|
534
|
+
const form = new FormData()
|
|
535
|
+
form.set('roomId', roomId)
|
|
536
|
+
if (options?.text) {
|
|
537
|
+
form.set(options.markdown ? 'markdown' : 'text', options.text)
|
|
538
|
+
}
|
|
539
|
+
if (options?.parentId) form.set('parentId', options.parentId)
|
|
540
|
+
form.set('files', file.content, file.filename)
|
|
541
|
+
|
|
542
|
+
const response = await fetch(`${BASE_URL}/messages`, {
|
|
543
|
+
method: 'POST',
|
|
544
|
+
headers: { Authorization: `Bearer ${this.ensureAuth()}` },
|
|
545
|
+
body: form,
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
if (!response.ok) {
|
|
549
|
+
const errorBody = (await response.json().catch(() => null)) as { message?: string } | null
|
|
550
|
+
throw new WebexError(errorBody?.message ?? `HTTP ${response.status}`, `http_${response.status}`)
|
|
551
|
+
}
|
|
552
|
+
return response.json() as Promise<WebexMessage>
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async downloadContent(contentRef: string): Promise<{ data: ArrayBuffer; filename: string; contentType: string }> {
|
|
556
|
+
const url = this.resolveContentUrl(contentRef)
|
|
557
|
+
const response = await fetch(url, {
|
|
558
|
+
headers: { Authorization: `Bearer ${this.ensureAuth()}` },
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
if (!response.ok) {
|
|
562
|
+
const errorBody = (await response.json().catch(() => null)) as { message?: string } | null
|
|
563
|
+
throw new WebexError(errorBody?.message ?? `HTTP ${response.status}`, `http_${response.status}`)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const disposition = response.headers.get('Content-Disposition') ?? ''
|
|
567
|
+
const match = disposition.match(/filename="?([^"]+)"?/)
|
|
568
|
+
const filename = sanitizeFilename(match?.[1]) ?? sanitizeFilename(contentRef.split('/').pop()) ?? 'download'
|
|
569
|
+
const contentType = response.headers.get('Content-Type') ?? 'application/octet-stream'
|
|
570
|
+
const data = await response.arrayBuffer()
|
|
571
|
+
return { data, filename, contentType }
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private resolveContentUrl(contentRef: string): string {
|
|
575
|
+
// A bare content id never contains a scheme or path separators.
|
|
576
|
+
if (!contentRef.includes('://') && !contentRef.includes('/')) {
|
|
577
|
+
return `${BASE_URL}/contents/${encodeURIComponent(contentRef)}`
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Only attach the bearer token to HTTPS Webex content URLs to avoid
|
|
581
|
+
// leaking credentials to attacker-controlled hosts (SSRF/token exfiltration).
|
|
582
|
+
let parsed: URL
|
|
583
|
+
try {
|
|
584
|
+
parsed = new URL(contentRef)
|
|
585
|
+
} catch {
|
|
586
|
+
throw new WebexError(`Invalid content reference: ${contentRef}`, 'invalid_content_ref')
|
|
587
|
+
}
|
|
588
|
+
if (parsed.protocol !== 'https:' || parsed.host !== CONTENT_HOST || !parsed.pathname.startsWith('/v1/contents/')) {
|
|
589
|
+
throw new WebexError(
|
|
590
|
+
`Refusing to download from untrusted location: ${parsed.origin}${parsed.pathname}`,
|
|
591
|
+
'untrusted_content_url',
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
return parsed.toString()
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function sanitizeFilename(name: string | undefined): string | undefined {
|
|
599
|
+
if (!name) return undefined
|
|
600
|
+
// Strip any path components so a server-supplied name cannot escape the target directory.
|
|
601
|
+
const base = name.replace(/\\/g, '/').split('/').pop()
|
|
602
|
+
if (!base || base === '.' || base === '..') return undefined
|
|
603
|
+
return base
|
|
510
604
|
}
|
|
511
605
|
|
|
512
606
|
interface InternalActivity {
|
|
@@ -1,45 +1,376 @@
|
|
|
1
1
|
export {}
|
|
2
2
|
|
|
3
3
|
declare module 'webex-message-handler' {
|
|
4
|
-
import
|
|
4
|
+
import { EventEmitter } from 'events'
|
|
5
5
|
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import type { JWK } from 'node-jose'
|
|
7
|
+
|
|
8
|
+
export interface Logger {
|
|
9
|
+
debug(message: string, ...args: unknown[]): void
|
|
10
|
+
info(message: string, ...args: unknown[]): void
|
|
11
|
+
warn(message: string, ...args: unknown[]): void
|
|
12
|
+
error(message: string, ...args: unknown[]): void
|
|
13
|
+
}
|
|
10
14
|
|
|
11
15
|
export const noopLogger: Logger
|
|
12
16
|
export const consoleLogger: Logger
|
|
13
17
|
|
|
18
|
+
export type NetworkMode = 'native' | 'injected'
|
|
19
|
+
|
|
20
|
+
export interface FetchRequest {
|
|
21
|
+
url: string
|
|
22
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
23
|
+
headers: Record<string, string>
|
|
24
|
+
body?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FetchResponse {
|
|
28
|
+
status: number
|
|
29
|
+
ok: boolean
|
|
30
|
+
json(): Promise<unknown>
|
|
31
|
+
text(): Promise<string>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type FetchFunction = (request: FetchRequest) => Promise<FetchResponse>
|
|
35
|
+
|
|
36
|
+
export interface InjectedWebSocket {
|
|
37
|
+
send(data: string): void
|
|
38
|
+
close(code?: number): void
|
|
39
|
+
readonly readyState: number
|
|
40
|
+
on(event: 'message', listener: (data: string) => void): void
|
|
41
|
+
on(event: 'open', listener: () => void): void
|
|
42
|
+
on(event: 'close', listener: (code: number, reason: string) => void): void
|
|
43
|
+
on(event: 'error', listener: (error: Error) => void): void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type WebSocketFactory = (url: string) => InjectedWebSocket
|
|
47
|
+
|
|
48
|
+
export interface WebexMessageHandlerConfig {
|
|
49
|
+
token: string
|
|
50
|
+
logger?: Logger
|
|
51
|
+
/** Networking mode: 'native' uses built-in fetch/WebSocket, 'injected' uses provided functions */
|
|
52
|
+
mode?: NetworkMode
|
|
53
|
+
/**
|
|
54
|
+
* Optional undici Dispatcher for native mode proxy support (HTTP + WebSocket).
|
|
55
|
+
* A single `ProxyAgent` proxies both `fetch()` and the native `WebSocket`.
|
|
56
|
+
* Example: `new ProxyAgent('http://proxy:8080')`
|
|
57
|
+
*/
|
|
58
|
+
dispatcher?: object
|
|
59
|
+
/** Custom fetch function for all HTTP requests (injected mode) */
|
|
60
|
+
fetch?: FetchFunction
|
|
61
|
+
/** Custom WebSocket factory (injected mode) */
|
|
62
|
+
webSocketFactory?: WebSocketFactory
|
|
63
|
+
/** Automatically filter out messages sent by this bot to prevent loops (default: true) */
|
|
64
|
+
ignoreSelfMessages?: boolean
|
|
65
|
+
/** Ping interval in ms (default: 15000) */
|
|
66
|
+
pingInterval?: number
|
|
67
|
+
/** Pong timeout in ms (default: 14000) */
|
|
68
|
+
pongTimeout?: number
|
|
69
|
+
/** Max reconnect backoff in ms (default: 32000) */
|
|
70
|
+
reconnectBackoffMax?: number
|
|
71
|
+
/** Max reconnect attempts before giving up (default: 10) */
|
|
72
|
+
maxReconnectAttempts?: number
|
|
73
|
+
/** Optional metrics callback for timing events (no overhead if not set) */
|
|
74
|
+
metricsCallback?: MetricsCallback
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface PersonInfo {
|
|
78
|
+
/** Person's unique ID */
|
|
79
|
+
id: string
|
|
80
|
+
/** Person's email address */
|
|
81
|
+
emails: string[]
|
|
82
|
+
/** Person's display name */
|
|
83
|
+
displayName: string
|
|
84
|
+
/** Person type (person or bot) */
|
|
85
|
+
type: 'person' | 'bot'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface DeviceRegistration {
|
|
89
|
+
/** The Mercury WebSocket URL */
|
|
90
|
+
webSocketUrl: string
|
|
91
|
+
/** The device URL (used as clientId for KMS) */
|
|
92
|
+
deviceUrl: string
|
|
93
|
+
/** The bot's user ID */
|
|
94
|
+
userId: string
|
|
95
|
+
/** Service catalog from WDM */
|
|
96
|
+
services: Record<string, string>
|
|
97
|
+
/** Encryption service URL extracted from services */
|
|
98
|
+
encryptionServiceUrl: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface MercuryActor {
|
|
102
|
+
id: string
|
|
103
|
+
objectType: string
|
|
104
|
+
emailAddress?: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface MercuryObject {
|
|
108
|
+
id: string
|
|
109
|
+
objectType: string
|
|
110
|
+
displayName?: string
|
|
111
|
+
content?: string
|
|
112
|
+
encryptionKeyUrl?: string
|
|
113
|
+
/** Card form input values (present on cardAction/submit activities). */
|
|
114
|
+
inputs?: Record<string, unknown>
|
|
115
|
+
/** File URLs attached to the message (present on file-share messages). */
|
|
116
|
+
files?: string[]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface MercuryTarget {
|
|
120
|
+
id: string
|
|
121
|
+
objectType: string
|
|
122
|
+
encryptionKeyUrl?: string
|
|
123
|
+
tags?: string[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface MercuryParent {
|
|
127
|
+
id: string
|
|
128
|
+
type: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface MercuryActivity {
|
|
132
|
+
id: string
|
|
133
|
+
/** Full Conversation-service activity URL, when present on the raw activity. */
|
|
134
|
+
url?: string
|
|
135
|
+
verb: string
|
|
136
|
+
actor: MercuryActor
|
|
137
|
+
object: MercuryObject
|
|
138
|
+
target: MercuryTarget
|
|
139
|
+
published: string
|
|
140
|
+
encryptionKeyUrl?: string
|
|
141
|
+
parent?: MercuryParent
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface MercuryEnvelope {
|
|
145
|
+
id: string
|
|
146
|
+
data: {
|
|
147
|
+
eventType: string
|
|
148
|
+
activity: MercuryActivity
|
|
149
|
+
}
|
|
150
|
+
timestamp: number
|
|
151
|
+
trackingId: string
|
|
152
|
+
sequenceNumber?: number
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface DecryptedMessage {
|
|
156
|
+
/** Mercury activity UUID. Works as parentId for threaded replies. */
|
|
157
|
+
id: string
|
|
158
|
+
/**
|
|
159
|
+
* Full Conversation-service activity URL, when present on the raw Mercury
|
|
160
|
+
* activity (e.g. for an outbound "acknowledge" read-receipt). Undefined if
|
|
161
|
+
* Mercury did not include it.
|
|
162
|
+
*/
|
|
163
|
+
url?: string
|
|
164
|
+
/** Parent activity UUID for threaded replies. Undefined if not a thread reply. */
|
|
165
|
+
parentId?: string
|
|
166
|
+
roomId: string
|
|
167
|
+
personId: string
|
|
168
|
+
personEmail: string
|
|
169
|
+
text: string
|
|
170
|
+
html?: string
|
|
171
|
+
created: string
|
|
172
|
+
roomType?: string
|
|
173
|
+
/** Person UUIDs mentioned via @mention in the message. */
|
|
174
|
+
mentionedPeople: string[]
|
|
175
|
+
/** Group mention types (e.g. "all") in the message. */
|
|
176
|
+
mentionedGroups: string[]
|
|
177
|
+
/** File URLs attached to the message. Empty if no files. */
|
|
178
|
+
files: string[]
|
|
179
|
+
raw: MercuryActivity
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface DeletedMessage {
|
|
183
|
+
messageId: string
|
|
184
|
+
roomId: string
|
|
185
|
+
personId: string
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface MembershipActivity {
|
|
189
|
+
/** Activity ID. */
|
|
190
|
+
id: string
|
|
191
|
+
/** ID of the person who performed the action. */
|
|
192
|
+
actorId: string
|
|
193
|
+
/** ID of the member affected. */
|
|
194
|
+
personId: string
|
|
195
|
+
/** Conversation/space ID. */
|
|
196
|
+
roomId: string
|
|
197
|
+
/** Membership action: "add", "leave", "assignModerator", or "unassignModerator". */
|
|
198
|
+
action: string
|
|
199
|
+
/** ISO 8601 timestamp. */
|
|
200
|
+
created: string
|
|
201
|
+
/** "direct", "group", or undefined. */
|
|
202
|
+
roomType?: string
|
|
203
|
+
/** Full raw activity for advanced use. */
|
|
204
|
+
raw: MercuryActivity
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface AttachmentAction {
|
|
208
|
+
/** Activity ID. */
|
|
209
|
+
id: string
|
|
210
|
+
/** ID of the message the card was attached to. */
|
|
211
|
+
messageId: string
|
|
212
|
+
/** ID of the person who submitted the card. */
|
|
213
|
+
personId: string
|
|
214
|
+
/** Email of the person who submitted the card. */
|
|
215
|
+
personEmail: string
|
|
216
|
+
/** Conversation/space ID. */
|
|
217
|
+
roomId: string
|
|
218
|
+
/** Card form input values. */
|
|
219
|
+
inputs: Record<string, unknown>
|
|
220
|
+
/** ISO 8601 timestamp. */
|
|
221
|
+
created: string
|
|
222
|
+
/** Full raw activity for advanced use. */
|
|
223
|
+
raw: MercuryActivity
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface RoomActivity {
|
|
227
|
+
/** Activity ID. */
|
|
228
|
+
id: string
|
|
229
|
+
/** Conversation/space ID. */
|
|
230
|
+
roomId: string
|
|
231
|
+
/** ID of the person who performed the action. */
|
|
232
|
+
actorId: string
|
|
233
|
+
/** Room action: "created" or "updated". */
|
|
234
|
+
action: string
|
|
235
|
+
/** ISO 8601 timestamp. */
|
|
236
|
+
created: string
|
|
237
|
+
/** Full raw activity for advanced use. */
|
|
238
|
+
raw: MercuryActivity
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export type ConnectionStatus = 'connected' | 'connecting' | 'reconnecting' | 'disconnected'
|
|
242
|
+
|
|
243
|
+
export interface HandlerStatus {
|
|
244
|
+
/** Overall connection state. */
|
|
245
|
+
status: ConnectionStatus
|
|
246
|
+
/** Whether the WebSocket is currently open. */
|
|
247
|
+
webSocketOpen: boolean
|
|
248
|
+
/** Whether the KMS encryption context is initialized. */
|
|
249
|
+
kmsInitialized: boolean
|
|
250
|
+
/** Whether the device is registered with WDM. */
|
|
251
|
+
deviceRegistered: boolean
|
|
252
|
+
/** Current auto-reconnect attempt number (0 if not reconnecting). */
|
|
253
|
+
reconnectAttempt: number
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface MetricsEvent {
|
|
257
|
+
/** Metric name: "connect", "kms_fetch", or "decrypt". */
|
|
258
|
+
name: string
|
|
259
|
+
/** Duration in milliseconds. */
|
|
260
|
+
durationMs: number
|
|
261
|
+
/** Whether the operation succeeded. */
|
|
262
|
+
success: boolean
|
|
263
|
+
/** Optional context metadata (e.g., key URI for kms_fetch). */
|
|
264
|
+
metadata?: Record<string, string>
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export type MetricsCallback = (event: MetricsEvent) => void
|
|
268
|
+
|
|
269
|
+
export interface WebexMessageHandlerEvents {
|
|
270
|
+
'message:created': (msg: DecryptedMessage) => void
|
|
271
|
+
'message:updated': (msg: DecryptedMessage) => void
|
|
272
|
+
'message:deleted': (data: DeletedMessage) => void
|
|
273
|
+
'membership:created': (activity: MembershipActivity) => void
|
|
274
|
+
'attachmentAction:created': (action: AttachmentAction) => void
|
|
275
|
+
'room:created': (activity: RoomActivity) => void
|
|
276
|
+
'room:updated': (activity: RoomActivity) => void
|
|
277
|
+
connected: () => void
|
|
278
|
+
disconnected: (reason: string) => void
|
|
279
|
+
reconnecting: (attempt: number) => void
|
|
280
|
+
error: (err: Error) => void
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
interface TypedEventEmitter<T> {
|
|
284
|
+
on<K extends keyof T>(event: K, listener: T[K]): this
|
|
285
|
+
emit<K extends keyof T>(
|
|
286
|
+
event: K,
|
|
287
|
+
...args: Parameters<T[K] extends (...a: infer P) => unknown ? (...a: P) => unknown : never>
|
|
288
|
+
): boolean
|
|
289
|
+
off<K extends keyof T>(event: K, listener: T[K]): this
|
|
290
|
+
once<K extends keyof T>(event: K, listener: T[K]): this
|
|
291
|
+
removeAllListeners<K extends keyof T>(event?: K): this
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export class WebexMessageHandler extends EventEmitter implements TypedEventEmitter<WebexMessageHandlerEvents> {
|
|
295
|
+
constructor(config: WebexMessageHandlerConfig)
|
|
296
|
+
connect(): Promise<void>
|
|
297
|
+
disconnect(): Promise<void>
|
|
298
|
+
reconnect(newToken: string): Promise<void>
|
|
299
|
+
get connected(): boolean
|
|
300
|
+
status(): HandlerStatus
|
|
301
|
+
deviceRegistration(): DeviceRegistration | null
|
|
302
|
+
serviceUrl(name: string): string | undefined
|
|
303
|
+
on<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this
|
|
304
|
+
emit<K extends keyof WebexMessageHandlerEvents>(
|
|
305
|
+
event: K,
|
|
306
|
+
...args: Parameters<
|
|
307
|
+
WebexMessageHandlerEvents[K] extends (...a: infer P) => unknown ? (...a: P) => unknown : never
|
|
308
|
+
>
|
|
309
|
+
): boolean
|
|
310
|
+
off<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this
|
|
311
|
+
once<K extends keyof WebexMessageHandlerEvents>(event: K, listener: WebexMessageHandlerEvents[K]): this
|
|
312
|
+
removeAllListeners<K extends keyof WebexMessageHandlerEvents>(event?: K): this
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
type HttpDoFn = (request: FetchRequest) => Promise<FetchResponse>
|
|
316
|
+
|
|
317
|
+
export interface DeviceManagerOptions {
|
|
318
|
+
logger?: Logger
|
|
319
|
+
httpDo: HttpDoFn
|
|
320
|
+
}
|
|
321
|
+
|
|
14
322
|
export class DeviceManager {
|
|
15
|
-
constructor(options:
|
|
16
|
-
register(token: string): Promise<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
323
|
+
constructor(options: DeviceManagerOptions)
|
|
324
|
+
register(token: string): Promise<DeviceRegistration>
|
|
325
|
+
refresh(token: string): Promise<DeviceRegistration>
|
|
326
|
+
unregister(token: string): Promise<void>
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface MercurySocketOptions {
|
|
330
|
+
logger?: Logger
|
|
331
|
+
wsFactory: WebSocketFactory
|
|
332
|
+
pingInterval?: number
|
|
333
|
+
pongTimeout?: number
|
|
334
|
+
reconnectBackoffMax?: number
|
|
335
|
+
maxReconnectAttempts?: number
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export class MercurySocket extends EventEmitter {
|
|
339
|
+
constructor(options: MercurySocketOptions)
|
|
340
|
+
connect(url: string, token: string): Promise<void>
|
|
29
341
|
disconnect(): Promise<void>
|
|
342
|
+
get connected(): boolean
|
|
343
|
+
get currentReconnectAttempts(): number
|
|
344
|
+
on(event: 'kms:response', handler: (data: unknown) => void): this
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface KmsClientConfig {
|
|
348
|
+
token: string
|
|
349
|
+
deviceUrl: string
|
|
350
|
+
userId: string
|
|
351
|
+
encryptionServiceUrl: string
|
|
352
|
+
logger?: Logger
|
|
353
|
+
httpDo: HttpDoFn
|
|
30
354
|
}
|
|
31
355
|
|
|
32
356
|
export class KmsClient {
|
|
33
|
-
constructor(
|
|
34
|
-
token: string
|
|
35
|
-
deviceUrl: string
|
|
36
|
-
userId: string
|
|
37
|
-
encryptionServiceUrl: string
|
|
38
|
-
logger: Logger
|
|
39
|
-
httpDo: HttpDo
|
|
40
|
-
})
|
|
41
|
-
initialize(): Promise<void>
|
|
42
|
-
getKey(keyUri: string): Promise<jose.JWK.Key | null>
|
|
357
|
+
constructor(config: KmsClientConfig)
|
|
43
358
|
handleKmsMessage(data: unknown): void
|
|
359
|
+
initialize(): Promise<void>
|
|
360
|
+
getKey(keyUri: string): Promise<JWK.Key>
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export class MessageDecryptor {
|
|
364
|
+
constructor({ kmsClient, logger }: { kmsClient: KmsClient; logger?: Logger })
|
|
365
|
+
decryptActivity(activity: MercuryActivity): Promise<MercuryActivity>
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface ParsedMentions {
|
|
369
|
+
mentionedPeople: string[]
|
|
370
|
+
mentionedGroups: string[]
|
|
44
371
|
}
|
|
372
|
+
|
|
373
|
+
export function parseMentions(html: string | undefined | null): ParsedMentions
|
|
374
|
+
export function toRestId(uuid: string, type: 'MESSAGE' | 'PEOPLE' | 'ROOM'): string
|
|
375
|
+
export function fromRestId(restId: string): string
|
|
45
376
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
|
|
5
|
+
import pkg from '../../../package.json' with { type: 'json' }
|
|
6
|
+
import {
|
|
7
|
+
authCommand,
|
|
8
|
+
fileCommand,
|
|
9
|
+
listenCommand,
|
|
10
|
+
memberCommand,
|
|
11
|
+
messageCommand,
|
|
12
|
+
snapshotCommand,
|
|
13
|
+
spaceCommand,
|
|
14
|
+
userCommand,
|
|
15
|
+
whoamiCommand,
|
|
16
|
+
} from './commands/index'
|
|
17
|
+
|
|
18
|
+
const program = new Command()
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.name('agent-webexbot')
|
|
22
|
+
.description('CLI tool for Webex bot integration using bot tokens')
|
|
23
|
+
.version(pkg.version)
|
|
24
|
+
.option('--pretty', 'Pretty-print JSON output')
|
|
25
|
+
.option('--bot <id>', 'Bot ID to use')
|
|
26
|
+
.hook('preAction', (thisCmd, actionCmd) => {
|
|
27
|
+
for (const [key, value] of Object.entries(thisCmd.opts())) {
|
|
28
|
+
if (value === undefined) continue
|
|
29
|
+
const source = actionCmd.getOptionValueSource(key)
|
|
30
|
+
if (source === undefined || source === 'default') {
|
|
31
|
+
actionCmd.setOptionValue(key, value)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
program.addCommand(authCommand)
|
|
37
|
+
program.addCommand(whoamiCommand)
|
|
38
|
+
program.addCommand(messageCommand)
|
|
39
|
+
program.addCommand(spaceCommand)
|
|
40
|
+
program.addCommand(memberCommand)
|
|
41
|
+
program.addCommand(userCommand)
|
|
42
|
+
program.addCommand(fileCommand)
|
|
43
|
+
program.addCommand(snapshotCommand)
|
|
44
|
+
program.addCommand(listenCommand)
|
|
45
|
+
|
|
46
|
+
program.parseAsync(process.argv)
|
|
47
|
+
|
|
48
|
+
export default program
|