agent-messenger 1.3.6 → 1.5.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/README.md +38 -14
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +3 -0
- package/CONTRIBUTING.md +24 -1
- package/README.md +12 -8
- package/dist/package.json +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/discord/cli.d.ts +2 -2
- package/dist/src/platforms/discord/cli.d.ts.map +1 -1
- package/dist/src/platforms/discord/cli.js +23 -1
- package/dist/src/platforms/discord/cli.js.map +1 -1
- package/dist/src/platforms/discord/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/file.js +13 -7
- package/dist/src/platforms/discord/commands/file.js.map +1 -1
- package/dist/src/platforms/discord/commands/friend.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/friend.js +30 -30
- package/dist/src/platforms/discord/commands/friend.js.map +1 -1
- package/dist/src/platforms/discord/commands/index.d.ts +7 -0
- package/dist/src/platforms/discord/commands/index.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/index.js +7 -0
- package/dist/src/platforms/discord/commands/index.js.map +1 -1
- package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/snapshot.js +1 -2
- package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/discord/ensure-auth.d.ts +2 -0
- package/dist/src/platforms/discord/ensure-auth.d.ts.map +1 -0
- package/dist/src/platforms/discord/ensure-auth.js +31 -0
- package/dist/src/platforms/discord/ensure-auth.js.map +1 -0
- package/dist/src/platforms/slack/cli.d.ts +2 -2
- package/dist/src/platforms/slack/cli.d.ts.map +1 -1
- package/dist/src/platforms/slack/cli.js +15 -0
- package/dist/src/platforms/slack/cli.js.map +1 -1
- package/dist/src/platforms/slack/client.d.ts +1 -0
- package/dist/src/platforms/slack/client.d.ts.map +1 -1
- package/dist/src/platforms/slack/client.js +13 -0
- package/dist/src/platforms/slack/client.js.map +1 -1
- package/dist/src/platforms/slack/commands/channel.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/channel.js +2 -0
- package/dist/src/platforms/slack/commands/channel.js.map +1 -1
- package/dist/src/platforms/slack/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/file.js +13 -5
- package/dist/src/platforms/slack/commands/file.js.map +1 -1
- package/dist/src/platforms/slack/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/message.js +12 -6
- package/dist/src/platforms/slack/commands/message.js.map +1 -1
- package/dist/src/platforms/slack/commands/reaction.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/reaction.js +3 -0
- package/dist/src/platforms/slack/commands/reaction.js.map +1 -1
- package/dist/src/platforms/slack/commands/sections.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/sections.js +5 -6
- package/dist/src/platforms/slack/commands/sections.js.map +1 -1
- package/dist/src/platforms/slack/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/snapshot.js +1 -2
- package/dist/src/platforms/slack/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/slack/commands/unread.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/unread.js +2 -0
- package/dist/src/platforms/slack/commands/unread.js.map +1 -1
- package/dist/src/platforms/slack/commands/user.js +8 -8
- package/dist/src/platforms/slack/ensure-auth.d.ts +2 -0
- package/dist/src/platforms/slack/ensure-auth.d.ts.map +1 -0
- package/dist/src/platforms/slack/ensure-auth.js +30 -0
- package/dist/src/platforms/slack/ensure-auth.js.map +1 -0
- package/dist/src/platforms/slackbot/client.d.ts +1 -0
- package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/client.js +13 -0
- package/dist/src/platforms/slackbot/client.js.map +1 -1
- package/dist/src/platforms/slackbot/commands/channel.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/commands/channel.js +3 -2
- package/dist/src/platforms/slackbot/commands/channel.js.map +1 -1
- package/dist/src/platforms/slackbot/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/commands/message.js +18 -12
- package/dist/src/platforms/slackbot/commands/message.js.map +1 -1
- package/dist/src/platforms/slackbot/commands/reaction.d.ts.map +1 -1
- package/dist/src/platforms/slackbot/commands/reaction.js +8 -6
- package/dist/src/platforms/slackbot/commands/reaction.js.map +1 -1
- package/dist/src/platforms/teams/cli.d.ts +2 -2
- package/dist/src/platforms/teams/cli.d.ts.map +1 -1
- package/dist/src/platforms/teams/cli.js +15 -0
- package/dist/src/platforms/teams/cli.js.map +1 -1
- package/dist/src/platforms/teams/commands/file.js +12 -12
- package/dist/src/platforms/teams/commands/file.js.map +1 -1
- package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/snapshot.js +1 -2
- package/dist/src/platforms/teams/commands/snapshot.js.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.d.ts +2 -0
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -0
- package/dist/src/platforms/teams/ensure-auth.js +32 -0
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -0
- package/e2e/README.md +1 -1
- package/package.json +1 -1
- package/skills/agent-discord/SKILL.md +22 -22
- package/skills/agent-slack/SKILL.md +28 -40
- package/skills/agent-teams/SKILL.md +41 -65
- package/skills/agent-teams/references/common-patterns.md +63 -49
- package/src/cli.ts +4 -0
- package/src/platforms/discord/cli.ts +30 -0
- package/src/platforms/discord/commands/file.ts +13 -7
- package/src/platforms/discord/commands/friend.ts +34 -34
- package/src/platforms/discord/commands/index.ts +7 -0
- package/src/platforms/discord/commands/snapshot.ts +1 -2
- package/src/platforms/discord/ensure-auth.test.ts +123 -0
- package/src/platforms/discord/ensure-auth.ts +31 -0
- package/src/platforms/slack/cli.ts +16 -0
- package/src/platforms/slack/client.test.ts +101 -0
- package/src/platforms/slack/client.ts +22 -0
- package/src/platforms/slack/commands/channel.ts +2 -0
- package/src/platforms/slack/commands/file.ts +15 -5
- package/src/platforms/slack/commands/message.ts +17 -6
- package/src/platforms/slack/commands/reaction.ts +3 -0
- package/src/platforms/slack/commands/sections.ts +8 -9
- package/src/platforms/slack/commands/snapshot.ts +1 -2
- package/src/platforms/slack/commands/unread.ts +2 -0
- package/src/platforms/slack/commands/user.ts +8 -8
- package/src/platforms/slack/ensure-auth.test.ts +186 -0
- package/src/platforms/slack/ensure-auth.ts +30 -0
- package/src/platforms/slackbot/client.test.ts +87 -0
- package/src/platforms/slackbot/client.ts +21 -0
- package/src/platforms/slackbot/commands/channel.ts +3 -2
- package/src/platforms/slackbot/commands/message.ts +18 -12
- package/src/platforms/slackbot/commands/reaction.ts +8 -6
- package/src/platforms/teams/cli.ts +16 -0
- package/src/platforms/teams/commands/file.ts +12 -12
- package/src/platforms/teams/commands/snapshot.ts +1 -2
- package/src/platforms/teams/ensure-auth.test.ts +167 -0
- package/src/platforms/teams/ensure-auth.ts +34 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
|
|
2
|
+
import { SlackClient } from './client'
|
|
3
|
+
import { CredentialManager } from './credential-manager'
|
|
4
|
+
import { ensureSlackAuth } from './ensure-auth'
|
|
5
|
+
import { TokenExtractor } from './token-extractor'
|
|
6
|
+
|
|
7
|
+
let getWorkspaceSpy: ReturnType<typeof spyOn>
|
|
8
|
+
let extractSpy: ReturnType<typeof spyOn>
|
|
9
|
+
let testAuthSpy: ReturnType<typeof spyOn>
|
|
10
|
+
let setWorkspaceSpy: ReturnType<typeof spyOn>
|
|
11
|
+
let loadSpy: ReturnType<typeof spyOn>
|
|
12
|
+
let setCurrentWorkspaceSpy: ReturnType<typeof spyOn>
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
getWorkspaceSpy = spyOn(CredentialManager.prototype, 'getWorkspace').mockResolvedValue(null)
|
|
16
|
+
|
|
17
|
+
extractSpy = spyOn(TokenExtractor.prototype, 'extract').mockResolvedValue([
|
|
18
|
+
{
|
|
19
|
+
workspace_id: 'T123',
|
|
20
|
+
workspace_name: 'test-workspace',
|
|
21
|
+
token: 'xoxc-test-token',
|
|
22
|
+
cookie: 'xoxd-test-cookie',
|
|
23
|
+
},
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
testAuthSpy = spyOn(SlackClient.prototype, 'testAuth').mockResolvedValue({
|
|
27
|
+
user_id: 'U123',
|
|
28
|
+
team_id: 'T123',
|
|
29
|
+
user: 'testuser',
|
|
30
|
+
team: 'Test Team',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
setWorkspaceSpy = spyOn(CredentialManager.prototype, 'setWorkspace').mockResolvedValue(undefined)
|
|
34
|
+
|
|
35
|
+
loadSpy = spyOn(CredentialManager.prototype, 'load').mockResolvedValue({
|
|
36
|
+
current_workspace: null,
|
|
37
|
+
workspaces: {},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
setCurrentWorkspaceSpy = spyOn(CredentialManager.prototype, 'setCurrentWorkspace').mockResolvedValue(undefined)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
getWorkspaceSpy?.mockRestore()
|
|
45
|
+
extractSpy?.mockRestore()
|
|
46
|
+
testAuthSpy?.mockRestore()
|
|
47
|
+
setWorkspaceSpy?.mockRestore()
|
|
48
|
+
loadSpy?.mockRestore()
|
|
49
|
+
setCurrentWorkspaceSpy?.mockRestore()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('ensureSlackAuth', () => {
|
|
53
|
+
test('skips extraction when credentials already exist', async () => {
|
|
54
|
+
// given
|
|
55
|
+
getWorkspaceSpy.mockResolvedValue({
|
|
56
|
+
workspace_id: 'T123',
|
|
57
|
+
workspace_name: 'existing',
|
|
58
|
+
token: 'xoxc-existing',
|
|
59
|
+
cookie: 'xoxd-existing',
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// when
|
|
63
|
+
await ensureSlackAuth()
|
|
64
|
+
|
|
65
|
+
// then
|
|
66
|
+
expect(extractSpy).not.toHaveBeenCalled()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('extracts and saves credentials when none exist', async () => {
|
|
70
|
+
// when
|
|
71
|
+
await ensureSlackAuth()
|
|
72
|
+
|
|
73
|
+
// then
|
|
74
|
+
expect(extractSpy).toHaveBeenCalled()
|
|
75
|
+
expect(testAuthSpy).toHaveBeenCalled()
|
|
76
|
+
expect(setWorkspaceSpy).toHaveBeenCalledWith(
|
|
77
|
+
expect.objectContaining({
|
|
78
|
+
workspace_id: 'T123',
|
|
79
|
+
token: 'xoxc-test-token',
|
|
80
|
+
cookie: 'xoxd-test-cookie',
|
|
81
|
+
workspace_name: 'Test Team',
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('sets first workspace as current when none set', async () => {
|
|
87
|
+
// when
|
|
88
|
+
await ensureSlackAuth()
|
|
89
|
+
|
|
90
|
+
// then
|
|
91
|
+
expect(setCurrentWorkspaceSpy).toHaveBeenCalledWith('T123')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('does not override existing current workspace', async () => {
|
|
95
|
+
// given
|
|
96
|
+
loadSpy.mockResolvedValue({
|
|
97
|
+
current_workspace: 'T999',
|
|
98
|
+
workspaces: { T999: { workspace_id: 'T999', workspace_name: 'other', token: 't', cookie: 'c' } },
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// when
|
|
102
|
+
await ensureSlackAuth()
|
|
103
|
+
|
|
104
|
+
// then
|
|
105
|
+
expect(setCurrentWorkspaceSpy).not.toHaveBeenCalled()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('handles multiple workspaces', async () => {
|
|
109
|
+
// given
|
|
110
|
+
extractSpy.mockResolvedValue([
|
|
111
|
+
{ workspace_id: 'T1', workspace_name: 'ws1', token: 'xoxc-1', cookie: 'xoxd-1' },
|
|
112
|
+
{ workspace_id: 'T2', workspace_name: 'ws2', token: 'xoxc-2', cookie: 'xoxd-2' },
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
// when
|
|
116
|
+
await ensureSlackAuth()
|
|
117
|
+
|
|
118
|
+
// then
|
|
119
|
+
expect(setWorkspaceSpy).toHaveBeenCalledTimes(2)
|
|
120
|
+
expect(setCurrentWorkspaceSpy).toHaveBeenCalledWith('T1')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('skips invalid workspaces during validation', async () => {
|
|
124
|
+
// given
|
|
125
|
+
extractSpy.mockResolvedValue([
|
|
126
|
+
{ workspace_id: 'T-bad', workspace_name: 'bad', token: 'xoxc-bad', cookie: 'xoxd-bad' },
|
|
127
|
+
{ workspace_id: 'T-good', workspace_name: 'good', token: 'xoxc-good', cookie: 'xoxd-good' },
|
|
128
|
+
])
|
|
129
|
+
let callCount = 0
|
|
130
|
+
testAuthSpy.mockImplementation(() => {
|
|
131
|
+
callCount++
|
|
132
|
+
if (callCount === 1) throw new Error('invalid_auth')
|
|
133
|
+
return Promise.resolve({ user_id: 'U1', team_id: 'T-good', user: 'user', team: 'Good Team' })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// when
|
|
137
|
+
await ensureSlackAuth()
|
|
138
|
+
|
|
139
|
+
// then - only the valid workspace is saved
|
|
140
|
+
expect(setWorkspaceSpy).toHaveBeenCalledTimes(1)
|
|
141
|
+
expect(setWorkspaceSpy).toHaveBeenCalledWith(expect.objectContaining({ workspace_id: 'T-good' }))
|
|
142
|
+
expect(setCurrentWorkspaceSpy).toHaveBeenCalledWith('T-good')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('silently handles extraction failure', async () => {
|
|
146
|
+
// given
|
|
147
|
+
extractSpy.mockRejectedValue(new Error('Slack directory not found'))
|
|
148
|
+
|
|
149
|
+
// when/then
|
|
150
|
+
await ensureSlackAuth()
|
|
151
|
+
|
|
152
|
+
// then
|
|
153
|
+
expect(setWorkspaceSpy).not.toHaveBeenCalled()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('does not save when no workspaces extracted', async () => {
|
|
157
|
+
// given
|
|
158
|
+
extractSpy.mockResolvedValue([])
|
|
159
|
+
|
|
160
|
+
// when
|
|
161
|
+
await ensureSlackAuth()
|
|
162
|
+
|
|
163
|
+
// then
|
|
164
|
+
expect(setWorkspaceSpy).not.toHaveBeenCalled()
|
|
165
|
+
expect(setCurrentWorkspaceSpy).not.toHaveBeenCalled()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('updates workspace_name from auth response', async () => {
|
|
169
|
+
// given
|
|
170
|
+
extractSpy.mockResolvedValue([
|
|
171
|
+
{ workspace_id: 'T1', workspace_name: 'old-name', token: 'xoxc-1', cookie: 'xoxd-1' },
|
|
172
|
+
])
|
|
173
|
+
testAuthSpy.mockResolvedValue({
|
|
174
|
+
user_id: 'U1',
|
|
175
|
+
team_id: 'T1',
|
|
176
|
+
user: 'user',
|
|
177
|
+
team: 'New Team Name',
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// when
|
|
181
|
+
await ensureSlackAuth()
|
|
182
|
+
|
|
183
|
+
// then
|
|
184
|
+
expect(setWorkspaceSpy).toHaveBeenCalledWith(expect.objectContaining({ workspace_name: 'New Team Name' }))
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SlackClient } from './client'
|
|
2
|
+
import { CredentialManager } from './credential-manager'
|
|
3
|
+
import { TokenExtractor } from './token-extractor'
|
|
4
|
+
|
|
5
|
+
export async function ensureSlackAuth(): Promise<void> {
|
|
6
|
+
try {
|
|
7
|
+
const credManager = new CredentialManager()
|
|
8
|
+
const workspace = await credManager.getWorkspace()
|
|
9
|
+
if (workspace) return
|
|
10
|
+
|
|
11
|
+
const extractor = new TokenExtractor()
|
|
12
|
+
const workspaces = await extractor.extract()
|
|
13
|
+
|
|
14
|
+
const validWorkspaces = []
|
|
15
|
+
for (const ws of workspaces) {
|
|
16
|
+
try {
|
|
17
|
+
const client = new SlackClient(ws.token, ws.cookie)
|
|
18
|
+
const authInfo = await client.testAuth()
|
|
19
|
+
ws.workspace_name = authInfo.team || ws.workspace_name
|
|
20
|
+
await credManager.setWorkspace(ws)
|
|
21
|
+
validWorkspaces.push(ws)
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const config = await credManager.load()
|
|
26
|
+
if (!config.current_workspace && validWorkspaces.length > 0) {
|
|
27
|
+
await credManager.setCurrentWorkspace(validWorkspaces[0].workspace_id)
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
@@ -252,6 +252,93 @@ describe('SlackBotClient', () => {
|
|
|
252
252
|
})
|
|
253
253
|
})
|
|
254
254
|
|
|
255
|
+
describe('resolveChannel', () => {
|
|
256
|
+
test('returns channel ID unchanged when it starts with C', async () => {
|
|
257
|
+
// given
|
|
258
|
+
const client = new SlackBotClient('xoxb-test-token')
|
|
259
|
+
|
|
260
|
+
// when
|
|
261
|
+
const channel = await client.resolveChannel('C123ABC')
|
|
262
|
+
|
|
263
|
+
// then
|
|
264
|
+
expect(channel).toBe('C123ABC')
|
|
265
|
+
expect(mockConversations.list).not.toHaveBeenCalled()
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('returns channel ID unchanged when it starts with D', async () => {
|
|
269
|
+
// given
|
|
270
|
+
const client = new SlackBotClient('xoxb-test-token')
|
|
271
|
+
|
|
272
|
+
// when
|
|
273
|
+
const channel = await client.resolveChannel('D123ABC')
|
|
274
|
+
|
|
275
|
+
// then
|
|
276
|
+
expect(channel).toBe('D123ABC')
|
|
277
|
+
expect(mockConversations.list).not.toHaveBeenCalled()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('returns channel ID unchanged when it starts with G', async () => {
|
|
281
|
+
// given
|
|
282
|
+
const client = new SlackBotClient('xoxb-test-token')
|
|
283
|
+
|
|
284
|
+
// when
|
|
285
|
+
const channel = await client.resolveChannel('G123ABC')
|
|
286
|
+
|
|
287
|
+
// then
|
|
288
|
+
expect(channel).toBe('G123ABC')
|
|
289
|
+
expect(mockConversations.list).not.toHaveBeenCalled()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('resolves channel name to ID', async () => {
|
|
293
|
+
// given
|
|
294
|
+
const client = new SlackBotClient('xoxb-test-token')
|
|
295
|
+
|
|
296
|
+
// when
|
|
297
|
+
const channel = await client.resolveChannel('general')
|
|
298
|
+
|
|
299
|
+
// then
|
|
300
|
+
expect(channel).toBe('C123')
|
|
301
|
+
expect(mockConversations.list).toHaveBeenCalled()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('strips leading # from channel name', async () => {
|
|
305
|
+
// given
|
|
306
|
+
const client = new SlackBotClient('xoxb-test-token')
|
|
307
|
+
|
|
308
|
+
// when
|
|
309
|
+
const channel = await client.resolveChannel('#general')
|
|
310
|
+
|
|
311
|
+
// then
|
|
312
|
+
expect(channel).toBe('C123')
|
|
313
|
+
expect(mockConversations.list).toHaveBeenCalled()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test('returns channel ID unchanged when input is #C prefixed ID', async () => {
|
|
317
|
+
// given
|
|
318
|
+
const client = new SlackBotClient('xoxb-test-token')
|
|
319
|
+
|
|
320
|
+
// when
|
|
321
|
+
const channel = await client.resolveChannel('#C123ABC')
|
|
322
|
+
|
|
323
|
+
// then
|
|
324
|
+
expect(channel).toBe('C123ABC')
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('throws channel_not_found error when name is not found', async () => {
|
|
328
|
+
// given
|
|
329
|
+
const client = new SlackBotClient('xoxb-test-token')
|
|
330
|
+
|
|
331
|
+
// when/then
|
|
332
|
+
try {
|
|
333
|
+
await client.resolveChannel('does-not-exist')
|
|
334
|
+
throw new Error('Expected resolveChannel to throw')
|
|
335
|
+
} catch (error) {
|
|
336
|
+
expect(error).toBeInstanceOf(SlackBotError)
|
|
337
|
+
expect((error as SlackBotError).code).toBe('channel_not_found')
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
255
342
|
describe('listUsers', () => {
|
|
256
343
|
test('returns list of users', async () => {
|
|
257
344
|
// given
|
|
@@ -261,6 +261,27 @@ export class SlackBotClient {
|
|
|
261
261
|
})
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
async resolveChannel(channel: string): Promise<string> {
|
|
265
|
+
const normalized = channel.replace(/^#/, '')
|
|
266
|
+
|
|
267
|
+
if (/^[CDG][A-Z0-9]+$/.test(normalized)) {
|
|
268
|
+
return normalized
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const name = normalized
|
|
272
|
+
const channels = await this.listChannels()
|
|
273
|
+
const found = channels.find((ch) => ch.name === name)
|
|
274
|
+
|
|
275
|
+
if (!found) {
|
|
276
|
+
throw new SlackBotError(
|
|
277
|
+
`Channel not found: "${channel}". Use channel ID or exact channel name.`,
|
|
278
|
+
'channel_not_found',
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return found.id
|
|
283
|
+
}
|
|
284
|
+
|
|
264
285
|
async listUsers(options?: { limit?: number; cursor?: string }): Promise<SlackUser[]> {
|
|
265
286
|
const users: SlackUser[] = []
|
|
266
287
|
let cursor: string | undefined = options?.cursor
|
|
@@ -15,9 +15,10 @@ async function listAction(options: BotOption & { limit?: string }): Promise<void
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
async function infoAction(
|
|
18
|
+
async function infoAction(channelInput: string, options: BotOption): Promise<void> {
|
|
19
19
|
try {
|
|
20
20
|
const client = await getClient(options)
|
|
21
|
+
const channel = await client.resolveChannel(channelInput)
|
|
21
22
|
const info = await client.getChannelInfo(channel)
|
|
22
23
|
|
|
23
24
|
console.log(formatOutput(info, options.pretty))
|
|
@@ -39,7 +40,7 @@ export const channelCommand = new Command('channel')
|
|
|
39
40
|
.addCommand(
|
|
40
41
|
new Command('info')
|
|
41
42
|
.description('Get channel info')
|
|
42
|
-
.argument('<channel>', 'Channel ID')
|
|
43
|
+
.argument('<channel>', 'Channel ID or name')
|
|
43
44
|
.option('--bot <id>', 'Use specific bot')
|
|
44
45
|
.option('--pretty', 'Pretty print JSON output')
|
|
45
46
|
.action(infoAction),
|
|
@@ -3,9 +3,10 @@ import { handleError } from '@/shared/utils/error-handler'
|
|
|
3
3
|
import { formatOutput } from '@/shared/utils/output'
|
|
4
4
|
import { type BotOption, getClient } from './shared'
|
|
5
5
|
|
|
6
|
-
async function sendAction(
|
|
6
|
+
async function sendAction(channelInput: string, text: string, options: BotOption & { thread?: string }): Promise<void> {
|
|
7
7
|
try {
|
|
8
8
|
const client = await getClient(options)
|
|
9
|
+
const channel = await client.resolveChannel(channelInput)
|
|
9
10
|
const result = await client.postMessage(channel, text, {
|
|
10
11
|
thread_ts: options.thread,
|
|
11
12
|
})
|
|
@@ -26,9 +27,10 @@ async function sendAction(channel: string, text: string, options: BotOption & {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
async function listAction(
|
|
30
|
+
async function listAction(channelInput: string, options: BotOption & { limit?: string }): Promise<void> {
|
|
30
31
|
try {
|
|
31
32
|
const client = await getClient(options)
|
|
33
|
+
const channel = await client.resolveChannel(channelInput)
|
|
32
34
|
const limit = options.limit ? parseInt(options.limit, 10) : 20
|
|
33
35
|
const messages = await client.getConversationHistory(channel, { limit })
|
|
34
36
|
|
|
@@ -38,9 +40,10 @@ async function listAction(channel: string, options: BotOption & { limit?: string
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
async function getAction(
|
|
43
|
+
async function getAction(channelInput: string, ts: string, options: BotOption): Promise<void> {
|
|
42
44
|
try {
|
|
43
45
|
const client = await getClient(options)
|
|
46
|
+
const channel = await client.resolveChannel(channelInput)
|
|
44
47
|
const message = await client.getMessage(channel, ts)
|
|
45
48
|
|
|
46
49
|
if (!message) {
|
|
@@ -54,9 +57,10 @@ async function getAction(channel: string, ts: string, options: BotOption): Promi
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
async function updateAction(
|
|
60
|
+
async function updateAction(channelInput: string, ts: string, text: string, options: BotOption): Promise<void> {
|
|
58
61
|
try {
|
|
59
62
|
const client = await getClient(options)
|
|
63
|
+
const channel = await client.resolveChannel(channelInput)
|
|
60
64
|
const message = await client.updateMessage(channel, ts, text)
|
|
61
65
|
|
|
62
66
|
console.log(
|
|
@@ -75,7 +79,7 @@ async function updateAction(channel: string, ts: string, text: string, options:
|
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
async function deleteAction(
|
|
82
|
+
async function deleteAction(channelInput: string, ts: string, options: BotOption & { force?: boolean }): Promise<void> {
|
|
79
83
|
try {
|
|
80
84
|
if (!options.force) {
|
|
81
85
|
console.log(formatOutput({ warning: 'Use --force to confirm deletion', ts }, options.pretty))
|
|
@@ -83,6 +87,7 @@ async function deleteAction(channel: string, ts: string, options: BotOption & {
|
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
const client = await getClient(options)
|
|
90
|
+
const channel = await client.resolveChannel(channelInput)
|
|
86
91
|
await client.deleteMessage(channel, ts)
|
|
87
92
|
|
|
88
93
|
console.log(formatOutput({ deleted: ts }, options.pretty))
|
|
@@ -92,12 +97,13 @@ async function deleteAction(channel: string, ts: string, options: BotOption & {
|
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
async function repliesAction(
|
|
95
|
-
|
|
100
|
+
channelInput: string,
|
|
96
101
|
threadTs: string,
|
|
97
102
|
options: BotOption & { limit?: string },
|
|
98
103
|
): Promise<void> {
|
|
99
104
|
try {
|
|
100
105
|
const client = await getClient(options)
|
|
106
|
+
const channel = await client.resolveChannel(channelInput)
|
|
101
107
|
const limit = options.limit ? parseInt(options.limit, 10) : 100
|
|
102
108
|
const messages = await client.getThreadReplies(channel, threadTs, { limit })
|
|
103
109
|
|
|
@@ -112,7 +118,7 @@ export const messageCommand = new Command('message')
|
|
|
112
118
|
.addCommand(
|
|
113
119
|
new Command('send')
|
|
114
120
|
.description('Send a message to a channel')
|
|
115
|
-
.argument('<channel>', 'Channel ID')
|
|
121
|
+
.argument('<channel>', 'Channel ID or name')
|
|
116
122
|
.argument('<text>', 'Message text')
|
|
117
123
|
.option('--thread <ts>', 'Thread timestamp for replies')
|
|
118
124
|
.option('--bot <id>', 'Use specific bot')
|
|
@@ -122,7 +128,7 @@ export const messageCommand = new Command('message')
|
|
|
122
128
|
.addCommand(
|
|
123
129
|
new Command('list')
|
|
124
130
|
.description('List messages in a channel')
|
|
125
|
-
.argument('<channel>', 'Channel ID')
|
|
131
|
+
.argument('<channel>', 'Channel ID or name')
|
|
126
132
|
.option('--limit <n>', 'Number of messages to fetch', '20')
|
|
127
133
|
.option('--bot <id>', 'Use specific bot')
|
|
128
134
|
.option('--pretty', 'Pretty print JSON output')
|
|
@@ -131,7 +137,7 @@ export const messageCommand = new Command('message')
|
|
|
131
137
|
.addCommand(
|
|
132
138
|
new Command('get')
|
|
133
139
|
.description('Get a single message')
|
|
134
|
-
.argument('<channel>', 'Channel ID')
|
|
140
|
+
.argument('<channel>', 'Channel ID or name')
|
|
135
141
|
.argument('<ts>', 'Message timestamp')
|
|
136
142
|
.option('--bot <id>', 'Use specific bot')
|
|
137
143
|
.option('--pretty', 'Pretty print JSON output')
|
|
@@ -140,7 +146,7 @@ export const messageCommand = new Command('message')
|
|
|
140
146
|
.addCommand(
|
|
141
147
|
new Command('update')
|
|
142
148
|
.description('Update a message')
|
|
143
|
-
.argument('<channel>', 'Channel ID')
|
|
149
|
+
.argument('<channel>', 'Channel ID or name')
|
|
144
150
|
.argument('<ts>', 'Message timestamp')
|
|
145
151
|
.argument('<text>', 'New message text')
|
|
146
152
|
.option('--bot <id>', 'Use specific bot')
|
|
@@ -150,7 +156,7 @@ export const messageCommand = new Command('message')
|
|
|
150
156
|
.addCommand(
|
|
151
157
|
new Command('delete')
|
|
152
158
|
.description('Delete a message')
|
|
153
|
-
.argument('<channel>', 'Channel ID')
|
|
159
|
+
.argument('<channel>', 'Channel ID or name')
|
|
154
160
|
.argument('<ts>', 'Message timestamp')
|
|
155
161
|
.option('--force', 'Skip confirmation')
|
|
156
162
|
.option('--bot <id>', 'Use specific bot')
|
|
@@ -160,7 +166,7 @@ export const messageCommand = new Command('message')
|
|
|
160
166
|
.addCommand(
|
|
161
167
|
new Command('replies')
|
|
162
168
|
.description('Get thread replies')
|
|
163
|
-
.argument('<channel>', 'Channel ID')
|
|
169
|
+
.argument('<channel>', 'Channel ID or name')
|
|
164
170
|
.argument('<thread_ts>', 'Thread timestamp')
|
|
165
171
|
.option('--limit <n>', 'Number of replies to fetch', '100')
|
|
166
172
|
.option('--bot <id>', 'Use specific bot')
|
|
@@ -3,9 +3,10 @@ import { handleError } from '@/shared/utils/error-handler'
|
|
|
3
3
|
import { formatOutput } from '@/shared/utils/output'
|
|
4
4
|
import { type BotOption, getClient } from './shared'
|
|
5
5
|
|
|
6
|
-
async function addAction(
|
|
6
|
+
async function addAction(channelInput: string, timestamp: string, emoji: string, options: BotOption): Promise<void> {
|
|
7
7
|
try {
|
|
8
8
|
const client = await getClient(options)
|
|
9
|
+
const channel = await client.resolveChannel(channelInput)
|
|
9
10
|
await client.addReaction(channel, timestamp, emoji)
|
|
10
11
|
|
|
11
12
|
console.log(formatOutput({ success: true, channel, timestamp, emoji }, options.pretty))
|
|
@@ -14,9 +15,10 @@ async function addAction(channel: string, timestamp: string, emoji: string, opti
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
async function removeAction(
|
|
18
|
+
async function removeAction(channelInput: string, timestamp: string, emoji: string, options: BotOption): Promise<void> {
|
|
18
19
|
try {
|
|
19
20
|
const client = await getClient(options)
|
|
21
|
+
const channel = await client.resolveChannel(channelInput)
|
|
20
22
|
await client.removeReaction(channel, timestamp, emoji)
|
|
21
23
|
|
|
22
24
|
console.log(formatOutput({ success: true, channel, timestamp, emoji }, options.pretty))
|
|
@@ -30,8 +32,8 @@ export const reactionCommand = new Command('reaction')
|
|
|
30
32
|
.addCommand(
|
|
31
33
|
new Command('add')
|
|
32
34
|
.description('Add a reaction to a message')
|
|
33
|
-
.argument('<channel>', 'Channel ID')
|
|
34
|
-
.argument('<
|
|
35
|
+
.argument('<channel>', 'Channel ID or name')
|
|
36
|
+
.argument('<ts>', 'Message timestamp')
|
|
35
37
|
.argument('<emoji>', 'Emoji name (with or without colons)')
|
|
36
38
|
.option('--bot <id>', 'Use specific bot')
|
|
37
39
|
.option('--pretty', 'Pretty print JSON output')
|
|
@@ -40,8 +42,8 @@ export const reactionCommand = new Command('reaction')
|
|
|
40
42
|
.addCommand(
|
|
41
43
|
new Command('remove')
|
|
42
44
|
.description('Remove a reaction from a message')
|
|
43
|
-
.argument('<channel>', 'Channel ID')
|
|
44
|
-
.argument('<
|
|
45
|
+
.argument('<channel>', 'Channel ID or name')
|
|
46
|
+
.argument('<ts>', 'Message timestamp')
|
|
45
47
|
.argument('<emoji>', 'Emoji name (with or without colons)')
|
|
46
48
|
.option('--bot <id>', 'Use specific bot')
|
|
47
49
|
.option('--pretty', 'Pretty print JSON output')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import type { Command as CommandType } from 'commander'
|
|
3
4
|
import { Command } from 'commander'
|
|
4
5
|
import pkg from '../../../package.json' with { type: 'json' }
|
|
5
6
|
import {
|
|
@@ -12,6 +13,16 @@ import {
|
|
|
12
13
|
teamCommand,
|
|
13
14
|
userCommand,
|
|
14
15
|
} from './commands'
|
|
16
|
+
import { ensureTeamsAuth } from './ensure-auth'
|
|
17
|
+
|
|
18
|
+
function isAuthCommand(command: CommandType): boolean {
|
|
19
|
+
let cmd: CommandType | null = command
|
|
20
|
+
while (cmd) {
|
|
21
|
+
if (cmd.name() === 'auth') return true
|
|
22
|
+
cmd = cmd.parent
|
|
23
|
+
}
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
15
26
|
|
|
16
27
|
const program = new Command()
|
|
17
28
|
|
|
@@ -22,6 +33,11 @@ program
|
|
|
22
33
|
.option('--pretty', 'Pretty-print JSON output')
|
|
23
34
|
.option('--team <id>', 'Use specific team')
|
|
24
35
|
|
|
36
|
+
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
37
|
+
if (isAuthCommand(actionCommand)) return
|
|
38
|
+
await ensureTeamsAuth()
|
|
39
|
+
})
|
|
40
|
+
|
|
25
41
|
program.addCommand(authCommand)
|
|
26
42
|
program.addCommand(teamCommand)
|
|
27
43
|
program.addCommand(channelCommand)
|
|
@@ -105,30 +105,30 @@ export async function infoAction(
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export const fileCommand = new Command('file')
|
|
108
|
-
.description('
|
|
108
|
+
.description('File commands')
|
|
109
109
|
.addCommand(
|
|
110
110
|
new Command('upload')
|
|
111
|
-
.description('
|
|
112
|
-
.argument('<team>', '
|
|
113
|
-
.argument('<channel>', '
|
|
114
|
-
.argument('<path>', '
|
|
111
|
+
.description('Upload file to channel')
|
|
112
|
+
.argument('<team-id>', 'Team ID')
|
|
113
|
+
.argument('<channel-id>', 'Channel ID')
|
|
114
|
+
.argument('<path>', 'File path')
|
|
115
115
|
.option('--pretty', 'Pretty print JSON output')
|
|
116
116
|
.action(uploadAction),
|
|
117
117
|
)
|
|
118
118
|
.addCommand(
|
|
119
119
|
new Command('list')
|
|
120
|
-
.description('
|
|
121
|
-
.argument('<team>', '
|
|
122
|
-
.argument('<channel>', '
|
|
120
|
+
.description('List files in channel')
|
|
121
|
+
.argument('<team-id>', 'Team ID')
|
|
122
|
+
.argument('<channel-id>', 'Channel ID')
|
|
123
123
|
.option('--pretty', 'Pretty print JSON output')
|
|
124
124
|
.action(listAction),
|
|
125
125
|
)
|
|
126
126
|
.addCommand(
|
|
127
127
|
new Command('info')
|
|
128
|
-
.description('
|
|
129
|
-
.argument('<team>', '
|
|
130
|
-
.argument('<channel>', '
|
|
131
|
-
.argument('<file>', '
|
|
128
|
+
.description('Show file details')
|
|
129
|
+
.argument('<team-id>', 'Team ID')
|
|
130
|
+
.argument('<channel-id>', 'Channel ID')
|
|
131
|
+
.argument('<file-id>', 'File ID')
|
|
132
132
|
.option('--pretty', 'Pretty print JSON output')
|
|
133
133
|
.action(infoAction),
|
|
134
134
|
)
|
|
@@ -91,8 +91,7 @@ export async function snapshotAction(options: {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export const snapshotCommand = new Command()
|
|
95
|
-
.name('snapshot')
|
|
94
|
+
export const snapshotCommand = new Command('snapshot')
|
|
96
95
|
.description('Get comprehensive team state for AI agents')
|
|
97
96
|
.option('--channels-only', 'Include only channels (exclude messages and members)')
|
|
98
97
|
.option('--users-only', 'Include only members (exclude channels and messages)')
|