@urugus/slack-cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/CHANGELOG.md +39 -0
- package/dist/commands/unread.d.ts.map +1 -1
- package/dist/commands/unread.js +6 -49
- package/dist/commands/unread.js.map +1 -1
- package/dist/utils/channel-resolver.d.ts +26 -0
- package/dist/utils/channel-resolver.d.ts.map +1 -0
- package/dist/utils/channel-resolver.js +74 -0
- package/dist/utils/channel-resolver.js.map +1 -0
- package/dist/utils/constants.d.ts +17 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +21 -1
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/formatters/channel-formatters.d.ts +16 -0
- package/dist/utils/formatters/channel-formatters.d.ts.map +1 -0
- package/dist/utils/formatters/channel-formatters.js +73 -0
- package/dist/utils/formatters/channel-formatters.js.map +1 -0
- package/dist/utils/formatters/output-formatter.d.ts +7 -0
- package/dist/utils/formatters/output-formatter.d.ts.map +1 -0
- package/dist/utils/formatters/output-formatter.js +7 -0
- package/dist/utils/formatters/output-formatter.js.map +1 -0
- package/dist/utils/profile-config.d.ts.map +1 -1
- package/dist/utils/profile-config.js +5 -3
- package/dist/utils/profile-config.js.map +1 -1
- package/dist/utils/slack-api-client.d.ts +2 -0
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +67 -96
- package/dist/utils/slack-api-client.js.map +1 -1
- package/package.json +5 -4
- package/src/commands/unread.ts +11 -52
- package/src/utils/channel-resolver.ts +86 -0
- package/src/utils/constants.ts +23 -0
- package/src/utils/formatters/channel-formatters.ts +69 -0
- package/src/utils/formatters/output-formatter.ts +7 -0
- package/src/utils/profile-config.ts +7 -5
- package/src/utils/slack-api-client.ts +81 -105
- package/tests/commands/unread.test.ts +31 -0
- package/tests/utils/channel-resolver.test.ts +157 -0
- package/tests/utils/slack-api-client.test.ts +12 -3
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.slackApiClient = exports.SlackApiClient = void 0;
|
|
4
7
|
const web_api_1 = require("@slack/web-api");
|
|
8
|
+
const p_limit_1 = __importDefault(require("p-limit"));
|
|
9
|
+
const channel_resolver_1 = require("./channel-resolver");
|
|
10
|
+
const constants_1 = require("./constants");
|
|
5
11
|
class SlackApiClient {
|
|
6
12
|
constructor(token) {
|
|
7
|
-
this.client = new web_api_1.WebClient(token
|
|
13
|
+
this.client = new web_api_1.WebClient(token, {
|
|
14
|
+
retryConfig: {
|
|
15
|
+
retries: 0, // Disable automatic retries to handle rate limits manually
|
|
16
|
+
},
|
|
17
|
+
logLevel: web_api_1.LogLevel.ERROR, // Reduce noise from WebClient logs
|
|
18
|
+
});
|
|
19
|
+
// Limit concurrent API calls to avoid rate limiting
|
|
20
|
+
this.rateLimiter = (0, p_limit_1.default)(constants_1.RATE_LIMIT.CONCURRENT_REQUESTS);
|
|
8
21
|
}
|
|
9
22
|
async sendMessage(channel, text) {
|
|
10
23
|
return await this.client.chat.postMessage({
|
|
@@ -31,46 +44,12 @@ class SlackApiClient {
|
|
|
31
44
|
return channels;
|
|
32
45
|
}
|
|
33
46
|
async getHistory(channel, options) {
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
exclude_archived: true,
|
|
41
|
-
limit: 1000,
|
|
42
|
-
});
|
|
43
|
-
// Try multiple matching strategies
|
|
44
|
-
const foundChannel = channels.find((c) => {
|
|
45
|
-
// Direct name match
|
|
46
|
-
if (c.name === channel)
|
|
47
|
-
return true;
|
|
48
|
-
// Match without # prefix
|
|
49
|
-
if (c.name === channel.replace('#', ''))
|
|
50
|
-
return true;
|
|
51
|
-
// Case-insensitive match
|
|
52
|
-
if (c.name?.toLowerCase() === channel.toLowerCase())
|
|
53
|
-
return true;
|
|
54
|
-
// Match with normalized name
|
|
55
|
-
if (c.name_normalized === channel)
|
|
56
|
-
return true;
|
|
57
|
-
return false;
|
|
58
|
-
});
|
|
59
|
-
if (!foundChannel) {
|
|
60
|
-
// Provide helpful error message
|
|
61
|
-
const similarChannels = channels
|
|
62
|
-
.filter((c) => c.name?.toLowerCase().includes(channel.toLowerCase()))
|
|
63
|
-
.slice(0, 5)
|
|
64
|
-
.map((c) => c.name);
|
|
65
|
-
if (similarChannels.length > 0) {
|
|
66
|
-
throw new Error(`Channel '${channel}' not found. Did you mean one of these? ${similarChannels.join(', ')}`);
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
throw new Error(`Channel '${channel}' not found. Make sure you are a member of this channel.`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
channelId = foundChannel.id;
|
|
73
|
-
}
|
|
47
|
+
// Resolve channel name to ID if needed
|
|
48
|
+
const channelId = await channel_resolver_1.channelResolver.resolveChannelId(channel, () => this.listChannels({
|
|
49
|
+
types: 'public_channel,private_channel,im,mpim',
|
|
50
|
+
exclude_archived: true,
|
|
51
|
+
limit: constants_1.DEFAULTS.CHANNELS_LIMIT,
|
|
52
|
+
}));
|
|
74
53
|
const response = await this.client.conversations.history({
|
|
75
54
|
channel: channelId,
|
|
76
55
|
limit: options.limit,
|
|
@@ -98,6 +77,26 @@ class SlackApiClient {
|
|
|
98
77
|
return { messages, users };
|
|
99
78
|
}
|
|
100
79
|
async listUnreadChannels() {
|
|
80
|
+
try {
|
|
81
|
+
// Use users.conversations to get unread counts in a single API call
|
|
82
|
+
const response = await this.client.users.conversations({
|
|
83
|
+
types: 'public_channel,private_channel,im,mpim',
|
|
84
|
+
exclude_archived: true,
|
|
85
|
+
limit: 1000,
|
|
86
|
+
user: undefined, // Current authenticated user
|
|
87
|
+
});
|
|
88
|
+
const channels = response.channels;
|
|
89
|
+
// Filter to only channels with unread messages
|
|
90
|
+
// The users.conversations endpoint includes unread_count_display
|
|
91
|
+
return channels.filter((channel) => (channel.unread_count_display || 0) > 0);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Fallback to the old method if users.conversations fails
|
|
95
|
+
console.warn('Failed to use users.conversations, falling back to conversations.list');
|
|
96
|
+
return this.listUnreadChannelsFallback();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async listUnreadChannelsFallback() {
|
|
101
100
|
// Get all conversations the user is a member of
|
|
102
101
|
const response = await this.client.conversations.list({
|
|
103
102
|
types: 'public_channel,private_channel,im,mpim',
|
|
@@ -105,71 +104,43 @@ class SlackApiClient {
|
|
|
105
104
|
limit: 1000,
|
|
106
105
|
});
|
|
107
106
|
const channels = response.channels;
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
const channelsWithUnread = [];
|
|
108
|
+
// Process channels one by one with delay to avoid rate limits
|
|
109
|
+
for (const channel of channels) {
|
|
110
110
|
try {
|
|
111
111
|
const info = await this.client.conversations.info({
|
|
112
112
|
channel: channel.id,
|
|
113
113
|
include_num_members: false,
|
|
114
114
|
});
|
|
115
115
|
const channelInfo = info.channel;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
catch {
|
|
124
|
-
return channel;
|
|
125
|
-
}
|
|
126
|
-
}));
|
|
127
|
-
// Filter to only channels with unread messages
|
|
128
|
-
return channelsWithUnread.filter((channel) => (channel.unread_count_display || 0) > 0);
|
|
129
|
-
}
|
|
130
|
-
async getChannelUnread(channelNameOrId) {
|
|
131
|
-
// First, find the channel
|
|
132
|
-
let channelId = channelNameOrId;
|
|
133
|
-
if (!channelNameOrId.startsWith('C') &&
|
|
134
|
-
!channelNameOrId.startsWith('D') &&
|
|
135
|
-
!channelNameOrId.startsWith('G')) {
|
|
136
|
-
// It's a name, not an ID - need to find the ID
|
|
137
|
-
const channels = await this.listChannels({
|
|
138
|
-
types: 'public_channel,private_channel,im,mpim',
|
|
139
|
-
exclude_archived: true,
|
|
140
|
-
limit: 1000,
|
|
141
|
-
});
|
|
142
|
-
// Try multiple matching strategies (same as getHistory)
|
|
143
|
-
const channel = channels.find((c) => {
|
|
144
|
-
// Direct name match
|
|
145
|
-
if (c.name === channelNameOrId)
|
|
146
|
-
return true;
|
|
147
|
-
// Match without # prefix
|
|
148
|
-
if (c.name === channelNameOrId.replace('#', ''))
|
|
149
|
-
return true;
|
|
150
|
-
// Case-insensitive match
|
|
151
|
-
if (c.name?.toLowerCase() === channelNameOrId.toLowerCase())
|
|
152
|
-
return true;
|
|
153
|
-
// Match with normalized name
|
|
154
|
-
if (c.name_normalized === channelNameOrId)
|
|
155
|
-
return true;
|
|
156
|
-
return false;
|
|
157
|
-
});
|
|
158
|
-
if (!channel) {
|
|
159
|
-
// Provide helpful error message
|
|
160
|
-
const similarChannels = channels
|
|
161
|
-
.filter((c) => c.name?.toLowerCase().includes(channelNameOrId.toLowerCase()))
|
|
162
|
-
.slice(0, 5)
|
|
163
|
-
.map((c) => c.name);
|
|
164
|
-
if (similarChannels.length > 0) {
|
|
165
|
-
throw new Error(`Channel '${channelNameOrId}' not found. Did you mean one of these? ${similarChannels.join(', ')}`);
|
|
116
|
+
if (channelInfo.unread_count_display && channelInfo.unread_count_display > 0) {
|
|
117
|
+
channelsWithUnread.push({
|
|
118
|
+
...channel,
|
|
119
|
+
unread_count: channelInfo.unread_count || 0,
|
|
120
|
+
unread_count_display: channelInfo.unread_count_display || 0,
|
|
121
|
+
last_read: channelInfo.last_read,
|
|
122
|
+
});
|
|
166
123
|
}
|
|
167
|
-
|
|
168
|
-
|
|
124
|
+
// Add delay between API calls to avoid rate limiting
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
// Skip channels that fail
|
|
129
|
+
if (error instanceof Error && error.message?.includes('rate limit')) {
|
|
130
|
+
// If we hit rate limit, wait longer
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
169
132
|
}
|
|
170
133
|
}
|
|
171
|
-
channelId = channel.id;
|
|
172
134
|
}
|
|
135
|
+
return channelsWithUnread;
|
|
136
|
+
}
|
|
137
|
+
async getChannelUnread(channelNameOrId) {
|
|
138
|
+
// Resolve channel name to ID if needed
|
|
139
|
+
const channelId = await channel_resolver_1.channelResolver.resolveChannelId(channelNameOrId, () => this.listChannels({
|
|
140
|
+
types: 'public_channel,private_channel,im,mpim',
|
|
141
|
+
exclude_archived: true,
|
|
142
|
+
limit: constants_1.DEFAULTS.CHANNELS_LIMIT,
|
|
143
|
+
}));
|
|
173
144
|
// Get channel info with unread count
|
|
174
145
|
const info = await this.client.conversations.info({
|
|
175
146
|
channel: channelId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-api-client.js","sourceRoot":"","sources":["../../src/utils/slack-api-client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"slack-api-client.js","sourceRoot":"","sources":["../../src/utils/slack-api-client.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.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "A command-line tool for sending messages to Slack",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -32,14 +32,15 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@slack/web-api": "^6.9.0",
|
|
35
|
+
"chalk": "^4.1.2",
|
|
35
36
|
"commander": "^11.1.0",
|
|
36
37
|
"dotenv": "^16.3.1",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
38
|
+
"inquirer": "^8.2.6",
|
|
39
|
+
"p-limit": "^3.1.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^20.10.0",
|
|
42
42
|
"@types/inquirer": "^8.2.10",
|
|
43
|
+
"@types/node": "^20.10.0",
|
|
43
44
|
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
|
44
45
|
"@typescript-eslint/parser": "^6.13.0",
|
|
45
46
|
"@vitest/coverage-v8": "^1.0.4",
|
package/src/commands/unread.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { wrapCommand } from '../utils/command-wrapper';
|
|
3
3
|
import { createSlackClient } from '../utils/client-factory';
|
|
4
|
-
import { SlackApiClient
|
|
4
|
+
import { SlackApiClient } from '../utils/slack-api-client';
|
|
5
5
|
import { UnreadOptions } from '../types/commands';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { formatSlackTimestamp } from '../utils/date-utils';
|
|
8
8
|
import { formatChannelName } from '../utils/channel-formatter';
|
|
9
|
+
import { createChannelFormatter } from '../utils/formatters/channel-formatters';
|
|
10
|
+
import { DEFAULTS } from '../utils/constants';
|
|
9
11
|
|
|
10
12
|
async function handleSpecificChannelUnread(
|
|
11
13
|
client: SlackApiClient,
|
|
@@ -40,58 +42,11 @@ async function handleAllChannelsUnread(
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
// Apply limit
|
|
43
|
-
const limit = parseInt(options.limit ||
|
|
45
|
+
const limit = parseInt(options.limit || DEFAULTS.UNREAD_DISPLAY_LIMIT.toString(), 10);
|
|
44
46
|
const displayChannels = channels.slice(0, limit);
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} else if (options.format === 'json') {
|
|
49
|
-
displayAsJson(displayChannels);
|
|
50
|
-
} else if (options.format === 'simple') {
|
|
51
|
-
displayAsSimple(displayChannels);
|
|
52
|
-
} else {
|
|
53
|
-
displayAsTable(displayChannels);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function displayCountOnly(channels: Channel[]): void {
|
|
58
|
-
let totalUnread = 0;
|
|
59
|
-
channels.forEach((channel) => {
|
|
60
|
-
const count = channel.unread_count || 0;
|
|
61
|
-
totalUnread += count;
|
|
62
|
-
const channelName = formatChannelName(channel.name);
|
|
63
|
-
console.log(`${channelName}: ${count}`);
|
|
64
|
-
});
|
|
65
|
-
console.log(chalk.bold(`Total: ${totalUnread} unread messages`));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function displayAsJson(channels: Channel[]): void {
|
|
69
|
-
const output = channels.map((channel) => ({
|
|
70
|
-
channel: formatChannelName(channel.name),
|
|
71
|
-
channelId: channel.id,
|
|
72
|
-
unreadCount: channel.unread_count || 0,
|
|
73
|
-
}));
|
|
74
|
-
console.log(JSON.stringify(output, null, 2));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function displayAsSimple(channels: Channel[]): void {
|
|
78
|
-
channels.forEach((channel) => {
|
|
79
|
-
const channelName = formatChannelName(channel.name);
|
|
80
|
-
console.log(`${channelName} (${channel.unread_count || 0})`);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function displayAsTable(channels: Channel[]): void {
|
|
85
|
-
console.log(chalk.bold('Channel Unread Last Message'));
|
|
86
|
-
console.log('─'.repeat(50));
|
|
87
|
-
|
|
88
|
-
channels.forEach((channel) => {
|
|
89
|
-
const channelName = formatChannelName(channel.name);
|
|
90
|
-
const paddedName = channelName.padEnd(16);
|
|
91
|
-
const count = (channel.unread_count || 0).toString().padEnd(6);
|
|
92
|
-
const lastRead = channel.last_read ? formatSlackTimestamp(channel.last_read) : 'Unknown';
|
|
93
|
-
console.log(`${paddedName} ${count} ${lastRead}`);
|
|
94
|
-
});
|
|
48
|
+
const formatter = createChannelFormatter(options.format || 'table', options.countOnly || false);
|
|
49
|
+
formatter.format(displayChannels);
|
|
95
50
|
}
|
|
96
51
|
|
|
97
52
|
export function setupUnreadCommand(): Command {
|
|
@@ -100,7 +55,11 @@ export function setupUnreadCommand(): Command {
|
|
|
100
55
|
.option('-c, --channel <channel>', 'Show unread for a specific channel')
|
|
101
56
|
.option('--format <format>', 'Output format: table, simple, json', 'table')
|
|
102
57
|
.option('--count-only', 'Show only unread counts', false)
|
|
103
|
-
.option(
|
|
58
|
+
.option(
|
|
59
|
+
'--limit <number>',
|
|
60
|
+
'Maximum number of channels to display',
|
|
61
|
+
DEFAULTS.UNREAD_DISPLAY_LIMIT.toString()
|
|
62
|
+
)
|
|
104
63
|
.option('--profile <profile>', 'Use specific workspace profile')
|
|
105
64
|
.action(
|
|
106
65
|
wrapCommand(async (options: UnreadOptions) => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Channel } from './slack-api-client';
|
|
2
|
+
|
|
3
|
+
export type GetChannelsFunction = () => Promise<Channel[]>;
|
|
4
|
+
|
|
5
|
+
export class ChannelResolver {
|
|
6
|
+
/**
|
|
7
|
+
* Check if the given string is a channel ID
|
|
8
|
+
*/
|
|
9
|
+
isChannelId(channelNameOrId: string): boolean {
|
|
10
|
+
return (
|
|
11
|
+
channelNameOrId.startsWith('C') ||
|
|
12
|
+
channelNameOrId.startsWith('D') ||
|
|
13
|
+
channelNameOrId.startsWith('G')
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find a channel by name from the given list
|
|
19
|
+
*/
|
|
20
|
+
findChannel(channelName: string, channels: Channel[]): Channel | undefined {
|
|
21
|
+
return channels.find((c) => {
|
|
22
|
+
// Direct name match
|
|
23
|
+
if (c.name === channelName) return true;
|
|
24
|
+
// Match without # prefix
|
|
25
|
+
if (c.name === channelName.replace('#', '')) return true;
|
|
26
|
+
// Case-insensitive match
|
|
27
|
+
if (c.name?.toLowerCase() === channelName.toLowerCase()) return true;
|
|
28
|
+
// Match with normalized name
|
|
29
|
+
if (c.name_normalized === channelName) return true;
|
|
30
|
+
return false;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get similar channel names for suggestions
|
|
36
|
+
*/
|
|
37
|
+
getSimilarChannels(channelName: string, channels: Channel[], limit = 5): string[] {
|
|
38
|
+
return channels
|
|
39
|
+
.filter((c) => c.name?.toLowerCase().includes(channelName.toLowerCase()))
|
|
40
|
+
.slice(0, limit)
|
|
41
|
+
.map((c) => c.name as string);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create an error with channel suggestions
|
|
46
|
+
*/
|
|
47
|
+
resolveChannelError(channelName: string, channels: Channel[]): Error {
|
|
48
|
+
const similarChannels = this.getSimilarChannels(channelName, channels);
|
|
49
|
+
|
|
50
|
+
if (similarChannels.length > 0) {
|
|
51
|
+
return new Error(
|
|
52
|
+
`Channel '${channelName}' not found. Did you mean one of these? ${similarChannels.join(', ')}`
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
return new Error(
|
|
56
|
+
`Channel '${channelName}' not found. Make sure you are a member of this channel.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a channel name or ID to a channel ID
|
|
63
|
+
*/
|
|
64
|
+
async resolveChannelId(
|
|
65
|
+
channelNameOrId: string,
|
|
66
|
+
getChannels: GetChannelsFunction
|
|
67
|
+
): Promise<string> {
|
|
68
|
+
// If it's already an ID, return it
|
|
69
|
+
if (this.isChannelId(channelNameOrId)) {
|
|
70
|
+
return channelNameOrId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Otherwise, fetch channels and resolve the name
|
|
74
|
+
const channels = await getChannels();
|
|
75
|
+
const channel = this.findChannel(channelNameOrId, channels);
|
|
76
|
+
|
|
77
|
+
if (!channel) {
|
|
78
|
+
throw this.resolveChannelError(channelNameOrId, channels);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return channel.id;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Export a singleton instance
|
|
86
|
+
export const channelResolver = new ChannelResolver();
|
package/src/utils/constants.ts
CHANGED
|
@@ -45,3 +45,26 @@ export const API_LIMITS = {
|
|
|
45
45
|
MIN_MESSAGE_COUNT: 1,
|
|
46
46
|
DEFAULT_MESSAGE_COUNT: 10,
|
|
47
47
|
};
|
|
48
|
+
|
|
49
|
+
// API Rate Limiting Configuration
|
|
50
|
+
export const RATE_LIMIT = {
|
|
51
|
+
CONCURRENT_REQUESTS: 3,
|
|
52
|
+
BATCH_SIZE: 10,
|
|
53
|
+
BATCH_DELAY_MS: 1000,
|
|
54
|
+
RETRY_CONFIG: {
|
|
55
|
+
retries: 3,
|
|
56
|
+
factor: 2,
|
|
57
|
+
minTimeout: 1000,
|
|
58
|
+
maxTimeout: 30000,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Default values
|
|
63
|
+
export const DEFAULTS = {
|
|
64
|
+
HISTORY_LIMIT: 20,
|
|
65
|
+
CHANNELS_LIMIT: 1000,
|
|
66
|
+
UNREAD_DISPLAY_LIMIT: 50,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Time formats
|
|
70
|
+
export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { BaseFormatter } from './output-formatter';
|
|
3
|
+
import { Channel } from '../slack-api-client';
|
|
4
|
+
import { formatChannelName } from '../channel-formatter';
|
|
5
|
+
import { formatSlackTimestamp } from '../date-utils';
|
|
6
|
+
|
|
7
|
+
export class ChannelTableFormatter extends BaseFormatter<Channel> {
|
|
8
|
+
format(channels: Channel[]): void {
|
|
9
|
+
console.log(chalk.bold('Channel Unread Last Message'));
|
|
10
|
+
console.log('─'.repeat(50));
|
|
11
|
+
|
|
12
|
+
channels.forEach((channel) => {
|
|
13
|
+
const channelName = formatChannelName(channel.name);
|
|
14
|
+
const paddedName = channelName.padEnd(16);
|
|
15
|
+
const count = (channel.unread_count || 0).toString().padEnd(6);
|
|
16
|
+
const lastRead = channel.last_read ? formatSlackTimestamp(channel.last_read) : 'Unknown';
|
|
17
|
+
console.log(`${paddedName} ${count} ${lastRead}`);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ChannelSimpleFormatter extends BaseFormatter<Channel> {
|
|
23
|
+
format(channels: Channel[]): void {
|
|
24
|
+
channels.forEach((channel) => {
|
|
25
|
+
const channelName = formatChannelName(channel.name);
|
|
26
|
+
console.log(`${channelName} (${channel.unread_count || 0})`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ChannelJsonFormatter extends BaseFormatter<Channel> {
|
|
32
|
+
format(channels: Channel[]): void {
|
|
33
|
+
const output = channels.map((channel) => ({
|
|
34
|
+
channel: formatChannelName(channel.name),
|
|
35
|
+
channelId: channel.id,
|
|
36
|
+
unreadCount: channel.unread_count || 0,
|
|
37
|
+
}));
|
|
38
|
+
console.log(JSON.stringify(output, null, 2));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class ChannelCountFormatter extends BaseFormatter<Channel> {
|
|
43
|
+
format(channels: Channel[]): void {
|
|
44
|
+
let totalUnread = 0;
|
|
45
|
+
channels.forEach((channel) => {
|
|
46
|
+
const count = channel.unread_count || 0;
|
|
47
|
+
totalUnread += count;
|
|
48
|
+
const channelName = formatChannelName(channel.name);
|
|
49
|
+
console.log(`${channelName}: ${count}`);
|
|
50
|
+
});
|
|
51
|
+
console.log(chalk.bold(`Total: ${totalUnread} unread messages`));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createChannelFormatter(format: string, countOnly: boolean): BaseFormatter<Channel> {
|
|
56
|
+
if (countOnly) {
|
|
57
|
+
return new ChannelCountFormatter();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
switch (format) {
|
|
61
|
+
case 'json':
|
|
62
|
+
return new ChannelJsonFormatter();
|
|
63
|
+
case 'simple':
|
|
64
|
+
return new ChannelSimpleFormatter();
|
|
65
|
+
case 'table':
|
|
66
|
+
default:
|
|
67
|
+
return new ChannelTableFormatter();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -130,14 +130,16 @@ export class ProfileConfigManager {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
private needsMigration(data:
|
|
134
|
-
|
|
133
|
+
private needsMigration(data: unknown): boolean {
|
|
134
|
+
const configData = data as Record<string, unknown>;
|
|
135
|
+
return Boolean(configData.token && !configData.profiles);
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
private async migrateOldConfig(oldData:
|
|
138
|
+
private async migrateOldConfig(oldData: unknown): Promise<ConfigStore> {
|
|
139
|
+
const data = oldData as { token: string; updatedAt: string };
|
|
138
140
|
const oldConfig: Config = {
|
|
139
|
-
token:
|
|
140
|
-
updatedAt:
|
|
141
|
+
token: data.token,
|
|
142
|
+
updatedAt: data.updatedAt,
|
|
141
143
|
};
|
|
142
144
|
|
|
143
145
|
const newStore: ConfigStore = {
|