@urugus/slack-cli 0.2.12 → 0.2.13

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 (97) hide show
  1. package/README.md +4 -0
  2. package/dist/commands/history-display.d.ts +5 -1
  3. package/dist/commands/history-display.d.ts.map +1 -1
  4. package/dist/commands/history-display.js +3 -3
  5. package/dist/commands/history-display.js.map +1 -1
  6. package/dist/commands/history.d.ts.map +1 -1
  7. package/dist/commands/history.js +28 -11
  8. package/dist/commands/history.js.map +1 -1
  9. package/dist/types/commands.d.ts +1 -0
  10. package/dist/types/commands.d.ts.map +1 -1
  11. package/dist/utils/slack-api-client.d.ts +1 -0
  12. package/dist/utils/slack-api-client.d.ts.map +1 -1
  13. package/dist/utils/slack-api-client.js +3 -0
  14. package/dist/utils/slack-api-client.js.map +1 -1
  15. package/dist/utils/slack-operations/message-operations.d.ts +1 -0
  16. package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
  17. package/dist/utils/slack-operations/message-operations.js +21 -0
  18. package/dist/utils/slack-operations/message-operations.js.map +1 -1
  19. package/package.json +5 -2
  20. package/.claude/settings.local.json +0 -75
  21. package/.github/dependabot.yml +0 -18
  22. package/.github/workflows/ci.yml +0 -70
  23. package/.github/workflows/pr-validation.yml +0 -41
  24. package/.prettierignore +0 -11
  25. package/.prettierrc +0 -10
  26. package/CHANGELOG.md +0 -61
  27. package/CLAUDE.md +0 -16
  28. package/eslint.config.js +0 -38
  29. package/src/commands/channels.ts +0 -50
  30. package/src/commands/config-subcommands.ts +0 -63
  31. package/src/commands/config.ts +0 -50
  32. package/src/commands/history-display.ts +0 -19
  33. package/src/commands/history-validators.ts +0 -46
  34. package/src/commands/history.ts +0 -61
  35. package/src/commands/scheduled.ts +0 -71
  36. package/src/commands/send.ts +0 -69
  37. package/src/commands/unread.ts +0 -122
  38. package/src/index.ts +0 -27
  39. package/src/types/commands.ts +0 -58
  40. package/src/types/config.ts +0 -20
  41. package/src/utils/channel-formatter.ts +0 -45
  42. package/src/utils/channel-resolver.ts +0 -82
  43. package/src/utils/client-factory.ts +0 -10
  44. package/src/utils/command-wrapper.ts +0 -27
  45. package/src/utils/config/config-file-manager.ts +0 -56
  46. package/src/utils/config/profile-manager.ts +0 -79
  47. package/src/utils/config/token-crypto-service.ts +0 -80
  48. package/src/utils/config-helper.ts +0 -21
  49. package/src/utils/constants.ts +0 -78
  50. package/src/utils/date-utils.ts +0 -8
  51. package/src/utils/error-utils.ts +0 -6
  52. package/src/utils/errors.ts +0 -33
  53. package/src/utils/format-utils.ts +0 -9
  54. package/src/utils/formatters/base-formatter.ts +0 -34
  55. package/src/utils/formatters/channel-formatters.ts +0 -71
  56. package/src/utils/formatters/channels-list-formatters.ts +0 -55
  57. package/src/utils/formatters/history-formatters.ts +0 -123
  58. package/src/utils/formatters/message-formatters.ts +0 -85
  59. package/src/utils/mention-utils.ts +0 -47
  60. package/src/utils/option-parsers.ts +0 -100
  61. package/src/utils/profile-config.ts +0 -161
  62. package/src/utils/schedule-utils.ts +0 -41
  63. package/src/utils/slack-api-client.ts +0 -135
  64. package/src/utils/slack-operations/base-client.ts +0 -30
  65. package/src/utils/slack-operations/channel-operations.ts +0 -161
  66. package/src/utils/slack-operations/index.ts +0 -3
  67. package/src/utils/slack-operations/message-operations.ts +0 -176
  68. package/src/utils/slack-patterns.ts +0 -9
  69. package/src/utils/token-utils.ts +0 -17
  70. package/src/utils/validators.ts +0 -263
  71. package/tests/commands/channels.test.ts +0 -250
  72. package/tests/commands/config.test.ts +0 -158
  73. package/tests/commands/history.test.ts +0 -403
  74. package/tests/commands/scheduled.test.ts +0 -131
  75. package/tests/commands/send.test.ts +0 -414
  76. package/tests/commands/unread.test.ts +0 -492
  77. package/tests/index.test.ts +0 -40
  78. package/tests/test-utils.ts +0 -28
  79. package/tests/utils/channel-resolver.test.ts +0 -161
  80. package/tests/utils/config/config-file-manager.test.ts +0 -118
  81. package/tests/utils/config/profile-manager.test.ts +0 -266
  82. package/tests/utils/config/token-crypto-service.test.ts +0 -98
  83. package/tests/utils/config.test.ts +0 -400
  84. package/tests/utils/date-utils.test.ts +0 -30
  85. package/tests/utils/error-utils.test.ts +0 -34
  86. package/tests/utils/format-utils.test.ts +0 -61
  87. package/tests/utils/mention-utils.test.ts +0 -100
  88. package/tests/utils/option-parsers.test.ts +0 -173
  89. package/tests/utils/profile-config.test.ts +0 -282
  90. package/tests/utils/schedule-utils.test.ts +0 -63
  91. package/tests/utils/slack-api-client.test.ts +0 -313
  92. package/tests/utils/slack-operations/channel-operations.test.ts +0 -248
  93. package/tests/utils/slack-operations/message-operations.test.ts +0 -163
  94. package/tests/utils/token-utils.test.ts +0 -33
  95. package/tests/utils/validators.test.ts +0 -307
  96. package/tsconfig.json +0 -22
  97. package/vitest.config.ts +0 -27
