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,264 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { existsSync, rmSync } from 'node:fs'
|
|
3
|
+
import { mkdir, stat } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { SlackBotCredentialManager } from './credential-manager'
|
|
7
|
+
|
|
8
|
+
const CREDS_A = {
|
|
9
|
+
token: 'xoxb-token-a',
|
|
10
|
+
workspace_id: 'T123',
|
|
11
|
+
workspace_name: 'Workspace A',
|
|
12
|
+
bot_id: 'deploy',
|
|
13
|
+
bot_name: 'Deploy Bot',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CREDS_B = {
|
|
17
|
+
token: 'xoxb-token-b',
|
|
18
|
+
workspace_id: 'T123',
|
|
19
|
+
workspace_name: 'Workspace A',
|
|
20
|
+
bot_id: 'alert',
|
|
21
|
+
bot_name: 'Alert Bot',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CREDS_C = {
|
|
25
|
+
token: 'xoxb-token-c',
|
|
26
|
+
workspace_id: 'T999',
|
|
27
|
+
workspace_name: 'Workspace B',
|
|
28
|
+
bot_id: 'deploy',
|
|
29
|
+
bot_name: 'Deploy Bot (B)',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('SlackBotCredentialManager', () => {
|
|
33
|
+
let tempDir: string
|
|
34
|
+
let manager: SlackBotCredentialManager
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
tempDir = join(tmpdir(), `slackbot-cred-test-${Date.now()}`)
|
|
38
|
+
await mkdir(tempDir, { recursive: true })
|
|
39
|
+
manager = new SlackBotCredentialManager(tempDir)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
if (existsSync(tempDir)) {
|
|
44
|
+
rmSync(tempDir, { recursive: true })
|
|
45
|
+
}
|
|
46
|
+
delete process.env.E2E_SLACKBOT_TOKEN
|
|
47
|
+
delete process.env.E2E_SLACKBOT_WORKSPACE_ID
|
|
48
|
+
delete process.env.E2E_SLACKBOT_WORKSPACE_NAME
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('load', () => {
|
|
52
|
+
test('returns empty config when no file exists', async () => {
|
|
53
|
+
const config = await manager.load()
|
|
54
|
+
|
|
55
|
+
expect(config.current).toBeNull()
|
|
56
|
+
expect(config.workspaces).toEqual({})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('save and load', () => {
|
|
61
|
+
test('persists config to file', async () => {
|
|
62
|
+
const config = {
|
|
63
|
+
current: { workspace_id: 'T123', bot_id: 'deploy' },
|
|
64
|
+
workspaces: {
|
|
65
|
+
T123: {
|
|
66
|
+
workspace_id: 'T123',
|
|
67
|
+
workspace_name: 'Test Workspace',
|
|
68
|
+
bots: {
|
|
69
|
+
deploy: { bot_id: 'deploy', bot_name: 'Deploy Bot', token: 'xoxb-test-token' },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await manager.save(config)
|
|
76
|
+
const loaded = await manager.load()
|
|
77
|
+
|
|
78
|
+
expect(loaded).toEqual(config)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('getCredentials', () => {
|
|
83
|
+
test('returns null when no credentials exist', async () => {
|
|
84
|
+
expect(await manager.getCredentials()).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('returns current bot credentials', async () => {
|
|
88
|
+
await manager.setCredentials(CREDS_A)
|
|
89
|
+
|
|
90
|
+
const creds = await manager.getCredentials()
|
|
91
|
+
|
|
92
|
+
expect(creds).toEqual(CREDS_A)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('returns specific bot by id', async () => {
|
|
96
|
+
await manager.setCredentials(CREDS_A)
|
|
97
|
+
await manager.setCredentials(CREDS_B)
|
|
98
|
+
|
|
99
|
+
const creds = await manager.getCredentials('deploy')
|
|
100
|
+
|
|
101
|
+
expect(creds).toEqual(CREDS_A)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('returns specific bot by workspace_id/bot_id', async () => {
|
|
105
|
+
await manager.setCredentials(CREDS_A)
|
|
106
|
+
await manager.setCredentials(CREDS_C)
|
|
107
|
+
|
|
108
|
+
const creds = await manager.getCredentials('T123/deploy')
|
|
109
|
+
|
|
110
|
+
expect(creds).toEqual(CREDS_A)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('returns null for ambiguous bot_id across workspaces', async () => {
|
|
114
|
+
await manager.setCredentials(CREDS_A)
|
|
115
|
+
await manager.setCredentials(CREDS_C)
|
|
116
|
+
|
|
117
|
+
const creds = await manager.getCredentials('deploy')
|
|
118
|
+
|
|
119
|
+
expect(creds).toBeNull()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('env vars take precedence over file', async () => {
|
|
123
|
+
await manager.setCredentials(CREDS_A)
|
|
124
|
+
|
|
125
|
+
process.env.E2E_SLACKBOT_TOKEN = 'xoxb-env-token'
|
|
126
|
+
process.env.E2E_SLACKBOT_WORKSPACE_ID = 'T789'
|
|
127
|
+
process.env.E2E_SLACKBOT_WORKSPACE_NAME = 'Env Workspace'
|
|
128
|
+
|
|
129
|
+
const creds = await manager.getCredentials()
|
|
130
|
+
|
|
131
|
+
expect(creds?.token).toBe('xoxb-env-token')
|
|
132
|
+
expect(creds?.workspace_id).toBe('T789')
|
|
133
|
+
expect(creds?.bot_id).toBe('env')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('setCredentials', () => {
|
|
138
|
+
test('stores bot and sets as current', async () => {
|
|
139
|
+
await manager.setCredentials(CREDS_A)
|
|
140
|
+
|
|
141
|
+
const config = await manager.load()
|
|
142
|
+
expect(config.current).toEqual({ workspace_id: 'T123', bot_id: 'deploy' })
|
|
143
|
+
expect(config.workspaces.T123.bots.deploy.token).toBe('xoxb-token-a')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('stores multiple bots in same workspace', async () => {
|
|
147
|
+
await manager.setCredentials(CREDS_A)
|
|
148
|
+
await manager.setCredentials(CREDS_B)
|
|
149
|
+
|
|
150
|
+
const config = await manager.load()
|
|
151
|
+
expect(Object.keys(config.workspaces.T123.bots)).toEqual(['deploy', 'alert'])
|
|
152
|
+
expect(config.current).toEqual({ workspace_id: 'T123', bot_id: 'alert' })
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('stores bots across workspaces', async () => {
|
|
156
|
+
await manager.setCredentials(CREDS_A)
|
|
157
|
+
await manager.setCredentials(CREDS_C)
|
|
158
|
+
|
|
159
|
+
const config = await manager.load()
|
|
160
|
+
expect(Object.keys(config.workspaces)).toEqual(['T123', 'T999'])
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('listAll', () => {
|
|
165
|
+
test('returns all bots with current flag', async () => {
|
|
166
|
+
await manager.setCredentials(CREDS_A)
|
|
167
|
+
await manager.setCredentials(CREDS_B)
|
|
168
|
+
|
|
169
|
+
const all = await manager.listAll()
|
|
170
|
+
|
|
171
|
+
expect(all).toHaveLength(2)
|
|
172
|
+
expect(all.find((b) => b.bot_id === 'deploy')?.is_current).toBe(false)
|
|
173
|
+
expect(all.find((b) => b.bot_id === 'alert')?.is_current).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('setCurrent', () => {
|
|
178
|
+
test('switches current bot', async () => {
|
|
179
|
+
await manager.setCredentials(CREDS_A)
|
|
180
|
+
await manager.setCredentials(CREDS_B)
|
|
181
|
+
|
|
182
|
+
const switched = await manager.setCurrent('deploy')
|
|
183
|
+
|
|
184
|
+
expect(switched).toBe(true)
|
|
185
|
+
const creds = await manager.getCredentials()
|
|
186
|
+
expect(creds?.bot_id).toBe('deploy')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('returns false for unknown bot', async () => {
|
|
190
|
+
expect(await manager.setCurrent('nonexistent')).toBe(false)
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe('removeBot', () => {
|
|
195
|
+
test('removes a bot by id', async () => {
|
|
196
|
+
await manager.setCredentials(CREDS_A)
|
|
197
|
+
await manager.setCredentials(CREDS_B)
|
|
198
|
+
|
|
199
|
+
const removed = await manager.removeBot('deploy')
|
|
200
|
+
|
|
201
|
+
expect(removed).toBe(true)
|
|
202
|
+
const config = await manager.load()
|
|
203
|
+
expect(Object.keys(config.workspaces.T123.bots)).toEqual(['alert'])
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('removes workspace when last bot removed', async () => {
|
|
207
|
+
await manager.setCredentials(CREDS_A)
|
|
208
|
+
|
|
209
|
+
await manager.removeBot('deploy')
|
|
210
|
+
|
|
211
|
+
const config = await manager.load()
|
|
212
|
+
expect(config.workspaces.T123).toBeUndefined()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('clears current when current bot removed', async () => {
|
|
216
|
+
await manager.setCredentials(CREDS_A)
|
|
217
|
+
|
|
218
|
+
await manager.removeBot('deploy')
|
|
219
|
+
|
|
220
|
+
const config = await manager.load()
|
|
221
|
+
expect(config.current).toBeNull()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('returns false for unknown bot', async () => {
|
|
225
|
+
expect(await manager.removeBot('nonexistent')).toBe(false)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('returns false for ambiguous bot_id across workspaces', async () => {
|
|
229
|
+
await manager.setCredentials(CREDS_A)
|
|
230
|
+
await manager.setCredentials(CREDS_C)
|
|
231
|
+
|
|
232
|
+
const removed = await manager.removeBot('deploy')
|
|
233
|
+
|
|
234
|
+
expect(removed).toBe(false)
|
|
235
|
+
const config = await manager.load()
|
|
236
|
+
expect(config.workspaces.T123.bots.deploy).toBeDefined()
|
|
237
|
+
expect(config.workspaces.T999.bots.deploy).toBeDefined()
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
describe('clearCredentials', () => {
|
|
242
|
+
test('removes all credentials', async () => {
|
|
243
|
+
await manager.setCredentials(CREDS_A)
|
|
244
|
+
await manager.setCredentials(CREDS_B)
|
|
245
|
+
|
|
246
|
+
await manager.clearCredentials()
|
|
247
|
+
|
|
248
|
+
const config = await manager.load()
|
|
249
|
+
expect(config.current).toBeNull()
|
|
250
|
+
expect(config.workspaces).toEqual({})
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
describe('file permissions', () => {
|
|
255
|
+
test('saves file with secure permissions (600)', async () => {
|
|
256
|
+
await manager.setCredentials(CREDS_A)
|
|
257
|
+
|
|
258
|
+
const credPath = join(tempDir, 'slackbot-credentials.json')
|
|
259
|
+
const stats = await stat(credPath)
|
|
260
|
+
|
|
261
|
+
expect(stats.mode & 0o777).toBe(0o600)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
})
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import type { SlackBotConfig, SlackBotCredentials, SlackBotWorkspace } from './types'
|
|
6
|
+
|
|
7
|
+
export class SlackBotCredentialManager {
|
|
8
|
+
private configDir: string
|
|
9
|
+
private credentialsPath: string
|
|
10
|
+
|
|
11
|
+
constructor(configDir?: string) {
|
|
12
|
+
this.configDir = configDir ?? join(homedir(), '.config', 'agent-messenger')
|
|
13
|
+
this.credentialsPath = join(this.configDir, 'slackbot-credentials.json')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async load(): Promise<SlackBotConfig> {
|
|
17
|
+
if (!existsSync(this.credentialsPath)) {
|
|
18
|
+
return { current: null, workspaces: {} }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const content = await readFile(this.credentialsPath, 'utf-8')
|
|
22
|
+
return JSON.parse(content) as SlackBotConfig
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async save(config: SlackBotConfig): Promise<void> {
|
|
26
|
+
await mkdir(this.configDir, { recursive: true })
|
|
27
|
+
await writeFile(this.credentialsPath, JSON.stringify(config, null, 2))
|
|
28
|
+
await chmod(this.credentialsPath, 0o600)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getCredentials(botId?: string): Promise<SlackBotCredentials | null> {
|
|
32
|
+
const config = await this.load()
|
|
33
|
+
|
|
34
|
+
if (botId) {
|
|
35
|
+
return this.findBot(config, botId)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const envToken = process.env.E2E_SLACKBOT_TOKEN
|
|
39
|
+
const envWorkspaceId = process.env.E2E_SLACKBOT_WORKSPACE_ID
|
|
40
|
+
const envWorkspaceName = process.env.E2E_SLACKBOT_WORKSPACE_NAME
|
|
41
|
+
|
|
42
|
+
if (envToken && envWorkspaceId && envWorkspaceName) {
|
|
43
|
+
return {
|
|
44
|
+
token: envToken,
|
|
45
|
+
workspace_id: envWorkspaceId,
|
|
46
|
+
workspace_name: envWorkspaceName,
|
|
47
|
+
bot_id: 'env',
|
|
48
|
+
bot_name: 'env',
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!config.current) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const workspace = config.workspaces[config.current.workspace_id]
|
|
57
|
+
if (!workspace) return null
|
|
58
|
+
|
|
59
|
+
const bot = workspace.bots[config.current.bot_id]
|
|
60
|
+
if (!bot) return null
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
token: bot.token,
|
|
64
|
+
workspace_id: workspace.workspace_id,
|
|
65
|
+
workspace_name: workspace.workspace_name,
|
|
66
|
+
bot_id: bot.bot_id,
|
|
67
|
+
bot_name: bot.bot_name,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private findBot(config: SlackBotConfig, botId: string): SlackBotCredentials | null {
|
|
72
|
+
// Try "workspace_id/bot_id" format first
|
|
73
|
+
if (botId.includes('/')) {
|
|
74
|
+
const [workspaceId, id] = botId.split('/')
|
|
75
|
+
const workspace = config.workspaces[workspaceId]
|
|
76
|
+
if (!workspace) return null
|
|
77
|
+
const bot = workspace.bots[id]
|
|
78
|
+
if (!bot) return null
|
|
79
|
+
return {
|
|
80
|
+
token: bot.token,
|
|
81
|
+
workspace_id: workspace.workspace_id,
|
|
82
|
+
workspace_name: workspace.workspace_name,
|
|
83
|
+
bot_id: bot.bot_id,
|
|
84
|
+
bot_name: bot.bot_name,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Search by bot_id across all workspaces — must be unique
|
|
89
|
+
const matches: SlackBotCredentials[] = []
|
|
90
|
+
for (const workspace of Object.values(config.workspaces)) {
|
|
91
|
+
const bot = workspace.bots[botId]
|
|
92
|
+
if (bot) {
|
|
93
|
+
matches.push({
|
|
94
|
+
token: bot.token,
|
|
95
|
+
workspace_id: workspace.workspace_id,
|
|
96
|
+
workspace_name: workspace.workspace_name,
|
|
97
|
+
bot_id: bot.bot_id,
|
|
98
|
+
bot_name: bot.bot_name,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (matches.length === 1) return matches[0]
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async setCredentials(creds: SlackBotCredentials): Promise<void> {
|
|
108
|
+
const config = await this.load()
|
|
109
|
+
|
|
110
|
+
if (!config.workspaces[creds.workspace_id]) {
|
|
111
|
+
config.workspaces[creds.workspace_id] = {
|
|
112
|
+
workspace_id: creds.workspace_id,
|
|
113
|
+
workspace_name: creds.workspace_name,
|
|
114
|
+
bots: {},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const workspace = config.workspaces[creds.workspace_id]
|
|
119
|
+
workspace.workspace_name = creds.workspace_name
|
|
120
|
+
workspace.bots[creds.bot_id] = {
|
|
121
|
+
bot_id: creds.bot_id,
|
|
122
|
+
bot_name: creds.bot_name,
|
|
123
|
+
token: creds.token,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
config.current = {
|
|
127
|
+
workspace_id: creds.workspace_id,
|
|
128
|
+
bot_id: creds.bot_id,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await this.save(config)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async removeBot(botId: string): Promise<boolean> {
|
|
135
|
+
const config = await this.load()
|
|
136
|
+
|
|
137
|
+
if (botId.includes('/')) {
|
|
138
|
+
const [workspaceId, id] = botId.split('/')
|
|
139
|
+
const workspace = config.workspaces[workspaceId]
|
|
140
|
+
if (!workspace || !workspace.bots[id]) return false
|
|
141
|
+
|
|
142
|
+
delete workspace.bots[id]
|
|
143
|
+
if (Object.keys(workspace.bots).length === 0) {
|
|
144
|
+
delete config.workspaces[workspaceId]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (config.current?.workspace_id === workspaceId && config.current?.bot_id === id) {
|
|
148
|
+
config.current = null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await this.save(config)
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const matches: { workspace: SlackBotWorkspace }[] = []
|
|
156
|
+
for (const workspace of Object.values(config.workspaces)) {
|
|
157
|
+
if (workspace.bots[botId]) {
|
|
158
|
+
matches.push({ workspace })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (matches.length !== 1) return false
|
|
163
|
+
|
|
164
|
+
const { workspace } = matches[0]
|
|
165
|
+
delete workspace.bots[botId]
|
|
166
|
+
if (Object.keys(workspace.bots).length === 0) {
|
|
167
|
+
delete config.workspaces[workspace.workspace_id]
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
config.current?.workspace_id === workspace.workspace_id &&
|
|
171
|
+
config.current?.bot_id === botId
|
|
172
|
+
) {
|
|
173
|
+
config.current = null
|
|
174
|
+
}
|
|
175
|
+
await this.save(config)
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async setCurrent(botId: string): Promise<boolean> {
|
|
180
|
+
const config = await this.load()
|
|
181
|
+
const creds = this.findBot(config, botId)
|
|
182
|
+
if (!creds) return false
|
|
183
|
+
|
|
184
|
+
config.current = {
|
|
185
|
+
workspace_id: creds.workspace_id,
|
|
186
|
+
bot_id: creds.bot_id,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await this.save(config)
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async listAll(): Promise<Array<SlackBotCredentials & { is_current: boolean }>> {
|
|
194
|
+
const config = await this.load()
|
|
195
|
+
const results: Array<SlackBotCredentials & { is_current: boolean }> = []
|
|
196
|
+
|
|
197
|
+
for (const workspace of Object.values(config.workspaces)) {
|
|
198
|
+
for (const bot of Object.values(workspace.bots)) {
|
|
199
|
+
results.push({
|
|
200
|
+
token: bot.token,
|
|
201
|
+
workspace_id: workspace.workspace_id,
|
|
202
|
+
workspace_name: workspace.workspace_name,
|
|
203
|
+
bot_id: bot.bot_id,
|
|
204
|
+
bot_name: bot.bot_name,
|
|
205
|
+
is_current:
|
|
206
|
+
config.current?.workspace_id === workspace.workspace_id &&
|
|
207
|
+
config.current?.bot_id === bot.bot_id,
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return results
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async clearCredentials(): Promise<void> {
|
|
216
|
+
await this.save({ current: null, workspaces: {} })
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { SlackBotClient } from './client'
|
|
2
|
+
export { SlackBotCredentialManager } from './credential-manager'
|
|
3
|
+
export {
|
|
4
|
+
SlackBotConfig,
|
|
5
|
+
SlackBotConfigSchema,
|
|
6
|
+
SlackBotCredentials,
|
|
7
|
+
SlackBotCredentialsSchema,
|
|
8
|
+
SlackBotError,
|
|
9
|
+
SlackChannel,
|
|
10
|
+
SlackChannelSchema,
|
|
11
|
+
SlackFile,
|
|
12
|
+
SlackFileSchema,
|
|
13
|
+
SlackMessage,
|
|
14
|
+
SlackMessageSchema,
|
|
15
|
+
SlackReaction,
|
|
16
|
+
SlackReactionSchema,
|
|
17
|
+
SlackUser,
|
|
18
|
+
SlackUserSchema,
|
|
19
|
+
} from './types'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type SlackBotConfig,
|
|
4
|
+
SlackBotConfigSchema,
|
|
5
|
+
type SlackBotCredentials,
|
|
6
|
+
SlackBotCredentialsSchema,
|
|
7
|
+
SlackBotError,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
describe('SlackBotError', () => {
|
|
11
|
+
test('creates error with message and code', () => {
|
|
12
|
+
const error = new SlackBotError('Token is invalid', 'invalid_auth')
|
|
13
|
+
|
|
14
|
+
expect(error.message).toBe('Token is invalid')
|
|
15
|
+
expect(error.code).toBe('invalid_auth')
|
|
16
|
+
expect(error.name).toBe('SlackBotError')
|
|
17
|
+
expect(error instanceof Error).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('SlackBotCredentialsSchema', () => {
|
|
22
|
+
test('validates correct bot token credentials', () => {
|
|
23
|
+
const creds: SlackBotCredentials = {
|
|
24
|
+
token: 'xoxb-123456789-abcdef',
|
|
25
|
+
workspace_id: 'T12345678',
|
|
26
|
+
workspace_name: 'test-workspace',
|
|
27
|
+
bot_id: 'deploy',
|
|
28
|
+
bot_name: 'Deploy Bot',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = SlackBotCredentialsSchema.safeParse(creds)
|
|
32
|
+
expect(result.success).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('rejects user tokens (xoxp-)', () => {
|
|
36
|
+
const creds = {
|
|
37
|
+
token: 'xoxp-123456789-abcdef',
|
|
38
|
+
workspace_id: 'T12345678',
|
|
39
|
+
workspace_name: 'test-workspace',
|
|
40
|
+
bot_id: 'deploy',
|
|
41
|
+
bot_name: 'Deploy Bot',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = SlackBotCredentialsSchema.safeParse(creds)
|
|
45
|
+
expect(result.success).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('rejects missing fields', () => {
|
|
49
|
+
const creds = {
|
|
50
|
+
token: 'xoxb-123456789-abcdef',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = SlackBotCredentialsSchema.safeParse(creds)
|
|
54
|
+
expect(result.success).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('SlackBotConfigSchema', () => {
|
|
59
|
+
test('validates correct config', () => {
|
|
60
|
+
const config: SlackBotConfig = {
|
|
61
|
+
current: { workspace_id: 'T12345678', bot_id: 'deploy' },
|
|
62
|
+
workspaces: {
|
|
63
|
+
T12345678: {
|
|
64
|
+
workspace_id: 'T12345678',
|
|
65
|
+
workspace_name: 'test-workspace',
|
|
66
|
+
bots: {
|
|
67
|
+
deploy: {
|
|
68
|
+
bot_id: 'deploy',
|
|
69
|
+
bot_name: 'Deploy Bot',
|
|
70
|
+
token: 'xoxb-123456789-abcdef',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = SlackBotConfigSchema.safeParse(config)
|
|
78
|
+
expect(result.success).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('validates config with null current', () => {
|
|
82
|
+
const config: SlackBotConfig = {
|
|
83
|
+
current: null,
|
|
84
|
+
workspaces: {},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = SlackBotConfigSchema.safeParse(config)
|
|
88
|
+
expect(result.success).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
})
|