@urugus/slack-cli 0.1.9 → 0.2.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.
@@ -55,7 +55,8 @@
55
55
  "Bash(rg:*)",
56
56
  "Bash(npm version:*)",
57
57
  "Bash(npx tsx:*)",
58
- "Bash(gh workflow view:*)"
58
+ "Bash(gh workflow view:*)",
59
+ "Bash(git tag:*)"
59
60
  ],
60
61
  "deny": []
61
62
  }
@@ -8,7 +8,6 @@ interface ChannelWithUnreadInfo extends Channel {
8
8
  export declare class ChannelOperations extends BaseSlackClient {
9
9
  listChannels(options: ListChannelsOptions): Promise<Channel[]>;
10
10
  listUnreadChannels(): Promise<Channel[]>;
11
- private listUnreadChannelsFallback;
12
11
  getChannelInfo(channelNameOrId: string): Promise<ChannelWithUnreadInfo>;
13
12
  }
14
13
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"channel-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,UAAU,qBAAsB,SAAQ,OAAO;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAkB,SAAQ,eAAe;IAC9C,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAuB9D,kBAAkB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YAahC,0BAA0B;IAwClC,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAe9E"}
1
+ {"version":3,"file":"channel-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,UAAU,qBAAsB,SAAQ,OAAO;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAkB,SAAQ,eAAe;IAC9C,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAuB9D,kBAAkB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IA8ExC,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAe9E"}
@@ -24,19 +24,6 @@ class ChannelOperations extends base_client_1.BaseSlackClient {
24
24
  return channels;
25
25
  }
