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.
Files changed (127) hide show
  1. package/.claude-plugin/README.md +38 -14
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/ci.yml +3 -0
  4. package/CONTRIBUTING.md +24 -1
  5. package/README.md +12 -8
  6. package/dist/package.json +1 -1
  7. package/dist/src/cli.d.ts.map +1 -1
  8. package/dist/src/cli.js +3 -0
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/platforms/discord/cli.d.ts +2 -2
  11. package/dist/src/platforms/discord/cli.d.ts.map +1 -1
  12. package/dist/src/platforms/discord/cli.js +23 -1
  13. package/dist/src/platforms/discord/cli.js.map +1 -1
  14. package/dist/src/platforms/discord/commands/file.d.ts.map +1 -1
  15. package/dist/src/platforms/discord/commands/file.js +13 -7
  16. package/dist/src/platforms/discord/commands/file.js.map +1 -1
  17. package/dist/src/platforms/discord/commands/friend.d.ts.map +1 -1
  18. package/dist/src/platforms/discord/commands/friend.js +30 -30
  19. package/dist/src/platforms/discord/commands/friend.js.map +1 -1
  20. package/dist/src/platforms/discord/commands/index.d.ts +7 -0
  21. package/dist/src/platforms/discord/commands/index.d.ts.map +1 -1
  22. package/dist/src/platforms/discord/commands/index.js +7 -0
  23. package/dist/src/platforms/discord/commands/index.js.map +1 -1
  24. package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
  25. package/dist/src/platforms/discord/commands/snapshot.js +1 -2
  26. package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
  27. package/dist/src/platforms/discord/ensure-auth.d.ts +2 -0
  28. package/dist/src/platforms/discord/ensure-auth.d.ts.map +1 -0
  29. package/dist/src/platforms/discord/ensure-auth.js +31 -0
  30. package/dist/src/platforms/discord/ensure-auth.js.map +1 -0
  31. package/dist/src/platforms/slack/cli.d.ts +2 -2
  32. package/dist/src/platforms/slack/cli.d.ts.map +1 -1
  33. package/dist/src/platforms/slack/cli.js +15 -0
  34. package/dist/src/platforms/slack/cli.js.map +1 -1
  35. package/dist/src/platforms/slack/client.d.ts +1 -0
  36. package/dist/src/platforms/slack/client.d.ts.map +1 -1
  37. package/dist/src/platforms/slack/client.js +13 -0
  38. package/dist/src/platforms/slack/client.js.map +1 -1
  39. package/dist/src/platforms/slack/commands/channel.d.ts.map +1 -1
  40. package/dist/src/platforms/slack/commands/channel.js +2 -0
  41. package/dist/src/platforms/slack/commands/channel.js.map +1 -1
  42. package/dist/src/platforms/slack/commands/file.d.ts.map +1 -1
  43. package/dist/src/platforms/slack/commands/file.js +13 -5
  44. package/dist/src/platforms/slack/commands/file.js.map +1 -1
  45. package/dist/src/platforms/slack/commands/message.d.ts.map +1 -1
  46. package/dist/src/platforms/slack/commands/message.js +12 -6
  47. package/dist/src/platforms/slack/commands/message.js.map +1 -1
  48. package/dist/src/platforms/slack/commands/reaction.d.ts.map +1 -1
  49. package/dist/src/platforms/slack/commands/reaction.js +3 -0
  50. package/dist/src/platforms/slack/commands/reaction.js.map +1 -1
  51. package/dist/src/platforms/slack/commands/sections.d.ts.map +1 -1
  52. package/dist/src/platforms/slack/commands/sections.js +5 -6
  53. package/dist/src/platforms/slack/commands/sections.js.map +1 -1
  54. package/dist/src/platforms/slack/commands/snapshot.d.ts.map +1 -1
  55. package/dist/src/platforms/slack/commands/snapshot.js +1 -2
  56. package/dist/src/platforms/slack/commands/snapshot.js.map +1 -1
  57. package/dist/src/platforms/slack/commands/unread.d.ts.map +1 -1
  58. package/dist/src/platforms/slack/commands/unread.js +2 -0
  59. package/dist/src/platforms/slack/commands/unread.js.map +1 -1
  60. package/dist/src/platforms/slack/commands/user.js +8 -8
  61. package/dist/src/platforms/slack/ensure-auth.d.ts +2 -0
  62. package/dist/src/platforms/slack/ensure-auth.d.ts.map +1 -0
  63. package/dist/src/platforms/slack/ensure-auth.js +30 -0
  64. package/dist/src/platforms/slack/ensure-auth.js.map +1 -0
  65. package/dist/src/platforms/slackbot/client.d.ts +1 -0
  66. package/dist/src/platforms/slackbot/client.d.ts.map +1 -1
  67. package/dist/src/platforms/slackbot/client.js +13 -0
  68. package/dist/src/platforms/slackbot/client.js.map +1 -1
  69. package/dist/src/platforms/slackbot/commands/channel.d.ts.map +1 -1
  70. package/dist/src/platforms/slackbot/commands/channel.js +3 -2
  71. package/dist/src/platforms/slackbot/commands/channel.js.map +1 -1
  72. package/dist/src/platforms/slackbot/commands/message.d.ts.map +1 -1
  73. package/dist/src/platforms/slackbot/commands/message.js +18 -12
  74. package/dist/src/platforms/slackbot/commands/message.js.map +1 -1
  75. package/dist/src/platforms/slackbot/commands/reaction.d.ts.map +1 -1
  76. package/dist/src/platforms/slackbot/commands/reaction.js +8 -6
  77. package/dist/src/platforms/slackbot/commands/reaction.js.map +1 -1
  78. package/dist/src/platforms/teams/cli.d.ts +2 -2
  79. package/dist/src/platforms/teams/cli.d.ts.map +1 -1
  80. package/dist/src/platforms/teams/cli.js +15 -0
  81. package/dist/src/platforms/teams/cli.js.map +1 -1
  82. package/dist/src/platforms/teams/commands/file.js +12 -12
  83. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  84. package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -1
  85. package/dist/src/platforms/teams/commands/snapshot.js +1 -2
  86. package/dist/src/platforms/teams/commands/snapshot.js.map +1 -1
  87. package/dist/src/platforms/teams/ensure-auth.d.ts +2 -0
  88. package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -0
  89. package/dist/src/platforms/teams/ensure-auth.js +32 -0
  90. package/dist/src/platforms/teams/ensure-auth.js.map +1 -0
  91. package/e2e/README.md +1 -1
  92. package/package.json +1 -1
  93. package/skills/agent-discord/SKILL.md +22 -22
  94. package/skills/agent-slack/SKILL.md +28 -40
  95. package/skills/agent-teams/SKILL.md +41 -65
  96. package/skills/agent-teams/references/common-patterns.md +63 -49
  97. package/src/cli.ts +4 -0
  98. package/src/platforms/discord/cli.ts +30 -0
  99. package/src/platforms/discord/commands/file.ts +13 -7
  100. package/src/platforms/discord/commands/friend.ts +34 -34
  101. package/src/platforms/discord/commands/index.ts +7 -0
  102. package/src/platforms/discord/commands/snapshot.ts +1 -2
  103. package/src/platforms/discord/ensure-auth.test.ts +123 -0
  104. package/src/platforms/discord/ensure-auth.ts +31 -0
  105. package/src/platforms/slack/cli.ts +16 -0
  106. package/src/platforms/slack/client.test.ts +101 -0
  107. package/src/platforms/slack/client.ts +22 -0
  108. package/src/platforms/slack/commands/channel.ts +2 -0
  109. package/src/platforms/slack/commands/file.ts +15 -5
  110. package/src/platforms/slack/commands/message.ts +17 -6
  111. package/src/platforms/slack/commands/reaction.ts +3 -0
  112. package/src/platforms/slack/commands/sections.ts +8 -9
  113. package/src/platforms/slack/commands/snapshot.ts +1 -2
  114. package/src/platforms/slack/commands/unread.ts +2 -0
  115. package/src/platforms/slack/commands/user.ts +8 -8
  116. package/src/platforms/slack/ensure-auth.test.ts +186 -0
  117. package/src/platforms/slack/ensure-auth.ts +30 -0
  118. package/src/platforms/slackbot/client.test.ts +87 -0
  119. package/src/platforms/slackbot/client.ts +21 -0
  120. package/src/platforms/slackbot/commands/channel.ts +3 -2
  121. package/src/platforms/slackbot/commands/message.ts +18 -12
  122. package/src/platforms/slackbot/commands/reaction.ts +8 -6
  123. package/src/platforms/teams/cli.ts +16 -0
  124. package/src/platforms/teams/commands/file.ts +12 -12
  125. package/src/platforms/teams/commands/snapshot.ts +1 -2
  126. package/src/platforms/teams/ensure-auth.test.ts +167 -0
  127. 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(channel: string, options: BotOption): Promise<void> {
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(channel: string, text: string, options: BotOption & { thread?: string }): Promise<void> {
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(channel: string, options: BotOption & { limit?: string }): Promise<void> {
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(channel: string, ts: string, options: BotOption): Promise<void> {
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(channel: string, ts: string, text: string, options: BotOption): Promise<void> {
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(channel: string, ts: string, options: BotOption & { force?: boolean }): Promise<void> {
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
- channel: string,
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(channel: string, timestamp: string, emoji: string, options: BotOption): Promise<void> {
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(channel: string, timestamp: string, emoji: string, options: BotOption): Promise<void> {
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('<timestamp>', 'Message timestamp')
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('<timestamp>', 'Message timestamp')
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('file commands')
108
+ .description('File commands')
109
109
  .addCommand(
110
110
  new Command('upload')
111
- .description('upload file to channel')
112
- .argument('<team>', 'team ID')
113
- .argument('<channel>', 'channel ID')
114
- .argument('<path>', 'file 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('list files in channel')
121
- .argument('<team>', 'team ID')
122
- .argument('<channel>', 'channel ID')
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('show file details')
129
- .argument('<team>', 'team ID')
130
- .argument('<channel>', 'channel ID')
131
- .argument('<file>', 'file ID')
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)')