@urugus/slack-cli 0.1.4 → 0.1.6
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 +31 -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/index.js +6 -1
- package/dist/index.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 +1 -0
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +59 -101
- package/dist/utils/slack-api-client.js.map +1 -1
- package/package.json +5 -4
- package/src/commands/unread.ts +11 -52
- package/src/index.ts +7 -1
- 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 +73 -106
- package/tests/commands/unread.test.ts +31 -0
- package/tests/index.test.ts +40 -0
- package/tests/utils/channel-resolver.test.ts +157 -0
- package/tests/utils/slack-api-client.test.ts +8 -1
|
@@ -1,10 +1,20 @@
|
|
|
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: constants_1.RATE_LIMIT.RETRY_CONFIG,
|
|
15
|
+
});
|
|
16
|
+
// Limit concurrent API calls to avoid rate limiting
|
|
17
|
+
this.rateLimiter = (0, p_limit_1.default)(constants_1.RATE_LIMIT.CONCURRENT_REQUESTS);
|
|
8
18
|
}
|
|
9
19
|
async sendMessage(channel, text) {
|
|
10
20
|
return await this.client.chat.postMessage({
|
|
@@ -31,46 +41,12 @@ class SlackApiClient {
|
|
|
31
41
|
return channels;
|
|
32
42
|
}
|
|
33
43
|
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
|
-
}
|
|
44
|
+
// Resolve channel name to ID if needed
|
|
45
|
+
const channelId = await channel_resolver_1.channelResolver.resolveChannelId(channel, () => this.listChannels({
|
|
46
|
+
types: 'public_channel,private_channel,im,mpim',
|
|
47
|
+
exclude_archived: true,
|
|
48
|
+
limit: constants_1.DEFAULTS.CHANNELS_LIMIT,
|
|
49
|
+
}));
|
|
74
50
|
const response = await this.client.conversations.history({
|
|
75
51
|
channel: channelId,
|
|
76
52
|
limit: options.limit,
|
|
@@ -105,71 +81,53 @@ class SlackApiClient {
|
|
|
105
81
|
limit: 1000,
|
|
106
82
|
});
|
|
107
83
|
const channels = response.channels;
|
|
108
|
-
//
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
84
|
+
// Batch process channels to reduce rate limit issues
|
|
85
|
+
const batchSize = constants_1.RATE_LIMIT.BATCH_SIZE;
|
|
86
|
+
const batches = [];
|
|
87
|
+
for (let i = 0; i < channels.length; i += batchSize) {
|
|
88
|
+
batches.push(channels.slice(i, i + batchSize));
|
|
89
|
+
}
|
|
90
|
+
const channelsWithUnread = [];
|
|
91
|
+
// Process batches sequentially with delay
|
|
92
|
+
for (const batch of batches) {
|
|
93
|
+
const batchResults = await Promise.all(batch.map((channel) => this.rateLimiter(async () => {
|
|
94
|
+
try {
|
|
95
|
+
const info = await this.client.conversations.info({
|
|
96
|
+
channel: channel.id,
|
|
97
|
+
include_num_members: false,
|
|
98
|
+
});
|
|
99
|
+
const channelInfo = info.channel;
|
|
100
|
+
return {
|
|
101
|
+
...channel,
|
|
102
|
+
unread_count: channelInfo.unread_count || 0,
|
|
103
|
+
unread_count_display: channelInfo.unread_count_display || 0,
|
|
104
|
+
last_read: channelInfo.last_read,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// Log rate limit errors but continue processing
|
|
109
|
+
if (error instanceof Error && error.message?.includes('rate limit')) {
|
|
110
|
+
console.warn(`Rate limit hit for channel ${channel.name}, skipping...`);
|
|
111
|
+
}
|
|
112
|
+
return channel;
|
|
113
|
+
}
|
|
114
|
+
})));
|
|
115
|
+
channelsWithUnread.push(...batchResults);
|
|
116
|
+
// Add delay between batches to avoid rate limiting
|
|
117
|
+
if (batches.indexOf(batch) < batches.length - 1) {
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, constants_1.RATE_LIMIT.BATCH_DELAY_MS));
|
|
125
119
|
}
|
|
126
|
-
}
|
|
120
|
+
}
|
|
127
121
|
// Filter to only channels with unread messages
|
|
128
122
|
return channelsWithUnread.filter((channel) => (channel.unread_count_display || 0) > 0);
|
|
129
123
|
}
|
|
130
124
|
async getChannelUnread(channelNameOrId) {
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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(', ')}`);
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
throw new Error(`Channel '${channelNameOrId}' not found. Make sure you are a member of this channel.`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
channelId = channel.id;
|
|
172
|
-
}
|
|
125
|
+
// Resolve channel name to ID if needed
|
|
126
|
+
const channelId = await channel_resolver_1.channelResolver.resolveChannelId(channelNameOrId, () => this.listChannels({
|
|
127
|
+
types: 'public_channel,private_channel,im,mpim',
|
|
128
|
+
exclude_archived: true,
|
|
129
|
+
limit: constants_1.DEFAULTS.CHANNELS_LIMIT,
|
|
130
|
+
}));
|
|
173
131
|
// Get channel info with unread count
|
|
174
132
|
const info = await this.client.conversations.info({
|
|
175
133
|
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.6",
|
|
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) => {
|
package/src/index.ts
CHANGED
|
@@ -5,10 +5,16 @@ import { setupSendCommand } from './commands/send';
|
|
|
5
5
|
import { setupChannelsCommand } from './commands/channels';
|
|
6
6
|
import { setupHistoryCommand } from './commands/history';
|
|
7
7
|
import { setupUnreadCommand } from './commands/unread';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
8
10
|
|
|
9
11
|
const program = new Command();
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
// Read version from package.json
|
|
14
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
15
|
+
const version = packageJson.version;
|
|
16
|
+
|
|
17
|
+
program.name('slack-cli').description('CLI tool to send messages via Slack API').version(version);
|
|
12
18
|
|
|
13
19
|
program.addCommand(setupConfigCommand());
|
|
14
20
|
program.addCommand(setupSendCommand());
|
|
@@ -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 = {
|