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