agent-messenger 1.2.0 → 1.3.1

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 (271) hide show
  1. package/.claude-plugin/marketplace.json +27 -1
  2. package/.claude-plugin/plugin.json +17 -4
  3. package/.env.template +3 -0
  4. package/.github/workflows/release.yml +94 -0
  5. package/AGENTS.md +48 -0
  6. package/README.md +25 -20
  7. package/biome.json +15 -39
  8. package/bun.lock +69 -0
  9. package/dist/package.json +12 -4
  10. package/dist/src/cli.d.ts.map +1 -1
  11. package/dist/src/cli.js +1 -4
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/platforms/discord/client.d.ts.map +1 -1
  14. package/dist/src/platforms/discord/client.js.map +1 -1
  15. package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
  16. package/dist/src/platforms/discord/commands/auth.js.map +1 -1
  17. package/dist/src/platforms/discord/commands/channel.d.ts.map +1 -1
  18. package/dist/src/platforms/discord/commands/channel.js.map +1 -1
  19. package/dist/src/platforms/discord/commands/dm.d.ts.map +1 -1
  20. package/dist/src/platforms/discord/commands/dm.js.map +1 -1
  21. package/dist/src/platforms/discord/commands/file.d.ts.map +1 -1
  22. package/dist/src/platforms/discord/commands/file.js +1 -4
  23. package/dist/src/platforms/discord/commands/file.js.map +1 -1
  24. package/dist/src/platforms/discord/commands/friend.d.ts.map +1 -1
  25. package/dist/src/platforms/discord/commands/friend.js +1 -3
  26. package/dist/src/platforms/discord/commands/friend.js.map +1 -1
  27. package/dist/src/platforms/discord/commands/member.d.ts.map +1 -1
  28. package/dist/src/platforms/discord/commands/member.js.map +1 -1
  29. package/dist/src/platforms/discord/commands/mention.d.ts.map +1 -1
  30. package/dist/src/platforms/discord/commands/mention.js.map +1 -1
  31. package/dist/src/platforms/discord/commands/message.d.ts.map +1 -1
  32. package/dist/src/platforms/discord/commands/message.js.map +1 -1
  33. package/dist/src/platforms/discord/commands/note.d.ts.map +1 -1
  34. package/dist/src/platforms/discord/commands/note.js.map +1 -1
  35. package/dist/src/platforms/discord/commands/profile.d.ts.map +1 -1
  36. package/dist/src/platforms/discord/commands/profile.js.map +1 -1
  37. package/dist/src/platforms/discord/commands/reaction.d.ts.map +1 -1
  38. package/dist/src/platforms/discord/commands/reaction.js.map +1 -1
  39. package/dist/src/platforms/discord/commands/server.d.ts.map +1 -1
  40. package/dist/src/platforms/discord/commands/server.js.map +1 -1
  41. package/dist/src/platforms/discord/commands/snapshot.d.ts.map +1 -1
  42. package/dist/src/platforms/discord/commands/snapshot.js.map +1 -1
  43. package/dist/src/platforms/discord/commands/thread.d.ts.map +1 -1
  44. package/dist/src/platforms/discord/commands/thread.js.map +1 -1
  45. package/dist/src/platforms/discord/commands/user.d.ts.map +1 -1
  46. package/dist/src/platforms/discord/commands/user.js.map +1 -1
  47. package/dist/src/platforms/discord/credential-manager.d.ts.map +1 -1
  48. package/dist/src/platforms/discord/credential-manager.js.map +1 -1
  49. package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
  50. package/dist/src/platforms/discord/token-extractor.js +2 -7
  51. package/dist/src/platforms/discord/token-extractor.js.map +1 -1
  52. package/dist/src/platforms/slack/client.d.ts.map +1 -1
  53. package/dist/src/platforms/slack/client.js.map +1 -1
  54. package/dist/src/platforms/slack/commands/activity.d.ts.map +1 -1
  55. package/dist/src/platforms/slack/commands/activity.js.map +1 -1
  56. package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
  57. package/dist/src/platforms/slack/commands/auth.js.map +1 -1
  58. package/dist/src/platforms/slack/commands/channel.d.ts.map +1 -1
  59. package/dist/src/platforms/slack/commands/channel.js.map +1 -1
  60. package/dist/src/platforms/slack/commands/drafts.d.ts.map +1 -1
  61. package/dist/src/platforms/slack/commands/drafts.js.map +1 -1
  62. package/dist/src/platforms/slack/commands/file.d.ts.map +1 -1
  63. package/dist/src/platforms/slack/commands/file.js +1 -4
  64. package/dist/src/platforms/slack/commands/file.js.map +1 -1
  65. package/dist/src/platforms/slack/commands/message.d.ts.map +1 -1
  66. package/dist/src/platforms/slack/commands/message.js.map +1 -1
  67. package/dist/src/platforms/slack/commands/reaction.d.ts.map +1 -1
  68. package/dist/src/platforms/slack/commands/reaction.js.map +1 -1
  69. package/dist/src/platforms/slack/commands/saved.d.ts.map +1 -1
  70. package/dist/src/platforms/slack/commands/saved.js.map +1 -1
  71. package/dist/src/platforms/slack/commands/sections.d.ts.map +1 -1
  72. package/dist/src/platforms/slack/commands/sections.js.map +1 -1
  73. package/dist/src/platforms/slack/commands/snapshot.d.ts.map +1 -1
  74. package/dist/src/platforms/slack/commands/snapshot.js.map +1 -1
  75. package/dist/src/platforms/slack/commands/unread.d.ts.map +1 -1
  76. package/dist/src/platforms/slack/commands/unread.js.map +1 -1
  77. package/dist/src/platforms/slack/commands/user.d.ts.map +1 -1
  78. package/dist/src/platforms/slack/commands/user.js.map +1 -1
  79. package/dist/src/platforms/slack/commands/workspace.d.ts.map +1 -1
  80. package/dist/src/platforms/slack/commands/workspace.js.map +1 -1
  81. package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
  82. package/dist/src/platforms/slack/token-extractor.js +4 -5
  83. package/dist/src/platforms/slack/token-extractor.js.map +1 -1
  84. package/dist/src/platforms/slackbot/cli.d.ts +5 -0
  85. package/dist/src/platforms/slackbot/cli.d.ts.map +1 -0
  86. package/dist/src/platforms/slackbot/cli.js +19 -0
  87. package/dist/src/platforms/slackbot/cli.js.map +1 -0
  88. package/dist/src/platforms/slackbot/client.d.ts +43 -0
  89. package/dist/src/platforms/slackbot/client.d.ts.map +1 -0
  90. package/dist/src/platforms/slackbot/client.js +347 -0
  91. package/dist/src/platforms/slackbot/client.js.map +1 -0
  92. package/dist/src/platforms/slackbot/commands/auth.d.ts +35 -0
  93. package/dist/src/platforms/slackbot/commands/auth.d.ts.map +1 -0
  94. package/dist/src/platforms/slackbot/commands/auth.js +185 -0
  95. package/dist/src/platforms/slackbot/commands/auth.js.map +1 -0
  96. package/dist/src/platforms/slackbot/commands/channel.d.ts +3 -0
  97. package/dist/src/platforms/slackbot/commands/channel.d.ts.map +1 -0
  98. package/dist/src/platforms/slackbot/commands/channel.js +40 -0
  99. package/dist/src/platforms/slackbot/commands/channel.js.map +1 -0
  100. package/dist/src/platforms/slackbot/commands/index.d.ts +6 -0
  101. package/dist/src/platforms/slackbot/commands/index.d.ts.map +1 -0
  102. package/dist/src/platforms/slackbot/commands/index.js +6 -0
  103. package/dist/src/platforms/slackbot/commands/index.js.map +1 -0
  104. package/dist/src/platforms/slackbot/commands/message.d.ts +3 -0
  105. package/dist/src/platforms/slackbot/commands/message.d.ts.map +1 -0
  106. package/dist/src/platforms/slackbot/commands/message.js +135 -0
  107. package/dist/src/platforms/slackbot/commands/message.js.map +1 -0
  108. package/dist/src/platforms/slackbot/commands/reaction.d.ts +3 -0
  109. package/dist/src/platforms/slackbot/commands/reaction.d.ts.map +1 -0
  110. package/dist/src/platforms/slackbot/commands/reaction.js +43 -0
  111. package/dist/src/platforms/slackbot/commands/reaction.js.map +1 -0
  112. package/dist/src/platforms/slackbot/commands/shared.d.ts +9 -0
  113. package/dist/src/platforms/slackbot/commands/shared.d.ts.map +1 -0
  114. package/dist/src/platforms/slackbot/commands/shared.js +13 -0
  115. package/dist/src/platforms/slackbot/commands/shared.js.map +1 -0
  116. package/dist/src/platforms/slackbot/commands/user.d.ts +3 -0
  117. package/dist/src/platforms/slackbot/commands/user.d.ts.map +1 -0
  118. package/dist/src/platforms/slackbot/commands/user.js +40 -0
  119. package/dist/src/platforms/slackbot/commands/user.js.map +1 -0
  120. package/dist/src/platforms/slackbot/credential-manager.d.ts +18 -0
  121. package/dist/src/platforms/slackbot/credential-manager.d.ts.map +1 -0
  122. package/dist/src/platforms/slackbot/credential-manager.js +185 -0
  123. package/dist/src/platforms/slackbot/credential-manager.js.map +1 -0
  124. package/dist/src/platforms/slackbot/index.d.ts +4 -0
  125. package/dist/src/platforms/slackbot/index.d.ts.map +1 -0
  126. package/dist/src/platforms/slackbot/index.js +4 -0
  127. package/dist/src/platforms/slackbot/index.js.map +1 -0
  128. package/dist/src/platforms/slackbot/types.d.ts +460 -0
  129. package/dist/src/platforms/slackbot/types.d.ts.map +1 -0
  130. package/dist/src/platforms/slackbot/types.js +114 -0
  131. package/dist/src/platforms/slackbot/types.js.map +1 -0
  132. package/dist/src/platforms/teams/client.d.ts.map +1 -1
  133. package/dist/src/platforms/teams/client.js.map +1 -1
  134. package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
  135. package/dist/src/platforms/teams/commands/auth.js.map +1 -1
  136. package/dist/src/platforms/teams/commands/channel.d.ts.map +1 -1
  137. package/dist/src/platforms/teams/commands/channel.js.map +1 -1
  138. package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
  139. package/dist/src/platforms/teams/commands/file.js.map +1 -1
  140. package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
  141. package/dist/src/platforms/teams/commands/message.js.map +1 -1
  142. package/dist/src/platforms/teams/commands/reaction.d.ts.map +1 -1
  143. package/dist/src/platforms/teams/commands/reaction.js.map +1 -1
  144. package/dist/src/platforms/teams/commands/snapshot.d.ts.map +1 -1
  145. package/dist/src/platforms/teams/commands/snapshot.js.map +1 -1
  146. package/dist/src/platforms/teams/commands/team.d.ts.map +1 -1
  147. package/dist/src/platforms/teams/commands/team.js +1 -4
  148. package/dist/src/platforms/teams/commands/team.js.map +1 -1
  149. package/dist/src/platforms/teams/commands/user.d.ts.map +1 -1
  150. package/dist/src/platforms/teams/commands/user.js.map +1 -1
  151. package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
  152. package/dist/src/platforms/teams/token-extractor.js +3 -1
  153. package/dist/src/platforms/teams/token-extractor.js.map +1 -1
  154. package/docs/content/docs/agent-skills.mdx +4 -4
  155. package/docs/content/docs/index.mdx +11 -18
  156. package/docs/content/docs/integrations/discord.mdx +65 -1
  157. package/docs/content/docs/integrations/meta.json +1 -1
  158. package/docs/content/docs/integrations/slack.mdx +51 -1
  159. package/docs/content/docs/integrations/slackbot.mdx +214 -0
  160. package/docs/content/docs/integrations/teams.mdx +4 -1
  161. package/docs/content/docs/quick-start.mdx +3 -2
  162. package/docs/src/app/icon.png +0 -0
  163. package/docs/src/app/layout.config.tsx +8 -1
  164. package/docs/src/app/page.tsx +18 -1
  165. package/e2e/config.ts +26 -0
  166. package/e2e/helpers.ts +6 -1
  167. package/e2e/slackbot.e2e.test.ts +306 -0
  168. package/package.json +16 -8
  169. package/scripts/prepublish.ts +11 -0
  170. package/skills/agent-slackbot/SKILL.md +285 -0
  171. package/skills/agent-slackbot/references/authentication.md +253 -0
  172. package/skills/agent-slackbot/references/common-patterns.md +218 -0
  173. package/skills/agent-slackbot/templates/monitor-channel.sh +98 -0
  174. package/skills/agent-slackbot/templates/post-message.sh +107 -0
  175. package/skills/agent-slackbot/templates/workspace-summary.sh +113 -0
  176. package/src/cli.ts +1 -4
  177. package/src/platforms/discord/client.test.ts +6 -14
  178. package/src/platforms/discord/client.ts +12 -34
  179. package/src/platforms/discord/commands/auth.test.ts +2 -7
  180. package/src/platforms/discord/commands/auth.ts +14 -19
  181. package/src/platforms/discord/commands/channel.test.ts +18 -20
  182. package/src/platforms/discord/commands/channel.ts +9 -18
  183. package/src/platforms/discord/commands/dm.test.ts +1 -3
  184. package/src/platforms/discord/commands/dm.ts +6 -10
  185. package/src/platforms/discord/commands/file.ts +10 -23
  186. package/src/platforms/discord/commands/friend.ts +33 -35
  187. package/src/platforms/discord/commands/member.ts +5 -7
  188. package/src/platforms/discord/commands/mention.ts +5 -11
  189. package/src/platforms/discord/commands/message.test.ts +1 -3
  190. package/src/platforms/discord/commands/message.ts +23 -61
  191. package/src/platforms/discord/commands/note.ts +7 -15
  192. package/src/platforms/discord/commands/profile.ts +4 -6
  193. package/src/platforms/discord/commands/reaction.test.ts +1 -3
  194. package/src/platforms/discord/commands/reaction.ts +19 -29
  195. package/src/platforms/discord/commands/server.test.ts +14 -18
  196. package/src/platforms/discord/commands/server.ts +9 -15
  197. package/src/platforms/discord/commands/snapshot.ts +5 -7
  198. package/src/platforms/discord/commands/thread.ts +8 -15
  199. package/src/platforms/discord/commands/user.ts +9 -20
  200. package/src/platforms/discord/credential-manager.test.ts +2 -2
  201. package/src/platforms/discord/credential-manager.ts +1 -3
  202. package/src/platforms/discord/token-extractor.test.ts +28 -57
  203. package/src/platforms/discord/token-extractor.ts +10 -30
  204. package/src/platforms/discord/types.ts +1 -1
  205. package/src/platforms/slack/client.test.ts +14 -20
  206. package/src/platforms/slack/client.ts +4 -11
  207. package/src/platforms/slack/commands/activity.test.ts +3 -9
  208. package/src/platforms/slack/commands/activity.ts +7 -12
  209. package/src/platforms/slack/commands/auth.test.ts +2 -2
  210. package/src/platforms/slack/commands/auth.ts +15 -31
  211. package/src/platforms/slack/commands/channel.ts +10 -32
  212. package/src/platforms/slack/commands/drafts.ts +5 -14
  213. package/src/platforms/slack/commands/file.ts +9 -29
  214. package/src/platforms/slack/commands/message.ts +23 -67
  215. package/src/platforms/slack/commands/reaction.ts +19 -48
  216. package/src/platforms/slack/commands/saved.ts +5 -14
  217. package/src/platforms/slack/commands/sections.ts +4 -11
  218. package/src/platforms/slack/commands/snapshot.test.ts +1 -3
  219. package/src/platforms/slack/commands/snapshot.ts +5 -10
  220. package/src/platforms/slack/commands/unread.test.ts +6 -8
  221. package/src/platforms/slack/commands/unread.ts +10 -33
  222. package/src/platforms/slack/commands/user.test.ts +1 -4
  223. package/src/platforms/slack/commands/user.ts +6 -8
  224. package/src/platforms/slack/commands/workspace.test.ts +1 -1
  225. package/src/platforms/slack/commands/workspace.ts +7 -12
  226. package/src/platforms/slack/token-extractor-node-test.ts +1 -1
  227. package/src/platforms/slack/token-extractor.ts +8 -17
  228. package/src/platforms/slack/types.ts +1 -1
  229. package/src/platforms/slackbot/cli.ts +24 -0
  230. package/src/platforms/slackbot/client.test.ts +282 -0
  231. package/src/platforms/slackbot/client.ts +394 -0
  232. package/src/platforms/slackbot/commands/auth.test.ts +245 -0
  233. package/src/platforms/slackbot/commands/auth.ts +240 -0
  234. package/src/platforms/slackbot/commands/channel.ts +46 -0
  235. package/src/platforms/slackbot/commands/index.ts +5 -0
  236. package/src/platforms/slackbot/commands/message.ts +169 -0
  237. package/src/platforms/slackbot/commands/reaction.ts +49 -0
  238. package/src/platforms/slackbot/commands/shared.ts +21 -0
  239. package/src/platforms/slackbot/commands/user.ts +46 -0
  240. package/src/platforms/slackbot/credential-manager.test.ts +264 -0
  241. package/src/platforms/slackbot/credential-manager.ts +213 -0
  242. package/src/platforms/slackbot/index.ts +19 -0
  243. package/src/platforms/slackbot/types.test.ts +90 -0
  244. package/src/platforms/slackbot/types.ts +222 -0
  245. package/src/platforms/teams/client.test.ts +15 -32
  246. package/src/platforms/teams/client.ts +18 -51
  247. package/src/platforms/teams/commands/auth.test.ts +6 -16
  248. package/src/platforms/teams/commands/auth.ts +16 -26
  249. package/src/platforms/teams/commands/channel.test.ts +2 -5
  250. package/src/platforms/teams/commands/channel.ts +10 -20
  251. package/src/platforms/teams/commands/file.test.ts +1 -4
  252. package/src/platforms/teams/commands/file.ts +11 -21
  253. package/src/platforms/teams/commands/message.test.ts +1 -3
  254. package/src/platforms/teams/commands/message.ts +15 -25
  255. package/src/platforms/teams/commands/reaction.test.ts +2 -7
  256. package/src/platforms/teams/commands/reaction.ts +12 -16
  257. package/src/platforms/teams/commands/snapshot.ts +6 -11
  258. package/src/platforms/teams/commands/team.test.ts +15 -26
  259. package/src/platforms/teams/commands/team.ts +10 -19
  260. package/src/platforms/teams/commands/user.ts +8 -14
  261. package/src/platforms/teams/credential-manager.test.ts +2 -5
  262. package/src/platforms/teams/token-extractor.test.ts +21 -50
  263. package/src/platforms/teams/token-extractor.ts +12 -20
  264. package/src/platforms/teams/types.ts +1 -1
  265. package/src/shared/utils/concurrency.test.ts +2 -2
  266. package/src/shared/utils/concurrency.ts +1 -1
  267. package/.claude/commands/release.md +0 -92
  268. package/dist/src/platforms/discord/commands/guild.d.ts +0 -15
  269. package/dist/src/platforms/discord/commands/guild.d.ts.map +0 -1
  270. package/dist/src/platforms/discord/commands/guild.js +0 -102
  271. package/dist/src/platforms/discord/commands/guild.js.map +0 -1
