agent-messenger 1.2.0 → 1.3.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 +1 -1
- package/dist/package.json +3 -2
- package/dist/src/platforms/slackbot/cli.d.ts +5 -0
- package/dist/src/platforms/slackbot/cli.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/cli.js +19 -0
- package/dist/src/platforms/slackbot/cli.js.map +1 -0
- package/dist/src/platforms/slackbot/client.d.ts +43 -0
- package/dist/src/platforms/slackbot/client.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/client.js +347 -0
- package/dist/src/platforms/slackbot/client.js.map +1 -0
- package/dist/src/platforms/slackbot/commands/auth.d.ts +35 -0
- package/dist/src/platforms/slackbot/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/commands/auth.js +185 -0
- package/dist/src/platforms/slackbot/commands/auth.js.map +1 -0
- package/dist/src/platforms/slackbot/commands/channel.d.ts +3 -0
- package/dist/src/platforms/slackbot/commands/channel.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/commands/channel.js +40 -0
- package/dist/src/platforms/slackbot/commands/channel.js.map +1 -0
- package/dist/src/platforms/slackbot/commands/index.d.ts +6 -0
- package/dist/src/platforms/slackbot/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/commands/index.js +6 -0
- package/dist/src/platforms/slackbot/commands/index.js.map +1 -0
- package/dist/src/platforms/slackbot/commands/message.d.ts +3 -0
- package/dist/src/platforms/slackbot/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/commands/message.js +135 -0
- package/dist/src/platforms/slackbot/commands/message.js.map +1 -0
- package/dist/src/platforms/slackbot/commands/reaction.d.ts +3 -0
- package/dist/src/platforms/slackbot/commands/reaction.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/commands/reaction.js +43 -0
- package/dist/src/platforms/slackbot/commands/reaction.js.map +1 -0
- package/dist/src/platforms/slackbot/commands/shared.d.ts +9 -0
- package/dist/src/platforms/slackbot/commands/shared.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/commands/shared.js +13 -0
- package/dist/src/platforms/slackbot/commands/shared.js.map +1 -0
- package/dist/src/platforms/slackbot/commands/user.d.ts +3 -0
- package/dist/src/platforms/slackbot/commands/user.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/commands/user.js +40 -0
- package/dist/src/platforms/slackbot/commands/user.js.map +1 -0
- package/dist/src/platforms/slackbot/credential-manager.d.ts +18 -0
- package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/credential-manager.js +187 -0
- package/dist/src/platforms/slackbot/credential-manager.js.map +1 -0
- package/dist/src/platforms/slackbot/index.d.ts +4 -0
- package/dist/src/platforms/slackbot/index.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/index.js +4 -0
- package/dist/src/platforms/slackbot/index.js.map +1 -0
- package/dist/src/platforms/slackbot/types.d.ts +460 -0
- package/dist/src/platforms/slackbot/types.d.ts.map +1 -0
- package/dist/src/platforms/slackbot/types.js +114 -0
- package/dist/src/platforms/slackbot/types.js.map +1 -0
- package/docs/content/docs/integrations/meta.json +1 -1
- package/docs/content/docs/integrations/slackbot.mdx +204 -0
- package/docs/src/app/page.tsx +18 -1
- package/e2e/config.ts +26 -0
- package/e2e/helpers.ts +6 -1
- package/e2e/slackbot.e2e.test.ts +306 -0
- package/package.json +3 -2
- package/skills/agent-slackbot/SKILL.md +285 -0
- package/skills/agent-slackbot/references/authentication.md +253 -0
- package/skills/agent-slackbot/references/common-patterns.md +218 -0
- package/skills/agent-slackbot/templates/monitor-channel.sh +98 -0
- package/skills/agent-slackbot/templates/post-message.sh +107 -0
- package/skills/agent-slackbot/templates/workspace-summary.sh +113 -0
- package/src/platforms/slackbot/cli.ts +30 -0
- package/src/platforms/slackbot/client.test.ts +282 -0
- package/src/platforms/slackbot/client.ts +401 -0
- package/src/platforms/slackbot/commands/auth.test.ts +245 -0
- package/src/platforms/slackbot/commands/auth.ts +240 -0
- package/src/platforms/slackbot/commands/channel.ts +46 -0
- package/src/platforms/slackbot/commands/index.ts +5 -0
- package/src/platforms/slackbot/commands/message.ts +182 -0
- package/src/platforms/slackbot/commands/reaction.ts +59 -0
- package/src/platforms/slackbot/commands/shared.ts +23 -0
- package/src/platforms/slackbot/commands/user.ts +46 -0
- package/src/platforms/slackbot/credential-manager.test.ts +264 -0
- package/src/platforms/slackbot/credential-manager.ts +218 -0
- package/src/platforms/slackbot/index.ts +19 -0
- package/src/platforms/slackbot/types.test.ts +90 -0
- package/src/platforms/slackbot/types.ts +222 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { WebClient } from '@slack/web-api'
|
|
2
|
+
import { SlackBotError, type SlackChannel, type SlackMessage, type SlackUser } from './types'
|
|
3
|
+
|
|
4
|
+
const MAX_RETRIES = 3
|
|
5
|
+
const RATE_LIMIT_ERROR_CODE = 'slack_webapi_rate_limited_error'
|
|
6
|
+
|
|
7
|
+
export class SlackBotClient {
|
|
8
|
+
private client: WebClient
|
|
9
|
+
|
|
10
|
+
constructor(token: string) {
|
|
11
|
+
if (!token) {
|
|
12
|
+
throw new SlackBotError('Token is required', 'missing_token')
|
|
13
|
+
}
|
|
14
|
+
if (!token.startsWith('xoxb-')) {
|
|
15
|
+
throw new SlackBotError('Token must be a bot token (xoxb-)', 'invalid_token_type')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.client = new WebClient(token)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async withRetry<T>(operation: () => Promise<T>): Promise<T> {
|
|
22
|
+
let lastError: Error | undefined
|
|
23
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
24
|
+
try {
|
|
25
|
+
return await operation()
|
|
26
|
+
} catch (error: any) {
|
|
27
|
+
lastError = error
|
|
28
|
+
if (error.code === RATE_LIMIT_ERROR_CODE && attempt < MAX_RETRIES) {
|
|
29
|
+
const retryAfter = error.retryAfter || 1
|
|
30
|
+
await this.sleep(retryAfter * 1000 * (attempt + 1))
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw new SlackBotError(
|
|
37
|
+
lastError?.message || 'Unknown error',
|
|
38
|
+
(lastError as any)?.code || 'unknown_error'
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private sleep(ms: number): Promise<void> {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private checkResponse(response: { ok?: boolean; error?: string }): void {
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new SlackBotError(response.error || 'API call failed', response.error || 'api_error')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async testAuth(): Promise<{
|
|
53
|
+
user_id: string
|
|
54
|
+
team_id: string
|
|
55
|
+
bot_id?: string
|
|
56
|
+
user?: string
|
|
57
|
+
team?: string
|
|
58
|
+
}> {
|
|
59
|
+
return this.withRetry(async () => {
|
|
60
|
+
const response = await this.client.auth.test()
|
|
61
|
+
this.checkResponse(response)
|
|
62
|
+
return {
|
|
63
|
+
user_id: response.user_id!,
|
|
64
|
+
team_id: response.team_id!,
|
|
65
|
+
bot_id: response.bot_id,
|
|
66
|
+
user: response.user,
|
|
67
|
+
team: response.team,
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async postMessage(
|
|
73
|
+
channel: string,
|
|
74
|
+
text: string,
|
|
75
|
+
options?: { thread_ts?: string }
|
|
76
|
+
): Promise<SlackMessage> {
|
|
77
|
+
return this.withRetry(async () => {
|
|
78
|
+
const response = await this.client.chat.postMessage({
|
|
79
|
+
channel,
|
|
80
|
+
text,
|
|
81
|
+
thread_ts: options?.thread_ts,
|
|
82
|
+
})
|
|
83
|
+
this.checkResponse(response)
|
|
84
|
+
|
|
85
|
+
const msg = response.message!
|
|
86
|
+
return {
|
|
87
|
+
ts: response.ts!,
|
|
88
|
+
text: msg.text || text,
|
|
89
|
+
type: msg.type || 'message',
|
|
90
|
+
user: msg.user,
|
|
91
|
+
thread_ts: msg.thread_ts,
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getConversationHistory(
|
|
97
|
+
channel: string,
|
|
98
|
+
options?: { limit?: number; cursor?: string }
|
|
99
|
+
): Promise<SlackMessage[]> {
|
|
100
|
+
return this.withRetry(async () => {
|
|
101
|
+
const response = await this.client.conversations.history({
|
|
102
|
+
channel,
|
|
103
|
+
limit: options?.limit || 20,
|
|
104
|
+
cursor: options?.cursor,
|
|
105
|
+
})
|
|
106
|
+
this.checkResponse(response)
|
|
107
|
+
|
|
108
|
+
return (response.messages || []).map((msg) => ({
|
|
109
|
+
ts: msg.ts!,
|
|
110
|
+
text: msg.text || '',
|
|
111
|
+
type: msg.type || 'message',
|
|
112
|
+
user: msg.user,
|
|
113
|
+
username: msg.username,
|
|
114
|
+
thread_ts: msg.thread_ts,
|
|
115
|
+
reply_count: msg.reply_count,
|
|
116
|
+
replies: (msg as any).replies,
|
|
117
|
+
edited: msg.edited
|
|
118
|
+
? {
|
|
119
|
+
user: msg.edited.user || '',
|
|
120
|
+
ts: msg.edited.ts || '',
|
|
121
|
+
}
|
|
122
|
+
: undefined,
|
|
123
|
+
}))
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async getMessage(channel: string, ts: string): Promise<SlackMessage | null> {
|
|
128
|
+
return this.withRetry(async () => {
|
|
129
|
+
const response = await this.client.conversations.history({
|
|
130
|
+
channel,
|
|
131
|
+
oldest: ts,
|
|
132
|
+
inclusive: true,
|
|
133
|
+
limit: 1,
|
|
134
|
+
})
|
|
135
|
+
this.checkResponse(response)
|
|
136
|
+
|
|
137
|
+
const msg = response.messages?.[0]
|
|
138
|
+
if (!msg || msg.ts !== ts) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
ts: msg.ts!,
|
|
144
|
+
text: msg.text || '',
|
|
145
|
+
type: msg.type || 'message',
|
|
146
|
+
user: msg.user,
|
|
147
|
+
username: msg.username,
|
|
148
|
+
thread_ts: msg.thread_ts,
|
|
149
|
+
reply_count: msg.reply_count,
|
|
150
|
+
replies: (msg as any).replies,
|
|
151
|
+
edited: msg.edited
|
|
152
|
+
? {
|
|
153
|
+
user: msg.edited.user || '',
|
|
154
|
+
ts: msg.edited.ts || '',
|
|
155
|
+
}
|
|
156
|
+
: undefined,
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async addReaction(channel: string, timestamp: string, emoji: string): Promise<void> {
|
|
162
|
+
// Normalize emoji (remove colons if present)
|
|
163
|
+
const normalizedEmoji = emoji.replace(/^:|:$/g, '')
|
|
164
|
+
|
|
165
|
+
return this.withRetry(async () => {
|
|
166
|
+
const response = await this.client.reactions.add({
|
|
167
|
+
channel,
|
|
168
|
+
timestamp,
|
|
169
|
+
name: normalizedEmoji,
|
|
170
|
+
})
|
|
171
|
+
this.checkResponse(response)
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async removeReaction(channel: string, timestamp: string, emoji: string): Promise<void> {
|
|
176
|
+
// Normalize emoji (remove colons if present)
|
|
177
|
+
const normalizedEmoji = emoji.replace(/^:|:$/g, '')
|
|
178
|
+
|
|
179
|
+
return this.withRetry(async () => {
|
|
180
|
+
const response = await this.client.reactions.remove({
|
|
181
|
+
channel,
|
|
182
|
+
timestamp,
|
|
183
|
+
name: normalizedEmoji,
|
|
184
|
+
})
|
|
185
|
+
this.checkResponse(response)
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async listChannels(options?: { limit?: number; cursor?: string }): Promise<SlackChannel[]> {
|
|
190
|
+
const channels: SlackChannel[] = []
|
|
191
|
+
let cursor: string | undefined = options?.cursor
|
|
192
|
+
|
|
193
|
+
do {
|
|
194
|
+
// Only wrap individual API call in withRetry, not the entire loop
|
|
195
|
+
const response = await this.withRetry(async () => {
|
|
196
|
+
const res = await this.client.conversations.list({
|
|
197
|
+
cursor,
|
|
198
|
+
limit: options?.limit || 200,
|
|
199
|
+
types: 'public_channel,private_channel',
|
|
200
|
+
})
|
|
201
|
+
this.checkResponse(res)
|
|
202
|
+
return res
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
if (response.channels) {
|
|
206
|
+
for (const ch of response.channels) {
|
|
207
|
+
channels.push({
|
|
208
|
+
id: ch.id!,
|
|
209
|
+
name: ch.name!,
|
|
210
|
+
is_private: ch.is_private || false,
|
|
211
|
+
is_archived: ch.is_archived || false,
|
|
212
|
+
created: ch.created || 0,
|
|
213
|
+
creator: ch.creator || '',
|
|
214
|
+
topic: ch.topic
|
|
215
|
+
? {
|
|
216
|
+
value: ch.topic.value || '',
|
|
217
|
+
creator: ch.topic.creator || '',
|
|
218
|
+
last_set: ch.topic.last_set || 0,
|
|
219
|
+
}
|
|
220
|
+
: undefined,
|
|
221
|
+
purpose: ch.purpose
|
|
222
|
+
? {
|
|
223
|
+
value: ch.purpose.value || '',
|
|
224
|
+
creator: ch.purpose.creator || '',
|
|
225
|
+
last_set: ch.purpose.last_set || 0,
|
|
226
|
+
}
|
|
227
|
+
: undefined,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
cursor = response.response_metadata?.next_cursor
|
|
233
|
+
// Only paginate if no specific limit was requested
|
|
234
|
+
if (options?.limit) break
|
|
235
|
+
} while (cursor)
|
|
236
|
+
|
|
237
|
+
return channels
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async getChannelInfo(channel: string): Promise<SlackChannel> {
|
|
241
|
+
return this.withRetry(async () => {
|
|
242
|
+
const response = await this.client.conversations.info({ channel })
|
|
243
|
+
this.checkResponse(response)
|
|
244
|
+
|
|
245
|
+
const ch = response.channel!
|
|
246
|
+
return {
|
|
247
|
+
id: ch.id!,
|
|
248
|
+
name: ch.name!,
|
|
249
|
+
is_private: ch.is_private || false,
|
|
250
|
+
is_archived: ch.is_archived || false,
|
|
251
|
+
created: ch.created || 0,
|
|
252
|
+
creator: ch.creator || '',
|
|
253
|
+
topic: ch.topic
|
|
254
|
+
? {
|
|
255
|
+
value: ch.topic.value || '',
|
|
256
|
+
creator: ch.topic.creator || '',
|
|
257
|
+
last_set: ch.topic.last_set || 0,
|
|
258
|
+
}
|
|
259
|
+
: undefined,
|
|
260
|
+
purpose: ch.purpose
|
|
261
|
+
? {
|
|
262
|
+
value: ch.purpose.value || '',
|
|
263
|
+
creator: ch.purpose.creator || '',
|
|
264
|
+
last_set: ch.purpose.last_set || 0,
|
|
265
|
+
}
|
|
266
|
+
: undefined,
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async listUsers(options?: { limit?: number; cursor?: string }): Promise<SlackUser[]> {
|
|
272
|
+
const users: SlackUser[] = []
|
|
273
|
+
let cursor: string | undefined = options?.cursor
|
|
274
|
+
|
|
275
|
+
do {
|
|
276
|
+
// Only wrap individual API call in withRetry, not the entire loop
|
|
277
|
+
const response = await this.withRetry(async () => {
|
|
278
|
+
const res = await this.client.users.list({
|
|
279
|
+
cursor,
|
|
280
|
+
limit: options?.limit || 200,
|
|
281
|
+
})
|
|
282
|
+
this.checkResponse(res)
|
|
283
|
+
return res
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
if (response.members) {
|
|
287
|
+
for (const member of response.members) {
|
|
288
|
+
users.push({
|
|
289
|
+
id: member.id!,
|
|
290
|
+
name: member.name!,
|
|
291
|
+
real_name: member.real_name || member.name || '',
|
|
292
|
+
is_admin: member.is_admin || false,
|
|
293
|
+
is_owner: member.is_owner || false,
|
|
294
|
+
is_bot: member.is_bot || false,
|
|
295
|
+
is_app_user: member.is_app_user || false,
|
|
296
|
+
profile: member.profile
|
|
297
|
+
? {
|
|
298
|
+
email: member.profile.email,
|
|
299
|
+
phone: member.profile.phone,
|
|
300
|
+
title: member.profile.title,
|
|
301
|
+
status_text: member.profile.status_text,
|
|
302
|
+
}
|
|
303
|
+
: undefined,
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
cursor = response.response_metadata?.next_cursor
|
|
309
|
+
// Only paginate if no specific limit was requested
|
|
310
|
+
if (options?.limit) break
|
|
311
|
+
} while (cursor)
|
|
312
|
+
|
|
313
|
+
return users
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async getUserInfo(userId: string): Promise<SlackUser> {
|
|
317
|
+
return this.withRetry(async () => {
|
|
318
|
+
const response = await this.client.users.info({ user: userId })
|
|
319
|
+
this.checkResponse(response)
|
|
320
|
+
|
|
321
|
+
const member = response.user!
|
|
322
|
+
return {
|
|
323
|
+
id: member.id!,
|
|
324
|
+
name: member.name!,
|
|
325
|
+
real_name: member.real_name || member.name || '',
|
|
326
|
+
is_admin: member.is_admin || false,
|
|
327
|
+
is_owner: member.is_owner || false,
|
|
328
|
+
is_bot: member.is_bot || false,
|
|
329
|
+
is_app_user: member.is_app_user || false,
|
|
330
|
+
profile: member.profile
|
|
331
|
+
? {
|
|
332
|
+
email: member.profile.email,
|
|
333
|
+
phone: member.profile.phone,
|
|
334
|
+
title: member.profile.title,
|
|
335
|
+
status_text: member.profile.status_text,
|
|
336
|
+
}
|
|
337
|
+
: undefined,
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async updateMessage(channel: string, ts: string, text: string): Promise<SlackMessage> {
|
|
343
|
+
return this.withRetry(async () => {
|
|
344
|
+
const response = await this.client.chat.update({ channel, ts, text })
|
|
345
|
+
this.checkResponse(response)
|
|
346
|
+
const msg = (response as any).message
|
|
347
|
+
return {
|
|
348
|
+
ts: response.ts!,
|
|
349
|
+
text: msg?.text || response.text || text,
|
|
350
|
+
type: msg?.type || 'message',
|
|
351
|
+
user: msg?.user,
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async getThreadReplies(
|
|
357
|
+
channel: string,
|
|
358
|
+
ts: string,
|
|
359
|
+
options?: { limit?: number; cursor?: string }
|
|
360
|
+
): Promise<SlackMessage[]> {
|
|
361
|
+
return this.withRetry(async () => {
|
|
362
|
+
const response = await this.client.conversations.replies({
|
|
363
|
+
channel,
|
|
364
|
+
ts,
|
|
365
|
+
limit: options?.limit || 100,
|
|
366
|
+
cursor: options?.cursor,
|
|
367
|
+
})
|
|
368
|
+
this.checkResponse(response)
|
|
369
|
+
|
|
370
|
+
return (response.messages || []).map((msg: any) => ({
|
|
371
|
+
ts: msg.ts!,
|
|
372
|
+
text: msg.text || '',
|
|
373
|
+
type: msg.type || 'message',
|
|
374
|
+
user: msg.user,
|
|
375
|
+
username: msg.username,
|
|
376
|
+
thread_ts: msg.thread_ts,
|
|
377
|
+
reply_count: msg.reply_count,
|
|
378
|
+
edited: msg.edited
|
|
379
|
+
? {
|
|
380
|
+
user: msg.edited.user || '',
|
|
381
|
+
ts: msg.edited.ts || '',
|
|
382
|
+
}
|
|
383
|
+
: undefined,
|
|
384
|
+
}))
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async joinChannel(channel: string): Promise<void> {
|
|
389
|
+
return this.withRetry(async () => {
|
|
390
|
+
const response = await this.client.conversations.join({ channel })
|
|
391
|
+
this.checkResponse(response)
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async deleteMessage(channel: string, ts: string): Promise<void> {
|
|
396
|
+
return this.withRetry(async () => {
|
|
397
|
+
const response = await this.client.chat.delete({ channel, ts })
|
|
398
|
+
this.checkResponse(response)
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
import { existsSync, rmSync } from 'node:fs'
|
|
3
|
+
import { mkdir } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const mockTestAuth = mock(() =>
|
|
8
|
+
Promise.resolve({
|
|
9
|
+
ok: true,
|
|
10
|
+
user_id: 'U123',
|
|
11
|
+
team_id: 'T456',
|
|
12
|
+
bot_id: 'B789',
|
|
13
|
+
user: 'testbot',
|
|
14
|
+
team: 'Test Team',
|
|
15
|
+
})
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
mock.module('../client', () => ({
|
|
19
|
+
SlackBotClient: class MockSlackBotClient {
|
|
20
|
+
constructor(token: string) {
|
|
21
|
+
if (!token.startsWith('xoxb-')) {
|
|
22
|
+
throw new Error('Token must be a bot token (xoxb-)')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
testAuth = mockTestAuth
|
|
26
|
+
},
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
import { SlackBotCredentialManager } from '../credential-manager'
|
|
30
|
+
import { clearAction, listAction, removeAction, setAction, statusAction, useAction } from './auth'
|
|
31
|
+
|
|
32
|
+
describe('auth commands', () => {
|
|
33
|
+
let tempDir: string
|
|
34
|
+
let originalEnv: NodeJS.ProcessEnv
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
tempDir = join(tmpdir(), `slackbot-auth-test-${Date.now()}`)
|
|
38
|
+
await mkdir(tempDir, { recursive: true })
|
|
39
|
+
originalEnv = { ...process.env }
|
|
40
|
+
mockTestAuth.mockClear()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
if (existsSync(tempDir)) {
|
|
45
|
+
rmSync(tempDir, { recursive: true })
|
|
46
|
+
}
|
|
47
|
+
process.env = originalEnv
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('setAction', () => {
|
|
51
|
+
test('validates and stores bot token with default bot_id from auth', async () => {
|
|
52
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
53
|
+
|
|
54
|
+
const result = await setAction('xoxb-test-token', { _credManager: manager })
|
|
55
|
+
|
|
56
|
+
expect(result.success).toBe(true)
|
|
57
|
+
expect(result.workspace_id).toBe('T456')
|
|
58
|
+
expect(result.bot_id).toBe('B789')
|
|
59
|
+
|
|
60
|
+
const creds = await manager.getCredentials()
|
|
61
|
+
expect(creds?.token).toBe('xoxb-test-token')
|
|
62
|
+
expect(creds?.bot_id).toBe('B789')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('uses --bot flag as bot_id', async () => {
|
|
66
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
67
|
+
|
|
68
|
+
const result = await setAction('xoxb-test-token', { bot: 'deploy', _credManager: manager })
|
|
69
|
+
|
|
70
|
+
expect(result.bot_id).toBe('deploy')
|
|
71
|
+
const creds = await manager.getCredentials('deploy')
|
|
72
|
+
expect(creds?.token).toBe('xoxb-test-token')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('rejects user tokens', async () => {
|
|
76
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
77
|
+
|
|
78
|
+
const result = await setAction('xoxp-user-token', { _credManager: manager })
|
|
79
|
+
|
|
80
|
+
expect(result.error).toBeDefined()
|
|
81
|
+
expect(result.error).toContain('bot token')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('rejects invalid token format', async () => {
|
|
85
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
86
|
+
|
|
87
|
+
const result = await setAction('invalid-token', { _credManager: manager })
|
|
88
|
+
|
|
89
|
+
expect(result.error).toBeDefined()
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('clearAction', () => {
|
|
94
|
+
test('removes all stored credentials', async () => {
|
|
95
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
96
|
+
await manager.setCredentials({
|
|
97
|
+
token: 'xoxb-token',
|
|
98
|
+
workspace_id: 'T123',
|
|
99
|
+
workspace_name: 'Test',
|
|
100
|
+
bot_id: 'mybot',
|
|
101
|
+
bot_name: 'My Bot',
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const result = await clearAction({ _credManager: manager })
|
|
105
|
+
|
|
106
|
+
expect(result.success).toBe(true)
|
|
107
|
+
expect(await manager.getCredentials()).toBeNull()
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('statusAction', () => {
|
|
112
|
+
test('returns no credentials when none set', async () => {
|
|
113
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
114
|
+
|
|
115
|
+
const result = await statusAction({ _credManager: manager })
|
|
116
|
+
|
|
117
|
+
expect(result.valid).toBe(false)
|
|
118
|
+
expect(result.error).toBeDefined()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('returns valid status for current bot', async () => {
|
|
122
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
123
|
+
await manager.setCredentials({
|
|
124
|
+
token: 'xoxb-token',
|
|
125
|
+
workspace_id: 'T456',
|
|
126
|
+
workspace_name: 'Test Workspace',
|
|
127
|
+
bot_id: 'mybot',
|
|
128
|
+
bot_name: 'My Bot',
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const result = await statusAction({ _credManager: manager })
|
|
132
|
+
|
|
133
|
+
expect(result.valid).toBe(true)
|
|
134
|
+
expect(result.workspace_id).toBe('T456')
|
|
135
|
+
expect(result.bot_id).toBe('mybot')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('returns status for specific --bot', async () => {
|
|
139
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
140
|
+
await manager.setCredentials({
|
|
141
|
+
token: 'xoxb-token',
|
|
142
|
+
workspace_id: 'T456',
|
|
143
|
+
workspace_name: 'Test',
|
|
144
|
+
bot_id: 'deploy',
|
|
145
|
+
bot_name: 'Deploy',
|
|
146
|
+
})
|
|
147
|
+
await manager.setCredentials({
|
|
148
|
+
token: 'xoxb-token2',
|
|
149
|
+
workspace_id: 'T456',
|
|
150
|
+
workspace_name: 'Test',
|
|
151
|
+
bot_id: 'alert',
|
|
152
|
+
bot_name: 'Alert',
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const result = await statusAction({ bot: 'deploy', _credManager: manager })
|
|
156
|
+
|
|
157
|
+
expect(result.valid).toBe(true)
|
|
158
|
+
expect(result.bot_id).toBe('deploy')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('listAction', () => {
|
|
163
|
+
test('returns all stored bots', async () => {
|
|
164
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
165
|
+
await manager.setCredentials({
|
|
166
|
+
token: 'xoxb-a',
|
|
167
|
+
workspace_id: 'T123',
|
|
168
|
+
workspace_name: 'WS A',
|
|
169
|
+
bot_id: 'deploy',
|
|
170
|
+
bot_name: 'Deploy',
|
|
171
|
+
})
|
|
172
|
+
await manager.setCredentials({
|
|
173
|
+
token: 'xoxb-b',
|
|
174
|
+
workspace_id: 'T123',
|
|
175
|
+
workspace_name: 'WS A',
|
|
176
|
+
bot_id: 'alert',
|
|
177
|
+
bot_name: 'Alert',
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const result = await listAction({ _credManager: manager })
|
|
181
|
+
|
|
182
|
+
expect(result.bots).toHaveLength(2)
|
|
183
|
+
expect(result.bots?.find((b) => b.bot_id === 'alert')?.is_current).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('useAction', () => {
|
|
188
|
+
test('switches current bot', async () => {
|
|
189
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
190
|
+
await manager.setCredentials({
|
|
191
|
+
token: 'xoxb-a',
|
|
192
|
+
workspace_id: 'T123',
|
|
193
|
+
workspace_name: 'WS',
|
|
194
|
+
bot_id: 'deploy',
|
|
195
|
+
bot_name: 'Deploy',
|
|
196
|
+
})
|
|
197
|
+
await manager.setCredentials({
|
|
198
|
+
token: 'xoxb-b',
|
|
199
|
+
workspace_id: 'T123',
|
|
200
|
+
workspace_name: 'WS',
|
|
201
|
+
bot_id: 'alert',
|
|
202
|
+
bot_name: 'Alert',
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const result = await useAction('deploy', { _credManager: manager })
|
|
206
|
+
|
|
207
|
+
expect(result.success).toBe(true)
|
|
208
|
+
expect(result.bot_id).toBe('deploy')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('returns error for unknown bot', async () => {
|
|
212
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
213
|
+
|
|
214
|
+
const result = await useAction('nonexistent', { _credManager: manager })
|
|
215
|
+
|
|
216
|
+
expect(result.error).toBeDefined()
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('removeAction', () => {
|
|
221
|
+
test('removes a stored bot', async () => {
|
|
222
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
223
|
+
await manager.setCredentials({
|
|
224
|
+
token: 'xoxb-a',
|
|
225
|
+
workspace_id: 'T123',
|
|
226
|
+
workspace_name: 'WS',
|
|
227
|
+
bot_id: 'deploy',
|
|
228
|
+
bot_name: 'Deploy',
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const result = await removeAction('deploy', { _credManager: manager })
|
|
232
|
+
|
|
233
|
+
expect(result.success).toBe(true)
|
|
234
|
+
expect(await manager.getCredentials('deploy')).toBeNull()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('returns error for unknown bot', async () => {
|
|
238
|
+
const manager = new SlackBotCredentialManager(tempDir)
|
|
239
|
+
|
|
240
|
+
const result = await removeAction('nonexistent', { _credManager: manager })
|
|
241
|
+
|
|
242
|
+
expect(result.error).toBeDefined()
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
})
|