26
26
  async listUnreadChannels() {
27
- try {
28
- // Use users.conversations to get unread counts in a single API call
29
- // This endpoint doesn't return unread_count_display by default,
30
- // so we'll use the fallback method instead
31
- throw new Error('Using fallback method for unread counts');
32
- }
33
- catch (error) {
34
- // Fallback to the old method if users.conversations fails
35
- console.warn('Failed to use users.conversations, falling back to conversations.list');
36
- return this.listUnreadChannelsFallback();
37
- }
38
- }
39
- async listUnreadChannelsFallback() {
40
27
  // Get all conversations the user is a member of
41
28
  const response = await this.client.conversations.list({
42
29
  types: 'public_channel,private_channel,im,mpim',
@@ -53,13 +40,48 @@ class ChannelOperations extends base_client_1.BaseSlackClient {
53
40
  include_num_members: false,
54
41
  });
55
42
  const channelInfo = info.channel;
56
- if (channelInfo.unread_count_display && channelInfo.unread_count_display > 0) {
57
- channelsWithUnread.push({
58
- ...channel,
59
- unread_count: channelInfo.unread_count || 0,
60
- unread_count_display: channelInfo.unread_count_display || 0,
61
- last_read: channelInfo.last_read,
62
- });
43
+ // Get the latest message in the channel
44
+ const history = await this.client.conversations.history({
45
+ channel: channel.id,
46
+ limit: 1,
47
+ });
48
+ if (history.messages && history.messages.length > 0) {
49
+ let hasUnread = false;
50
+ let unreadCount = 0;
51
+ if (!channelInfo.last_read) {
52
+ // If last_read is empty, all messages are unread
53
+ hasUnread = true;
54
+ // Get total message count (up to 100)
55
+ const allHistory = await this.client.conversations.history({
56
+ channel: channel.id,
57
+ limit: 100,
58
+ });
59
+ unreadCount = allHistory.messages?.length || 0;
60
+ }
61
+ else {
62
+ // Check if there are messages after last_read
63
+ const latestMessage = history.messages[0];
64
+ const lastReadTs = parseFloat(channelInfo.last_read);
65
+ const latestMessageTs = parseFloat(latestMessage.ts || '0');
66
+ if (latestMessageTs > lastReadTs) {
67
+ hasUnread = true;
68
+ // Calculate unread count by fetching messages after last_read
69
+ const unreadHistory = await this.client.conversations.history({
70
+ channel: channel.id,
71
+ oldest: channelInfo.last_read,
72
+ limit: 100, // Get up to 100 unread messages
73
+ });
74
+ unreadCount = unreadHistory.messages?.length || 0;
75
+ }
76
+ }
77
+ if (hasUnread) {
78
+ channelsWithUnread.push({
79
+ ...channel,
80
+ unread_count: unreadCount,
81
+ unread_count_display: unreadCount,
82
+ last_read: channelInfo.last_read,
83
+ });
84
+ }
63
85
  }
64
86
  // Add delay between API calls to avoid rate limiting
65
87
  await this.delay(100);
@@ -1 +1 @@
1
- {"version":3,"file":"channel-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":";;;AAAA,+CAAgD;AAChD,0DAAsD;AACtD,4CAAwC;AASxC,MAAa,iBAAkB,SAAQ,6BAAe;IACpD,KAAK,CAAC,YAAY,CAAC,OAA4B;QAC7C,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,IAAI,MAA0B,CAAC;QAE/B,gCAAgC;QAChC,GAAG,CAAC;YACF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;gBACpD,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;gBAC1C,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,MAAM;aACP,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACtB,QAAQ,CAAC,IAAI,CAAC,GAAI,QAAQ,CAAC,QAAsB,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,GAAG,QAAQ,CAAC,iBAAiB,EAAE,WAAW,CAAC;QACnD,CAAC,QAAQ,MAAM,EAAE;QAEjB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,IAAI,CAAC;YACH,oEAAoE;YACpE,gEAAgE;YAChE,2CAA2C;YAC3C,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,0DAA0D;YAC1D,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;YACtF,OAAO,IAAI,CAAC,0BAA0B,EAAE,CAAC;QAC3C,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,0BAA0B;QACtC,gDAAgD;QAChD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;YACpD,KAAK,EAAE,wCAAwC;YAC/C,gBAAgB,EAAE,IAAI;YACtB,KAAK,EAAE,IAAI;SACZ,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAqB,CAAC;QAChD,MAAM,kBAAkB,GAAc,EAAE,CAAC;QAEzC,8DAA8D;QAC9D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;oBAChD,OAAO,EAAE,OAAO,CAAC,EAAE;oBACnB,mBAAmB,EAAE,KAAK;iBAC3B,CAAC,CAAC;gBACH,MAAM,WAAW,GAAG,IAAI,CAAC,OAAgC,CAAC;gBAE1D,IAAI,WAAW,CAAC,oBAAoB,IAAI,WAAW,CAAC,oBAAoB,GAAG,CAAC,EAAE,CAAC;oBAC7E,kBAAkB,CAAC,IAAI,CAAC;wBACtB,GAAG,OAAO;wBACV,YAAY,EAAE,WAAW,CAAC,YAAY,IAAI,CAAC;wBAC3C,oBAAoB,EAAE,WAAW,CAAC,oBAAoB,IAAI,CAAC;wBAC3D,SAAS,EAAE,WAAW,CAAC,SAAS;qBACjC,CAAC,CAAC;gBACL,CAAC;gBAED,qDAAqD;gBACrD,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACxB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,0BAA0B;gBAC1B,MAAM,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QAED,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,eAAuB;QAC1C,MAAM,SAAS,GAAG,MAAM,kCAAe,CAAC,gBAAgB,CAAC,eAAe,EAAE,GAAG,EAAE,CAC7E,IAAI,CAAC,YAAY,CAAC;YAChB,KAAK,EAAE,wCAAwC;YAC/C,gBAAgB,EAAE,IAAI;YACtB,KAAK,EAAE,oBAAQ,CAAC,cAAc;SAC/B,CAAC,CACH,CAAC;QAEF,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;YAChD,OAAO,EAAE,SAAS;SACnB,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,OAAgC,CAAC;IAC/C,CAAC;CACF;AA5FD,8CA4FC"}
1
+ {"version":3,"file":"channel-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urugus/slack-cli",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "A command-line tool for sending messages to Slack",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -34,19 +34,6 @@ export class ChannelOperations extends BaseSlackClient {
34
34
  }
35
35
 
36
36
  async listUnreadChannels(): Promise<Channel[]> {
37
- try {
38
- // Use users.conversations to get unread counts in a single API call
39
- // This endpoint doesn't return unread_count_display by default,
40
- // so we'll use the fallback method instead
41
- throw new Error('Using fallback method for unread counts');
42
- } catch (error) {
43
- // Fallback to the old method if users.conversations fails
44
- console.warn('Failed to use users.conversations, falling back to conversations.list');
45
- return this.listUnreadChannelsFallback();
46
- }
47
- }
48
-
49
- private async listUnreadChannelsFallback(): Promise<Channel[]> {
50
37
  // Get all conversations the user is a member of
51
38
  const response = await this.client.conversations.list({
52
39
  types: 'public_channel,private_channel,im,mpim',
@@ -66,13 +53,51 @@ export class ChannelOperations extends BaseSlackClient {
66
53
  });
67
54
  const channelInfo = info.channel as ChannelWithUnreadInfo;
68
55
 
69
- if (channelInfo.unread_count_display && channelInfo.unread_count_display > 0) {
70
- channelsWithUnread.push({
71
- ...channel,
72
- unread_count: channelInfo.unread_count || 0,
73
- unread_count_display: channelInfo.unread_count_display || 0,
74
- last_read: channelInfo.last_read,
75
- });
56
+ // Get the latest message in the channel
57
+ const history = await this.client.conversations.history({
58
+ channel: channel.id,
59
+ limit: 1,
60
+ });
61
+
62
+ if (history.messages && history.messages.length > 0) {
63
+ let hasUnread = false;
64
+ let unreadCount = 0;
65
+
66
+ if (!channelInfo.last_read) {
67
+ // If last_read is empty, all messages are unread
68
+ hasUnread = true;
69
+ // Get total message count (up to 100)
70
+ const allHistory = await this.client.conversations.history({
71
+ channel: channel.id,
72
+ limit: 100,
73
+ });
74
+ unreadCount = allHistory.messages?.length || 0;
75
+ } else {
76
+ // Check if there are messages after last_read
77
+ const latestMessage = history.messages[0];
78
+ const lastReadTs = parseFloat(channelInfo.last_read!);
79
+ const latestMessageTs = parseFloat(latestMessage.ts || '0');
80
+
81
+ if (latestMessageTs > lastReadTs) {
82
+ hasUnread = true;
83
+ // Calculate unread count by fetching messages after last_read
84
+ const unreadHistory = await this.client.conversations.history({
85
+ channel: channel.id,
86
+ oldest: channelInfo.last_read,
87
+ limit: 100, // Get up to 100 unread messages
88
+ });
89
+ unreadCount = unreadHistory.messages?.length || 0;
90
+ }
91
+ }
92
+
93
+ if (hasUnread) {
94
+ channelsWithUnread.push({
95
+ ...channel,
96
+ unread_count: unreadCount,
97
+ unread_count_display: unreadCount,
98
+ last_read: channelInfo.last_read,
99
+ });
100
+ }
76
101
  }
77
102
 
78
103
  // Add delay between API calls to avoid rate limiting
@@ -0,0 +1,243 @@
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
+ // Latest message is older than last_read
176
+ mockClient.conversations.history.mockResolvedValue({
177
+ messages: [{ ts: '1234567890.000100' }],
178
+ });
179
+
180
+ const result = await channelOps.listUnreadChannels();
181
+
182
+ expect(result).toHaveLength(0);
183
+ });
184
+
185
+ it('should handle API errors gracefully', async () => {
186
+ mockClient.conversations.list.mockResolvedValue({
187
+ channels: [
188
+ { id: 'C222', name: 'error-channel' },
189
+ { id: 'C333', name: 'good-channel' },
190
+ ],
191
+ });
192
+
193
+ // First channel throws error
194
+ mockClient.conversations.info
195
+ .mockRejectedValueOnce(new Error('API Error'))
196
+ .mockResolvedValueOnce({
197
+ channel: {
198
+ id: 'C333',
199
+ name: 'good-channel',
200
+ last_read: '1234567890.000100',
201
+ },
202
+ });
203
+
204
+ mockClient.conversations.history
205
+ .mockResolvedValueOnce({
206
+ messages: [{ ts: '1234567890.000200' }],
207
+ })
208
+ .mockResolvedValueOnce({
209
+ messages: [{ ts: '1234567890.000200' }],
210
+ });
211
+
212
+ const result = await channelOps.listUnreadChannels();
213
+
214
+ // Should skip the error channel and process the good one
215
+ expect(result).toHaveLength(1);
216
+ expect(result[0].id).toBe('C333');
217
+ });
218
+
219
+ it('should handle rate limiting with delay', async () => {
220
+ const delaySpy = vi.spyOn(channelOps as any, 'delay');
221
+
222
+ mockClient.conversations.list.mockResolvedValue({
223
+ channels: [
224
+ { id: 'C444', name: 'channel1' },
225
+ { id: 'C555', name: 'channel2' },
226
+ ],
227
+ });
228
+
229
+ mockClient.conversations.info.mockResolvedValue({
230
+ channel: { id: 'C444', name: 'channel1' },
231
+ });
232
+
233
+ mockClient.conversations.history.mockResolvedValue({
234
+ messages: [],
235
+ });
236
+
237
+ await channelOps.listUnreadChannels();
238
+
239
+ // Verify delay was called between API calls
240
+ expect(delaySpy).toHaveBeenCalledWith(100);
241
+ });
242
+ });
243
+ });