@@ -0,0 +1,394 @@
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(lastError?.message || 'Unknown error', (lastError as any)?.code || 'unknown_error')
37
+ }
38
+
39
+ private sleep(ms: number): Promise<void> {
40
+ return new Promise((resolve) => setTimeout(resolve, ms))
41
+ }
42
+
43
+ private checkResponse(response: { ok?: boolean; error?: string }): void {
44
+ if (!response.ok) {
45
+ throw new SlackBotError(response.error || 'API call failed', response.error || 'api_error')
46
+ }
47
+ }
48
+
49
+ async testAuth(): Promise<{
50
+ user_id: string
51
+ team_id: string
52
+ bot_id?: string
53
+ user?: string
54
+ team?: string
55
+ }> {
56
+ return this.withRetry(async () => {
57
+ const response = await this.client.auth.test()
58
+ this.checkResponse(response)
59
+ return {
60
+ user_id: response.user_id!,
61
+ team_id: response.team_id!,
62
+ bot_id: response.bot_id,
63
+ user: response.user,
64
+ team: response.team,
65
+ }
66
+ })
67
+ }
68
+
69
+ async postMessage(channel: string, text: string, options?: { thread_ts?: string }): Promise<SlackMessage> {
70
+ return this.withRetry(async () => {
71
+ const response = await this.client.chat.postMessage({
72
+ channel,
73
+ text,
74
+ thread_ts: options?.thread_ts,
75
+ })
76
+ this.checkResponse(response)
77
+
78
+ const msg = response.message!
79
+ return {
80
+ ts: response.ts!,
81
+ text: msg.text || text,
82
+ type: msg.type || 'message',
83
+ user: msg.user,
84
+ thread_ts: msg.thread_ts,
85
+ }
86
+ })
87
+ }
88
+
89
+ async getConversationHistory(
90
+ channel: string,
91
+ options?: { limit?: number; cursor?: string },
92
+ ): Promise<SlackMessage[]> {
93
+ return this.withRetry(async () => {
94
+ const response = await this.client.conversations.history({
95
+ channel,
96
+ limit: options?.limit || 20,
97
+ cursor: options?.cursor,
98
+ })
99
+ this.checkResponse(response)
100
+
101
+ return (response.messages || []).map((msg) => ({
102
+ ts: msg.ts!,
103
+ text: msg.text || '',
104
+ type: msg.type || 'message',
105
+ user: msg.user,
106
+ username: msg.username,
107
+ thread_ts: msg.thread_ts,
108
+ reply_count: msg.reply_count,
109
+ replies: (msg as any).replies,
110
+ edited: msg.edited
111
+ ? {
112
+ user: msg.edited.user || '',
113
+ ts: msg.edited.ts || '',
114
+ }
115
+ : undefined,
116
+ }))
117
+ })
118
+ }
119
+
120
+ async getMessage(channel: string, ts: string): Promise<SlackMessage | null> {
121
+ return this.withRetry(async () => {
122
+ const response = await this.client.conversations.history({
123
+ channel,
124
+ oldest: ts,
125
+ inclusive: true,
126
+ limit: 1,
127
+ })
128
+ this.checkResponse(response)
129
+
130
+ const msg = response.messages?.[0]
131
+ if (!msg || msg.ts !== ts) {
132
+ return null
133
+ }
134
+
135
+ return {
136
+ ts: msg.ts!,
137
+ text: msg.text || '',
138
+ type: msg.type || 'message',
139
+ user: msg.user,
140
+ username: msg.username,
141
+ thread_ts: msg.thread_ts,
142
+ reply_count: msg.reply_count,
143
+ replies: (msg as any).replies,
144
+ edited: msg.edited
145
+ ? {
146
+ user: msg.edited.user || '',
147
+ ts: msg.edited.ts || '',
148
+ }
149
+ : undefined,
150
+ }
151
+ })
152
+ }
153
+
154
+ async addReaction(channel: string, timestamp: string, emoji: string): Promise<void> {
155
+ // Normalize emoji (remove colons if present)
156
+ const normalizedEmoji = emoji.replace(/^:|:$/g, '')
157
+
158
+ return this.withRetry(async () => {
159
+ const response = await this.client.reactions.add({
160
+ channel,
161
+ timestamp,
162
+ name: normalizedEmoji,
163
+ })
164
+ this.checkResponse(response)
165
+ })
166
+ }
167
+
168
+ async removeReaction(channel: string, timestamp: string, emoji: string): Promise<void> {
169
+ // Normalize emoji (remove colons if present)
170
+ const normalizedEmoji = emoji.replace(/^:|:$/g, '')
171
+
172
+ return this.withRetry(async () => {
173
+ const response = await this.client.reactions.remove({
174
+ channel,
175
+ timestamp,
176
+ name: normalizedEmoji,
177
+ })
178
+ this.checkResponse(response)
179
+ })
180
+ }
181
+
182
+ async listChannels(options?: { limit?: number; cursor?: string }): Promise<SlackChannel[]> {
183
+ const channels: SlackChannel[] = []
184
+ let cursor: string | undefined = options?.cursor
185
+
186
+ do {
187
+ // Only wrap individual API call in withRetry, not the entire loop
188
+ const response = await this.withRetry(async () => {
189
+ const res = await this.client.conversations.list({
190
+ cursor,
191
+ limit: options?.limit || 200,
192
+ types: 'public_channel,private_channel',
193
+ })
194
+ this.checkResponse(res)
195
+ return res
196
+ })
197
+
198
+ if (response.channels) {
199
+ for (const ch of response.channels) {
200
+ channels.push({
201
+ id: ch.id!,
202
+ name: ch.name!,
203
+ is_private: ch.is_private || false,
204
+ is_archived: ch.is_archived || false,
205
+ created: ch.created || 0,
206
+ creator: ch.creator || '',
207
+ topic: ch.topic
208
+ ? {
209
+ value: ch.topic.value || '',
210
+ creator: ch.topic.creator || '',
211
+ last_set: ch.topic.last_set || 0,
212
+ }
213
+ : undefined,
214
+ purpose: ch.purpose
215
+ ? {
216
+ value: ch.purpose.value || '',
217
+ creator: ch.purpose.creator || '',
218
+ last_set: ch.purpose.last_set || 0,
219
+ }
220
+ : undefined,
221
+ })
222
+ }
223
+ }
224
+
225
+ cursor = response.response_metadata?.next_cursor
226
+ // Only paginate if no specific limit was requested
227
+ if (options?.limit) break
228
+ } while (cursor)
229
+
230
+ return channels
231
+ }
232
+
233
+ async getChannelInfo(channel: string): Promise<SlackChannel> {
234
+ return this.withRetry(async () => {
235
+ const response = await this.client.conversations.info({ channel })
236
+ this.checkResponse(response)
237
+
238
+ const ch = response.channel!
239
+ return {
240
+ id: ch.id!,
241
+ name: ch.name!,
242
+ is_private: ch.is_private || false,
243
+ is_archived: ch.is_archived || false,
244
+ created: ch.created || 0,
245
+ creator: ch.creator || '',
246
+ topic: ch.topic
247
+ ? {
248
+ value: ch.topic.value || '',
249
+ creator: ch.topic.creator || '',
250
+ last_set: ch.topic.last_set || 0,
251
+ }
252
+ : undefined,
253
+ purpose: ch.purpose
254
+ ? {
255
+ value: ch.purpose.value || '',
256
+ creator: ch.purpose.creator || '',
257
+ last_set: ch.purpose.last_set || 0,
258
+ }
259
+ : undefined,
260
+ }
261
+ })
262
+ }
263
+
264
+ async listUsers(options?: { limit?: number; cursor?: string }): Promise<SlackUser[]> {
265
+ const users: SlackUser[] = []
266
+ let cursor: string | undefined = options?.cursor
267
+
268
+ do {
269
+ // Only wrap individual API call in withRetry, not the entire loop
270
+ const response = await this.withRetry(async () => {
271
+ const res = await this.client.users.list({
272
+ cursor,
273
+ limit: options?.limit || 200,
274
+ })
275
+ this.checkResponse(res)
276
+ return res
277
+ })
278
+
279
+ if (response.members) {
280
+ for (const member of response.members) {
281
+ users.push({
282
+ id: member.id!,
283
+ name: member.name!,
284
+ real_name: member.real_name || member.name || '',
285
+ is_admin: member.is_admin || false,
286
+ is_owner: member.is_owner || false,
287
+ is_bot: member.is_bot || false,
288
+ is_app_user: member.is_app_user || false,
289
+ profile: member.profile
290
+ ? {
291
+ email: member.profile.email,
292
+ phone: member.profile.phone,
293
+ title: member.profile.title,
294
+ status_text: member.profile.status_text,
295
+ }
296
+ : undefined,
297
+ })
298
+ }
299
+ }
300
+
301
+ cursor = response.response_metadata?.next_cursor
302
+ // Only paginate if no specific limit was requested
303
+ if (options?.limit) break
304
+ } while (cursor)
305
+
306
+ return users
307
+ }
308
+
309
+ async getUserInfo(userId: string): Promise<SlackUser> {
310
+ return this.withRetry(async () => {
311
+ const response = await this.client.users.info({ user: userId })
312
+ this.checkResponse(response)
313
+
314
+ const member = response.user!
315
+ return {
316
+ id: member.id!,
317
+ name: member.name!,
318
+ real_name: member.real_name || member.name || '',
319
+ is_admin: member.is_admin || false,
320
+ is_owner: member.is_owner || false,
321
+ is_bot: member.is_bot || false,
322
+ is_app_user: member.is_app_user || false,
323
+ profile: member.profile
324
+ ? {
325
+ email: member.profile.email,
326
+ phone: member.profile.phone,
327
+ title: member.profile.title,
328
+ status_text: member.profile.status_text,
329
+ }
330
+ : undefined,
331
+ }
332
+ })
333
+ }
334
+
335
+ async updateMessage(channel: string, ts: string, text: string): Promise<SlackMessage> {
336
+ return this.withRetry(async () => {
337
+ const response = await this.client.chat.update({ channel, ts, text })
338
+ this.checkResponse(response)
339
+ const msg = (response as any).message
340
+ return {
341
+ ts: response.ts!,
342
+ text: msg?.text || response.text || text,
343
+ type: msg?.type || 'message',
344
+ user: msg?.user,
345
+ }
346
+ })
347
+ }
348
+
349
+ async getThreadReplies(
350
+ channel: string,
351
+ ts: string,
352
+ options?: { limit?: number; cursor?: string },
353
+ ): Promise<SlackMessage[]> {
354
+ return this.withRetry(async () => {
355
+ const response = await this.client.conversations.replies({
356
+ channel,
357
+ ts,
358
+ limit: options?.limit || 100,
359
+ cursor: options?.cursor,
360
+ })
361
+ this.checkResponse(response)
362
+
363
+ return (response.messages || []).map((msg: any) => ({
364
+ ts: msg.ts!,
365
+ text: msg.text || '',
366
+ type: msg.type || 'message',
367
+ user: msg.user,
368
+ username: msg.username,
369
+ thread_ts: msg.thread_ts,
370
+ reply_count: msg.reply_count,
371
+ edited: msg.edited
372
+ ? {
373
+ user: msg.edited.user || '',
374
+ ts: msg.edited.ts || '',
375
+ }
376
+ : undefined,
377
+ }))
378
+ })
379
+ }
380
+
381
+ async joinChannel(channel: string): Promise<void> {
382
+ return this.withRetry(async () => {
383
+ const response = await this.client.conversations.join({ channel })
384
+ this.checkResponse(response)
385
+ })
386
+ }
387
+
388
+ async deleteMessage(channel: string, ts: string): Promise<void> {
389
+ return this.withRetry(async () => {
390
+ const response = await this.client.chat.delete({ channel, ts })
391
+ this.checkResponse(response)
392
+ })
393
+ }
394
+ }
@@ -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
+ })