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.
Files changed (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/package.json +3 -2
  4. package/dist/src/platforms/slackbot/cli.d.ts +5 -0
  5. package/dist/src/platforms/slackbot/cli.d.ts.map +1 -0
  6. package/dist/src/platforms/slackbot/cli.js +19 -0
  7. package/dist/src/platforms/slackbot/cli.js.map +1 -0
  8. package/dist/src/platforms/slackbot/client.d.ts +43 -0
  9. package/dist/src/platforms/slackbot/client.d.ts.map +1 -0
  10. package/dist/src/platforms/slackbot/client.js +347 -0
  11. package/dist/src/platforms/slackbot/client.js.map +1 -0
  12. package/dist/src/platforms/slackbot/commands/auth.d.ts +35 -0
  13. package/dist/src/platforms/slackbot/commands/auth.d.ts.map +1 -0
  14. package/dist/src/platforms/slackbot/commands/auth.js +185 -0
  15. package/dist/src/platforms/slackbot/commands/auth.js.map +1 -0
  16. package/dist/src/platforms/slackbot/commands/channel.d.ts +3 -0
  17. package/dist/src/platforms/slackbot/commands/channel.d.ts.map +1 -0
  18. package/dist/src/platforms/slackbot/commands/channel.js +40 -0
  19. package/dist/src/platforms/slackbot/commands/channel.js.map +1 -0
  20. package/dist/src/platforms/slackbot/commands/index.d.ts +6 -0
  21. package/dist/src/platforms/slackbot/commands/index.d.ts.map +1 -0
  22. package/dist/src/platforms/slackbot/commands/index.js +6 -0
  23. package/dist/src/platforms/slackbot/commands/index.js.map +1 -0
  24. package/dist/src/platforms/slackbot/commands/message.d.ts +3 -0
  25. package/dist/src/platforms/slackbot/commands/message.d.ts.map +1 -0
  26. package/dist/src/platforms/slackbot/commands/message.js +135 -0
  27. package/dist/src/platforms/slackbot/commands/message.js.map +1 -0
  28. package/dist/src/platforms/slackbot/commands/reaction.d.ts +3 -0
  29. package/dist/src/platforms/slackbot/commands/reaction.d.ts.map +1 -0
  30. package/dist/src/platforms/slackbot/commands/reaction.js +43 -0
  31. package/dist/src/platforms/slackbot/commands/reaction.js.map +1 -0
  32. package/dist/src/platforms/slackbot/commands/shared.d.ts +9 -0
  33. package/dist/src/platforms/slackbot/commands/shared.d.ts.map +1 -0
  34. package/dist/src/platforms/slackbot/commands/shared.js +13 -0
  35. package/dist/src/platforms/slackbot/commands/shared.js.map +1 -0
  36. package/dist/src/platforms/slackbot/commands/user.d.ts +3 -0
  37. package/dist/src/platforms/slackbot/commands/user.d.ts.map +1 -0
  38. package/dist/src/platforms/slackbot/commands/user.js +40 -0
  39. package/dist/src/platforms/slackbot/commands/user.js.map +1 -0
  40. package/dist/src/platforms/slackbot/credential-manager.d.ts +18 -0
  41. package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -0
  42. package/dist/src/platforms/slackbot/credential-manager.js +187 -0
  43. package/dist/src/platforms/slackbot/credential-manager.js.map +1 -0
  44. package/dist/src/platforms/slackbot/index.d.ts +4 -0
  45. package/dist/src/platforms/slackbot/index.d.ts.map +1 -0
  46. package/dist/src/platforms/slackbot/index.js +4 -0
  47. package/dist/src/platforms/slackbot/index.js.map +1 -0
  48. package/dist/src/platforms/slackbot/types.d.ts +460 -0
  49. package/dist/src/platforms/slackbot/types.d.ts.map +1 -0
  50. package/dist/src/platforms/slackbot/types.js +114 -0
  51. package/dist/src/platforms/slackbot/types.js.map +1 -0
  52. package/docs/content/docs/integrations/meta.json +1 -1
  53. package/docs/content/docs/integrations/slackbot.mdx +204 -0
  54. package/docs/src/app/page.tsx +18 -1
  55. package/e2e/config.ts +26 -0
  56. package/e2e/helpers.ts +6 -1
  57. package/e2e/slackbot.e2e.test.ts +306 -0
  58. package/package.json +3 -2
  59. package/skills/agent-slackbot/SKILL.md +285 -0
  60. package/skills/agent-slackbot/references/authentication.md +253 -0
  61. package/skills/agent-slackbot/references/common-patterns.md +218 -0
  62. package/skills/agent-slackbot/templates/monitor-channel.sh +98 -0
  63. package/skills/agent-slackbot/templates/post-message.sh +107 -0
  64. package/skills/agent-slackbot/templates/workspace-summary.sh +113 -0
  65. package/src/platforms/slackbot/cli.ts +30 -0
  66. package/src/platforms/slackbot/client.test.ts +282 -0
  67. package/src/platforms/slackbot/client.ts +401 -0
  68. package/src/platforms/slackbot/commands/auth.test.ts +245 -0
  69. package/src/platforms/slackbot/commands/auth.ts +240 -0
  70. package/src/platforms/slackbot/commands/channel.ts +46 -0
  71. package/src/platforms/slackbot/commands/index.ts +5 -0
  72. package/src/platforms/slackbot/commands/message.ts +182 -0
  73. package/src/platforms/slackbot/commands/reaction.ts +59 -0
  74. package/src/platforms/slackbot/commands/shared.ts +23 -0
  75. package/src/platforms/slackbot/commands/user.ts +46 -0
  76. package/src/platforms/slackbot/credential-manager.test.ts +264 -0
  77. package/src/platforms/slackbot/credential-manager.ts +218 -0
  78. package/src/platforms/slackbot/index.ts +19 -0
  79. package/src/platforms/slackbot/types.test.ts +90 -0
  80. 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
+ })