@@ -1,313 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { SlackApiClient } from '../../src/utils/slack-api-client';
3
- import { WebClient, LogLevel } from '@slack/web-api';
4
-
5
- vi.mock('@slack/web-api');
6
-
7
- describe('SlackApiClient', () => {
8
- let client: SlackApiClient;
9
- let mockWebClient: any;
10
-
11
- beforeEach(() => {
12
- vi.clearAllMocks();
13
- mockWebClient = {
14
- chat: {
15
- postMessage: vi.fn(),
16
- scheduleMessage: vi.fn(),
17
- scheduledMessages: {
18
- list: vi.fn(),
19
- },
20
- },
21
- conversations: {
22
- list: vi.fn(),
23
- info: vi.fn(),
24
- },
25
- users: {
26
- conversations: vi.fn(),
27
- },
28
- };
29
- vi.mocked(WebClient).mockReturnValue(mockWebClient);
30
- client = new SlackApiClient('test-token');
31
- });
32
-
33
- describe('constructor', () => {
34
- it('should create WebClient with provided token', () => {
35
- expect(WebClient).toHaveBeenCalledWith('test-token', {
36
- retryConfig: {
37
- retries: 0, // Disabled to handle rate limits manually
38
- },
39
- logLevel: LogLevel.ERROR,
40
- });
41
- });
42
- });
43
-
44
- describe('sendMessage', () => {
45
- it('should send message to channel', async () => {
46
- const mockResponse = { ok: true, ts: '1234567890.123456' };
47
- vi.mocked(mockWebClient.chat.postMessage).mockResolvedValue(mockResponse as any);
48
-
49
- const result = await client.sendMessage('general', 'Hello, World!');
50
-
51
- expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
52
- channel: 'general',
53
- text: 'Hello, World!',
54
- });
55
- expect(result).toEqual(mockResponse);
56
- });
57
-
58
- it('should handle channel ID format', async () => {
59
- const mockResponse = { ok: true, ts: '1234567890.123456' };
60
- vi.mocked(mockWebClient.chat.postMessage).mockResolvedValue(mockResponse as any);
61
-
62
- await client.sendMessage('C1234567890', 'Hello!');
63
-
64
- expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
65
- channel: 'C1234567890',
66
- text: 'Hello!',
67
- });
68
- });
69
-
70
- it('should handle multi-line messages', async () => {
71
- const mockResponse = { ok: true, ts: '1234567890.123456' };
72
- vi.mocked(mockWebClient.chat.postMessage).mockResolvedValue(mockResponse as any);
73
-
74
- const multiLineMessage = 'Line 1\nLine 2\nLine 3';
75
- await client.sendMessage('general', multiLineMessage);
76
-
77
- expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
78
- channel: 'general',
79
- text: multiLineMessage,
80
- });
81
- });
82
-
83
- it('should throw error on API failure', async () => {
84
- const mockError = new Error('channel_not_found');
85
- vi.mocked(mockWebClient.chat.postMessage).mockRejectedValue(mockError);
86
-
87
- await expect(client.sendMessage('nonexistent', 'Hello')).rejects.toThrow('channel_not_found');
88
- });
89
- });
90
-
91
- describe('scheduleMessage', () => {
92
- it('should schedule message to channel', async () => {
93
- const mockResponse = { ok: true, scheduled_message_id: 'Q123', post_at: 1770855000 };
94
- vi.mocked(mockWebClient.chat.scheduleMessage).mockResolvedValue(mockResponse as any);
95
-
96
- const result = await client.scheduleMessage('general', 'Hello, future!', 1770855000);
97
-
98
- expect(mockWebClient.chat.scheduleMessage).toHaveBeenCalledWith({
99
- channel: 'general',
100
- text: 'Hello, future!',
101
- post_at: 1770855000,
102
- });
103
- expect(result).toEqual(mockResponse);
104
- });
105
- });
106
-
107
- describe('listScheduledMessages', () => {
108
- it('should list scheduled messages', async () => {
109
- const mockResponse = {
110
- ok: true,
111
- scheduled_messages: [
112
- { id: 'Q123', channel_id: 'C123', post_at: 1770855000, date_created: 1770854400 },
113
- ],
114
- };
115
- vi.mocked(mockWebClient.chat.scheduledMessages.list).mockResolvedValue(mockResponse as any);
116
-
117
- const result = await client.listScheduledMessages();
118
-
119
- expect(mockWebClient.chat.scheduledMessages.list).toHaveBeenCalledWith({ limit: 50 });
120
- expect(result).toEqual(mockResponse.scheduled_messages);
121
- });
122
- });
123
-
124
- describe('listChannels', () => {
125
- it('should list channels with default options', async () => {
126
- const mockChannels = [
127
- {
128
- id: 'C1234567890',
129
- name: 'general',
130
- is_channel: true,
131
- is_private: false,
132
- num_members: 150,
133
- created: 1579075200,
134
- purpose: { value: 'Company announcements' },
135
- },
136
- ];
137
- vi.mocked(mockWebClient.conversations.list).mockResolvedValue({
138
- ok: true,
139
- channels: mockChannels,
140
- } as any);
141
-
142
- const result = await client.listChannels({
143
- types: 'public_channel',
144
- exclude_archived: true,
145
- limit: 100,
146
- });
147
-
148
- expect(mockWebClient.conversations.list).toHaveBeenCalledWith({
149
- types: 'public_channel',
150
- exclude_archived: true,
151
- limit: 100,
152
- });
153
- expect(result).toEqual(mockChannels);
154
- });
155
-
156
- it('should handle private channels', async () => {
157
- const mockChannels = [
158
- {
159
- id: 'G1234567890',
160
- name: 'private-channel',
161
- is_group: true,
162
- is_private: true,
163
- num_members: 10,
164
- created: 1579075200,
165
- purpose: { value: 'Private discussions' },
166
- },
167
- ];
168
- vi.mocked(mockWebClient.conversations.list).mockResolvedValue({
169
- ok: true,
170
- channels: mockChannels,
171
- } as any);
172
-
173
- const result = await client.listChannels({
174
- types: 'private_channel',
175
- exclude_archived: true,
176
- limit: 50,
177
- });
178
-
179
- expect(mockWebClient.conversations.list).toHaveBeenCalledWith({
180
- types: 'private_channel',
181
- exclude_archived: true,
182
- limit: 50,
183
- });
184
- expect(result).toEqual(mockChannels);
185
- });
186
-
187
- it('should handle multiple channel types', async () => {
188
- vi.mocked(mockWebClient.conversations.list).mockResolvedValue({
189
- ok: true,
190
- channels: [],
191
- } as any);
192
-
193
- await client.listChannels({
194
- types: 'public_channel,private_channel,im',
195
- exclude_archived: false,
196
- limit: 200,
197
- });
198
-
199
- expect(mockWebClient.conversations.list).toHaveBeenCalledWith({
200
- types: 'public_channel,private_channel,im',
201
- exclude_archived: false,
202
- limit: 200,
203
- });
204
- });
205
-
206
- it('should handle API errors', async () => {
207
- vi.mocked(mockWebClient.conversations.list).mockRejectedValue(new Error('invalid_auth'));
208
-
209
- await expect(
210
- client.listChannels({
211
- types: 'public_channel',
212
- exclude_archived: true,
213
- limit: 100,
214
- })
215
- ).rejects.toThrow('invalid_auth');
216
- });
217
-
218
- it('should handle pagination when listing channels', async () => {
219
- // First page
220
- vi.mocked(mockWebClient.conversations.list).mockResolvedValueOnce({
221
- ok: true,
222
- channels: [
223
- { id: 'C001', name: 'channel1', is_private: false },
224
- { id: 'C002', name: 'channel2', is_private: false },
225
- ],
226
- response_metadata: {
227
- next_cursor: 'cursor123',
228
- },
229
- } as any);
230
-
231
- // Second page
232
- vi.mocked(mockWebClient.conversations.list).mockResolvedValueOnce({
233
- ok: true,
234
- channels: [
235
- { id: 'C003', name: 'channel3', is_private: false },
236
- { id: 'C004', name: 'channel4', is_private: false },
237
- ],
238
- response_metadata: {
239
- next_cursor: 'cursor456',
240
- },
241
- } as any);
242
-
243
- // Third page (last page)
244
- vi.mocked(mockWebClient.conversations.list).mockResolvedValueOnce({
245
- ok: true,
246
- channels: [{ id: 'C005', name: 'channel5', is_private: false }],
247
- response_metadata: {
248
- next_cursor: '',
249
- },
250
- } as any);
251
-
252
- const result = await client.listChannels({
253
- types: 'public_channel',
254
- exclude_archived: true,
255
- limit: 2,
256
- });
257
-
258
- // Should have called the API 3 times
259
- expect(mockWebClient.conversations.list).toHaveBeenCalledTimes(3);
260
-
261
- // First call
262
- expect(mockWebClient.conversations.list).toHaveBeenNthCalledWith(1, {
263
- types: 'public_channel',
264
- exclude_archived: true,
265
- limit: 2,
266
- cursor: undefined,
267
- });
268
-
269
- // Second call
270
- expect(mockWebClient.conversations.list).toHaveBeenNthCalledWith(2, {
271
- types: 'public_channel',
272
- exclude_archived: true,
273
- limit: 2,
274
- cursor: 'cursor123',
275
- });
276
-
277
- // Third call
278
- expect(mockWebClient.conversations.list).toHaveBeenNthCalledWith(3, {
279
- types: 'public_channel',
280
- exclude_archived: true,
281
- limit: 2,
282
- cursor: 'cursor456',
283
- });
284
-
285
- // Should return all channels
286
- expect(result).toHaveLength(5);
287
- expect(result[0].name).toBe('channel1');
288
- expect(result[1].name).toBe('channel2');
289
- expect(result[2].name).toBe('channel3');
290
- expect(result[3].name).toBe('channel4');
291
- expect(result[4].name).toBe('channel5');
292
- });
293
-
294
- it('should handle empty cursor in pagination', async () => {
295
- vi.mocked(mockWebClient.conversations.list).mockResolvedValueOnce({
296
- ok: true,
297
- channels: [{ id: 'C001', name: 'channel1', is_private: false }],
298
- response_metadata: {
299
- // No next_cursor field
300
- },
301
- } as any);
302
-
303
- const result = await client.listChannels({
304
- types: 'public_channel',
305
- exclude_archived: true,
306
- limit: 100,
307
- });
308
-
309
- expect(mockWebClient.conversations.list).toHaveBeenCalledTimes(1);
310
- expect(result).toHaveLength(1);
311
- });
312
- });
313
- });
@@ -1,248 +0,0 @@
1
- import { vi, describe, it, expect, beforeEach } from 'vitest';
2
- import { ChannelOperations } from '../../../src/utils/slack-operations/channel-operations';
3
-
4
- vi.mock('@slack/web-api', () => ({
5
- WebClient: vi.fn().mockImplementation(() => ({
6
- conversations: {
7
- list: vi.fn(),
8
- info: vi.fn(),
9
- history: vi.fn(),
10
- },
11
- })),
12
- LogLevel: {
13
- ERROR: 'error',
14
- },
15
- }));
16
-
17
- vi.mock('p-limit', () => ({
18
- default: () => (fn: any) => fn(),
19
- }));
20
-
21
- describe('ChannelOperations', () => {
22
- let channelOps: ChannelOperations;
23
- let mockClient: any;
24
-
25
- beforeEach(() => {
26
- vi.clearAllMocks();
27
- mockClient = {
28
- conversations: {
29
- list: vi.fn(),
30
- info: vi.fn(),
31
- history: vi.fn(),
32
- },
33
- };
34
- // Create instance with mocked token
35
- channelOps = new ChannelOperations('test-token');
36
- // Replace the client with our mock
37
- (channelOps as any).client = mockClient;
38
- });
39
-
40
- describe('listUnreadChannels', () => {
41
- it('should detect unread messages when last_read is present', async () => {
42
- // Mock conversations.list response
43
- mockClient.conversations.list.mockResolvedValue({
44
- channels: [
45
- { id: 'C123', name: 'general' },
46
- { id: 'C456', name: 'random' },
47
- ],
48
- });
49
-
50
- // Mock conversations.info responses
51
- mockClient.conversations.info
52
- .mockResolvedValueOnce({
53
- channel: {
54
- id: 'C123',
55
- name: 'general',
56
- last_read: '1234567890.000100',
57
- },
58
- })
59
- .mockResolvedValueOnce({
60
- channel: {
61
- id: 'C456',
62
- name: 'random',
63
- last_read: '1234567890.000200',
64
- },
65
- });
66
-
67
- // Mock conversations.history responses
68
- mockClient.conversations.history
69
- // First call for C123 - check latest message
70
- .mockResolvedValueOnce({
71
- messages: [{ ts: '1234567890.000200' }], // Newer than last_read
72
- })
73
- // Second call for C123 - get unread messages
74
- .mockResolvedValueOnce({
75
- messages: [
76
- { ts: '1234567890.000200' },
77
- { ts: '1234567890.000150' },
78
- ],
79
- })
80
- // Third call for C456 - check latest message
81
- .mockResolvedValueOnce({
82
- messages: [{ ts: '1234567890.000100' }], // Older than last_read
83
- });
84
-
85
- const result = await channelOps.listUnreadChannels();
86
-
87
- expect(result).toHaveLength(1);
88
- expect(result[0]).toMatchObject({
89
- id: 'C123',
90
- name: 'general',
91
- unread_count: 2,
92
- unread_count_display: 2,
93
- last_read: '1234567890.000100',
94
- });
95
- });
96
-
97
- it('should count all messages as unread when last_read is not present', async () => {
98
- // Mock conversations.list response
99
- mockClient.conversations.list.mockResolvedValue({
100
- channels: [{ id: 'C789', name: 'no-read-channel' }],
101
- });
102
-
103
- // Mock conversations.info response - no last_read
104
- mockClient.conversations.info.mockResolvedValue({
105
- channel: {
106
- id: 'C789',
107
- name: 'no-read-channel',
108
- // last_read is missing
109
- },
110
- });
111
-
112
- // Mock conversations.history responses
113
- mockClient.conversations.history
114
- // First call - check if channel has messages
115
- .mockResolvedValueOnce({
116
- messages: [{ ts: '1234567890.000300' }],
117
- })
118
- // Second call - get all messages (up to 100)
119
- .mockResolvedValueOnce({
120
- messages: [
121
- { ts: '1234567890.000300' },
122
- { ts: '1234567890.000200' },
123
- { ts: '1234567890.000100' },
124
- ],
125
- });
126
-
127
- const result = await channelOps.listUnreadChannels();
128
-
129
- expect(result).toHaveLength(1);
130
- expect(result[0]).toMatchObject({
131
- id: 'C789',
132
- name: 'no-read-channel',
133
- unread_count: 3,
134
- unread_count_display: 3,
135
- last_read: undefined,
136
- });
137
- });
138
-
139
- it('should skip channels with no messages', async () => {
140
- mockClient.conversations.list.mockResolvedValue({
141
- channels: [{ id: 'C999', name: 'empty-channel' }],
142
- });
143
-
144
- mockClient.conversations.info.mockResolvedValue({
145
- channel: {
146
- id: 'C999',
147
- name: 'empty-channel',
148
- last_read: '1234567890.000100',
149
- },
150
- });
151
-
152
- // No messages in channel
153
- mockClient.conversations.history.mockResolvedValue({
154
- messages: [],
155
- });
156
-
157
- const result = await channelOps.listUnreadChannels();
158
-
159
- expect(result).toHaveLength(0);
160
- });
161
-
162
- it('should skip channels with all messages read', async () => {
163
- mockClient.conversations.list.mockResolvedValue({
164
- channels: [{ id: 'C111', name: 'all-read' }],
165
- });
166
-
167
- mockClient.conversations.info.mockResolvedValue({
168
- channel: {
169
- id: 'C111',
170
- name: 'all-read',
171
- last_read: '1234567890.000200',
172
- },
173
- });
174
-
175
- // First call to get latest message (limit: 1)
176
- mockClient.conversations.history.mockResolvedValueOnce({
177
- messages: [{ ts: '1234567890.000100' }],
178
- });
179
-
180
- // Second call to get messages after last_read (should be empty)
181
- mockClient.conversations.history.mockResolvedValueOnce({
182
- messages: [],
183
- });
184
-
185
- const result = await channelOps.listUnreadChannels();
186
-
187
- expect(result).toHaveLength(0);
188
- });
189
-
190
- it('should handle API errors gracefully', async () => {
191
- mockClient.conversations.list.mockResolvedValue({
192
- channels: [
193
- { id: 'C222', name: 'error-channel' },
194
- { id: 'C333', name: 'good-channel' },
195
- ],
196
- });
197
-
198
- // First channel throws error
199
- mockClient.conversations.info
200
- .mockRejectedValueOnce(new Error('API Error'))
201
- .mockResolvedValueOnce({
202
- channel: {
203
- id: 'C333',
204
- name: 'good-channel',
205
- last_read: '1234567890.000100',
206
- },
207
- });
208
-
209
- mockClient.conversations.history
210
- .mockResolvedValueOnce({
211
- messages: [{ ts: '1234567890.000200' }],
212
- })
213
- .mockResolvedValueOnce({
214
- messages: [{ ts: '1234567890.000200' }],
215
- });
216
-
217
- const result = await channelOps.listUnreadChannels();
218
-
219
- // Should skip the error channel and process the good one
220
- expect(result).toHaveLength(1);
221
- expect(result[0].id).toBe('C333');
222
- });
223
-
224
- it('should handle rate limiting with delay', async () => {
225
- const delaySpy = vi.spyOn(channelOps as any, 'delay');
226
-
227
- mockClient.conversations.list.mockResolvedValue({
228
- channels: [
229
- { id: 'C444', name: 'channel1' },
230
- { id: 'C555', name: 'channel2' },
231
- ],
232
- });
233
-
234
- mockClient.conversations.info.mockResolvedValue({
235
- channel: { id: 'C444', name: 'channel1' },
236
- });
237
-
238
- mockClient.conversations.history.mockResolvedValue({
239
- messages: [],
240
- });
241
-
242
- await channelOps.listUnreadChannels();
243
-
244
- // Verify delay was called between API calls
245
- expect(delaySpy).toHaveBeenCalledWith(100);
246
- });
247
- });
248
- });