@urugus/slack-cli 0.1.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.
Files changed (116) hide show
  1. package/.claude/settings.local.json +53 -0
  2. package/.eslintrc.json +25 -0
  3. package/.github/dependabot.yml +18 -0
  4. package/.github/workflows/ci.yml +70 -0
  5. package/.github/workflows/pr-validation.yml +51 -0
  6. package/.prettierignore +11 -0
  7. package/.prettierrc +10 -0
  8. package/CLAUDE.md +16 -0
  9. package/README.md +161 -0
  10. package/dist/commands/channels.d.ts +3 -0
  11. package/dist/commands/channels.d.ts.map +1 -0
  12. package/dist/commands/channels.js +50 -0
  13. package/dist/commands/channels.js.map +1 -0
  14. package/dist/commands/config.d.ts +3 -0
  15. package/dist/commands/config.d.ts.map +1 -0
  16. package/dist/commands/config.js +87 -0
  17. package/dist/commands/config.js.map +1 -0
  18. package/dist/commands/history.d.ts +3 -0
  19. package/dist/commands/history.d.ts.map +1 -0
  20. package/dist/commands/history.js +79 -0
  21. package/dist/commands/history.js.map +1 -0
  22. package/dist/commands/send.d.ts +3 -0
  23. package/dist/commands/send.d.ts.map +1 -0
  24. package/dist/commands/send.js +85 -0
  25. package/dist/commands/send.js.map +1 -0
  26. package/dist/commands/unread.d.ts +3 -0
  27. package/dist/commands/unread.d.ts.map +1 -0
  28. package/dist/commands/unread.js +104 -0
  29. package/dist/commands/unread.js.map +1 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +18 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/types/commands.d.ts +40 -0
  35. package/dist/types/commands.d.ts.map +1 -0
  36. package/dist/types/commands.js +3 -0
  37. package/dist/types/commands.js.map +1 -0
  38. package/dist/types/config.d.ts +18 -0
  39. package/dist/types/config.d.ts.map +1 -0
  40. package/dist/types/config.js +3 -0
  41. package/dist/types/config.js.map +1 -0
  42. package/dist/utils/channel-formatter.d.ts +16 -0
  43. package/dist/utils/channel-formatter.d.ts.map +1 -0
  44. package/dist/utils/channel-formatter.js +77 -0
  45. package/dist/utils/channel-formatter.js.map +1 -0
  46. package/dist/utils/client-factory.d.ts +6 -0
  47. package/dist/utils/client-factory.d.ts.map +1 -0
  48. package/dist/utils/client-factory.js +13 -0
  49. package/dist/utils/client-factory.js.map +1 -0
  50. package/dist/utils/command-wrapper.d.ts +6 -0
  51. package/dist/utils/command-wrapper.d.ts.map +1 -0
  52. package/dist/utils/command-wrapper.js +27 -0
  53. package/dist/utils/command-wrapper.js.map +1 -0
  54. package/dist/utils/config-helper.d.ts +8 -0
  55. package/dist/utils/config-helper.d.ts.map +1 -0
  56. package/dist/utils/config-helper.js +19 -0
  57. package/dist/utils/config-helper.js.map +1 -0
  58. package/dist/utils/config.d.ts +10 -0
  59. package/dist/utils/config.d.ts.map +1 -0
  60. package/dist/utils/config.js +94 -0
  61. package/dist/utils/config.js.map +1 -0
  62. package/dist/utils/constants.d.ts +32 -0
  63. package/dist/utils/constants.d.ts.map +1 -0
  64. package/dist/utils/constants.js +42 -0
  65. package/dist/utils/constants.js.map +1 -0
  66. package/dist/utils/date-utils.d.ts +3 -0
  67. package/dist/utils/date-utils.d.ts.map +1 -0
  68. package/dist/utils/date-utils.js +12 -0
  69. package/dist/utils/date-utils.js.map +1 -0
  70. package/dist/utils/error-utils.d.ts +2 -0
  71. package/dist/utils/error-utils.d.ts.map +1 -0
  72. package/dist/utils/error-utils.js +10 -0
  73. package/dist/utils/error-utils.js.map +1 -0
  74. package/dist/utils/errors.d.ts +17 -0
  75. package/dist/utils/errors.d.ts.map +1 -0
  76. package/dist/utils/errors.js +40 -0
  77. package/dist/utils/errors.js.map +1 -0
  78. package/dist/utils/profile-config.d.ts +21 -0
  79. package/dist/utils/profile-config.d.ts.map +1 -0
  80. package/dist/utils/profile-config.js +173 -0
  81. package/dist/utils/profile-config.js.map +1 -0
  82. package/dist/utils/slack-api-client.d.ts +74 -0
  83. package/dist/utils/slack-api-client.d.ts.map +1 -0
  84. package/dist/utils/slack-api-client.js +132 -0
  85. package/dist/utils/slack-api-client.js.map +1 -0
  86. package/package.json +56 -0
  87. package/src/commands/channels.ts +65 -0
  88. package/src/commands/config.ts +104 -0
  89. package/src/commands/history.ts +96 -0
  90. package/src/commands/send.ts +52 -0
  91. package/src/commands/unread.ts +118 -0
  92. package/src/index.ts +19 -0
  93. package/src/types/commands.ts +46 -0
  94. package/src/types/config.ts +20 -0
  95. package/src/utils/channel-formatter.ts +89 -0
  96. package/src/utils/client-factory.ts +10 -0
  97. package/src/utils/command-wrapper.ts +27 -0
  98. package/src/utils/config-helper.ts +21 -0
  99. package/src/utils/constants.ts +47 -0
  100. package/src/utils/date-utils.ts +8 -0
  101. package/src/utils/error-utils.ts +6 -0
  102. package/src/utils/errors.ts +37 -0
  103. package/src/utils/profile-config.ts +171 -0
  104. package/src/utils/slack-api-client.ts +218 -0
  105. package/tests/commands/channels.test.ts +250 -0
  106. package/tests/commands/config.test.ts +158 -0
  107. package/tests/commands/history.test.ts +250 -0
  108. package/tests/commands/send.test.ts +156 -0
  109. package/tests/commands/unread.test.ts +248 -0
  110. package/tests/test-utils.ts +28 -0
  111. package/tests/utils/config.test.ts +400 -0
  112. package/tests/utils/date-utils.test.ts +30 -0
  113. package/tests/utils/error-utils.test.ts +34 -0
  114. package/tests/utils/slack-api-client.test.ts +170 -0
  115. package/tsconfig.json +22 -0
  116. package/vitest.config.ts +27 -0
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.slackApiClient = exports.SlackApiClient = void 0;
4
+ const web_api_1 = require("@slack/web-api");
5
+ class SlackApiClient {
6
+ constructor(token) {
7
+ this.client = new web_api_1.WebClient(token);
8
+ }
9
+ async sendMessage(channel, text) {
10
+ return await this.client.chat.postMessage({
11
+ channel,
12
+ text,
13
+ });
14
+ }
15
+ async listChannels(options) {
16
+ const response = await this.client.conversations.list({
17
+ types: options.types,
18
+ exclude_archived: options.exclude_archived,
19
+ limit: options.limit,
20
+ });
21
+ return response.channels;
22
+ }
23
+ async getHistory(channel, options) {
24
+ const response = await this.client.conversations.history({
25
+ channel,
26
+ limit: options.limit,
27
+ oldest: options.oldest,
28
+ });
29
+ const messages = response.messages;
30
+ // Get unique user IDs
31
+ const userIds = [...new Set(messages.filter((m) => m.user).map((m) => m.user))];
32
+ const users = new Map();
33
+ // Fetch user information
34
+ if (userIds.length > 0) {
35
+ for (const userId of userIds) {
36
+ try {
37
+ const userInfo = await this.client.users.info({ user: userId });
38
+ if (userInfo.user?.name) {
39
+ users.set(userId, userInfo.user.name);
40
+ }
41
+ }
42
+ catch (error) {
43
+ // If we can't get user info, we'll use the ID
44
+ users.set(userId, userId);
45
+ }
46
+ }
47
+ }
48
+ return { messages, users };
49
+ }
50
+ async listUnreadChannels() {
51
+ // Get all conversations the user is a member of
52
+ const response = await this.client.conversations.list({
53
+ types: 'public_channel,private_channel,im,mpim',
54
+ exclude_archived: true,
55
+ limit: 1000,
56
+ });
57
+ const channels = response.channels;
58
+ // Get unread count for each channel
59
+ const channelsWithUnread = await Promise.all(channels.map(async (channel) => {
60
+ try {
61
+ const info = await this.client.conversations.info({
62
+ channel: channel.id,
63
+ include_num_members: false,
64
+ });
65
+ const channelInfo = info.channel;
66
+ return {
67
+ ...channel,
68
+ unread_count: channelInfo.unread_count || 0,
69
+ unread_count_display: channelInfo.unread_count_display || 0,
70
+ last_read: channelInfo.last_read,
71
+ };
72
+ }
73
+ catch {
74
+ return channel;
75
+ }
76
+ }));
77
+ // Filter to only channels with unread messages
78
+ return channelsWithUnread.filter((channel) => (channel.unread_count_display || 0) > 0);
79
+ }
80
+ async getChannelUnread(channelNameOrId) {
81
+ // First, find the channel
82
+ let channelId = channelNameOrId;
83
+ if (!channelNameOrId.startsWith('C') &&
84
+ !channelNameOrId.startsWith('D') &&
85
+ !channelNameOrId.startsWith('G')) {
86
+ // It's a name, not an ID - need to find the ID
87
+ const channels = await this.listChannels({
88
+ types: 'public_channel,private_channel,im,mpim',
89
+ exclude_archived: true,
90
+ limit: 1000,
91
+ });
92
+ const channel = channels.find((c) => c.name === channelNameOrId || c.name === channelNameOrId.replace('#', ''));
93
+ if (!channel) {
94
+ throw new Error('channel_not_found');
95
+ }
96
+ channelId = channel.id;
97
+ }
98
+ // Get channel info with unread count
99
+ const info = await this.client.conversations.info({
100
+ channel: channelId,
101
+ });
102
+ const channel = info.channel;
103
+ // Get unread messages
104
+ let messages = [];
105
+ let users = new Map();
106
+ if (channel.last_read && channel.unread_count > 0) {
107
+ const historyResult = await this.getHistory(channelId, {
108
+ limit: channel.unread_count,
109
+ oldest: channel.last_read,
110
+ });
111
+ messages = historyResult.messages;
112
+ users = historyResult.users;
113
+ }
114
+ return {
115
+ channel: {
116
+ ...channel,
117
+ unread_count: channel.unread_count || 0,
118
+ unread_count_display: channel.unread_count_display || 0,
119
+ },
120
+ messages,
121
+ users,
122
+ };
123
+ }
124
+ }
125
+ exports.SlackApiClient = SlackApiClient;
126
+ exports.slackApiClient = {
127
+ listChannels: async (token, options) => {
128
+ const client = new SlackApiClient(token);
129
+ return client.listChannels(options);
130
+ },
131
+ };
132
+ //# sourceMappingURL=slack-api-client.js.map
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@urugus/slack-cli",
3
+ "version": "0.1.0",
4
+ "description": "A command-line tool for sending messages to Slack",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "slack-cli": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/index.ts",
12
+ "start": "node dist/index.js",
13
+ "test": "vitest",
14
+ "test:watch": "vitest --watch",
15
+ "test:coverage": "vitest --coverage",
16
+ "lint": "eslint src/**/*.ts",
17
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
18
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "slack",
23
+ "cli",
24
+ "command-line",
25
+ "messaging",
26
+ "api"
27
+ ],
28
+ "author": "urugus",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@slack/web-api": "^6.9.0",
35
+ "commander": "^11.1.0",
36
+ "dotenv": "^16.3.1",
37
+ "chalk": "^4.1.2",
38
+ "inquirer": "^8.2.6"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.10.0",
42
+ "@types/inquirer": "^8.2.10",
43
+ "@typescript-eslint/eslint-plugin": "^6.13.0",
44
+ "@typescript-eslint/parser": "^6.13.0",
45
+ "@vitest/coverage-v8": "^1.0.4",
46
+ "eslint": "^8.55.0",
47
+ "eslint-config-prettier": "^9.1.0",
48
+ "prettier": "^3.1.1",
49
+ "ts-node": "^10.9.1",
50
+ "typescript": "^5.3.2",
51
+ "vitest": "^1.0.4"
52
+ },
53
+ "engines": {
54
+ "node": ">=16.0.0"
55
+ }
56
+ }
@@ -0,0 +1,65 @@
1
+ import { Command } from 'commander';
2
+ import { wrapCommand } from '../utils/command-wrapper';
3
+ import { createSlackClient } from '../utils/client-factory';
4
+ import { ERROR_MESSAGES } from '../utils/constants';
5
+ import { ChannelsOptions } from '../types/commands';
6
+ import {
7
+ mapChannelToInfo,
8
+ formatChannelsAsTable,
9
+ formatChannelsAsSimple,
10
+ formatChannelsAsJson,
11
+ getChannelTypes,
12
+ } from '../utils/channel-formatter';
13
+
14
+ export function setupChannelsCommand(): Command {
15
+ const channelsCommand = new Command('channels');
16
+
17
+ channelsCommand
18
+ .description('List Slack channels')
19
+ .option('--type <type>', 'Channel type: public, private, im, mpim, all', 'public')
20
+ .option('--include-archived', 'Include archived channels', false)
21
+ .option('--format <format>', 'Output format: table, simple, json', 'table')
22
+ .option('--limit <number>', 'Maximum number of channels to list', '100')
23
+ .option('--profile <profile>', 'Use specific workspace profile')
24
+ .action(
25
+ wrapCommand(async (options: ChannelsOptions) => {
26
+ // Create Slack client
27
+ const client = await createSlackClient(options.profile);
28
+
29
+ // Map channel type to API types
30
+ const types = getChannelTypes(options.type);
31
+
32
+ // List channels
33
+ const channels = await client.listChannels({
34
+ types,
35
+ exclude_archived: !options.includeArchived,
36
+ limit: parseInt(options.limit, 10),
37
+ });
38
+
39
+ if (channels.length === 0) {
40
+ console.log(ERROR_MESSAGES.NO_CHANNELS_FOUND);
41
+ return;
42
+ }
43
+
44
+ // Format and display channels
45
+ const channelInfos = channels.map(mapChannelToInfo);
46
+
47
+ switch (options.format) {
48
+ case 'simple':
49
+ formatChannelsAsSimple(channelInfos);
50
+ break;
51
+
52
+ case 'json':
53
+ formatChannelsAsJson(channelInfos);
54
+ break;
55
+
56
+ case 'table':
57
+ default:
58
+ formatChannelsAsTable(channelInfos);
59
+ break;
60
+ }
61
+ })
62
+ );
63
+
64
+ return channelsCommand;
65
+ }
@@ -0,0 +1,104 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { ProfileConfigManager } from '../utils/profile-config';
4
+ import { wrapCommand, getProfileName } from '../utils/command-wrapper';
5
+ import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../utils/constants';
6
+
7
+ export function setupConfigCommand(): Command {
8
+ const config = new Command('config').description('Manage Slack CLI configuration');
9
+
10
+ config
11
+ .command('set')
12
+ .description('Set API token')
13
+ .requiredOption('--token <token>', 'Slack API token')
14
+ .option('--profile <profile>', 'Profile name (default: "default")')
15
+ .action(
16
+ wrapCommand(async (options) => {
17
+ const configManager = new ProfileConfigManager();
18
+ const profileName = await getProfileName(configManager, options.profile);
19
+ await configManager.setToken(options.token, options.profile);
20
+ console.log(chalk.green(`✓ ${SUCCESS_MESSAGES.TOKEN_SAVED(profileName)}`));
21
+ })
22
+ );
23
+
24
+ config
25
+ .command('get')
26
+ .description('Show current configuration')
27
+ .option('--profile <profile>', 'Profile name')
28
+ .action(
29
+ wrapCommand(async (options) => {
30
+ const configManager = new ProfileConfigManager();
31
+ const profileName = await getProfileName(configManager, options.profile);
32
+ const currentConfig = await configManager.getConfig(options.profile);
33
+
34
+ if (!currentConfig) {
35
+ console.log(chalk.yellow(ERROR_MESSAGES.NO_CONFIG(profileName)));
36
+ return;
37
+ }
38
+
39
+ console.log(chalk.bold(`Configuration for profile "${profileName}":`));
40
+ console.log(` Token: ${chalk.cyan(configManager.maskToken(currentConfig.token))}`);
41
+ console.log(` Updated: ${chalk.gray(currentConfig.updatedAt)}`);
42
+ })
43
+ );
44
+
45
+ config
46
+ .command('profiles')
47
+ .description('List all profiles')
48
+ .action(
49
+ wrapCommand(async () => {
50
+ const configManager = new ProfileConfigManager();
51
+ const profiles = await configManager.listProfiles();
52
+ const currentProfile = await configManager.getCurrentProfile();
53
+
54
+ if (profiles.length === 0) {
55
+ console.log(chalk.yellow(ERROR_MESSAGES.NO_PROFILES_FOUND));
56
+ return;
57
+ }
58
+
59
+ console.log(chalk.bold('Available profiles:'));
60
+ profiles.forEach((profile) => {
61
+ const marker = profile.name === currentProfile ? '*' : ' ';
62
+ const maskedToken = configManager.maskToken(profile.config.token);
63
+ console.log(` ${marker} ${chalk.cyan(profile.name)} (${maskedToken})`);
64
+ });
65
+ })
66
+ );
67
+
68
+ config
69
+ .command('use <profile>')
70
+ .description('Switch to a different profile')
71
+ .action(
72
+ wrapCommand(async (profile) => {
73
+ const configManager = new ProfileConfigManager();
74
+ await configManager.useProfile(profile);
75
+ console.log(chalk.green(`✓ ${SUCCESS_MESSAGES.PROFILE_SWITCHED(profile)}`));
76
+ })
77
+ );
78
+
79
+ config
80
+ .command('current')
81
+ .description('Show current active profile')
82
+ .action(
83
+ wrapCommand(async () => {
84
+ const configManager = new ProfileConfigManager();
85
+ const currentProfile = await configManager.getCurrentProfile();
86
+ console.log(chalk.bold(`Current profile: ${chalk.cyan(currentProfile)}`));
87
+ })
88
+ );
89
+
90
+ config
91
+ .command('clear')
92
+ .description('Clear configuration')
93
+ .option('--profile <profile>', 'Profile name')
94
+ .action(
95
+ wrapCommand(async (options) => {
96
+ const configManager = new ProfileConfigManager();
97
+ const profileName = await getProfileName(configManager, options.profile);
98
+ await configManager.clearConfig(options.profile);
99
+ console.log(chalk.green(`✓ ${SUCCESS_MESSAGES.PROFILE_CLEARED(profileName)}`));
100
+ })
101
+ );
102
+
103
+ return config;
104
+ }
@@ -0,0 +1,96 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { HistoryOptions as ApiHistoryOptions, Message } from '../utils/slack-api-client';
4
+ import { wrapCommand } from '../utils/command-wrapper';
5
+ import { createSlackClient } from '../utils/client-factory';
6
+ import { HistoryOptions } from '../types/commands';
7
+ import { formatSlackTimestamp } from '../utils/date-utils';
8
+ import { API_LIMITS } from '../utils/constants';
9
+
10
+ export function setupHistoryCommand(): Command {
11
+ const historyCommand = new Command('history')
12
+ .description('Get message history from a Slack channel')
13
+ .requiredOption('-c, --channel <channel>', 'Target channel name or ID')
14
+ .option(
15
+ '-n, --number <number>',
16
+ 'Number of messages to retrieve',
17
+ API_LIMITS.DEFAULT_MESSAGE_COUNT.toString()
18
+ )
19
+ .option('--since <date>', 'Get messages since specific date (YYYY-MM-DD HH:MM:SS)')
20
+ .option('--profile <profile>', 'Use specific workspace profile')
21
+ .hook('preAction', (thisCommand) => {
22
+ const options = thisCommand.opts();
23
+
24
+ // Validate number option
25
+ if (options.number) {
26
+ const num = parseInt(options.number, 10);
27
+ if (
28
+ isNaN(num) ||
29
+ num < API_LIMITS.MIN_MESSAGE_COUNT ||
30
+ num > API_LIMITS.MAX_MESSAGE_COUNT
31
+ ) {
32
+ thisCommand.error(
33
+ `Error: Message count must be between ${API_LIMITS.MIN_MESSAGE_COUNT} and ${API_LIMITS.MAX_MESSAGE_COUNT}`
34
+ );
35
+ }
36
+ }
37
+
38
+ // Validate since option
39
+ if (options.since) {
40
+ const timestamp = Date.parse(options.since);
41
+ if (isNaN(timestamp)) {
42
+ thisCommand.error('Error: Invalid date format. Use YYYY-MM-DD HH:MM:SS');
43
+ }
44
+ }
45
+ })
46
+ .action(
47
+ wrapCommand(async (options: HistoryOptions) => {
48
+ // Create Slack client
49
+ const client = await createSlackClient(options.profile);
50
+
51
+ // Prepare API options
52
+ const historyOptions: ApiHistoryOptions = {
53
+ limit: parseInt(options.number || API_LIMITS.DEFAULT_MESSAGE_COUNT.toString(), 10),
54
+ };
55
+
56
+ if (options.since) {
57
+ // Convert date to Unix timestamp (in seconds)
58
+ const timestamp = Math.floor(Date.parse(options.since) / 1000);
59
+ historyOptions.oldest = timestamp.toString();
60
+ }
61
+
62
+ // Get message history
63
+ const { messages, users } = await client.getHistory(options.channel, historyOptions);
64
+
65
+ // Display results
66
+ if (messages.length === 0) {
67
+ console.log(chalk.yellow('No messages found in the specified channel.'));
68
+ return;
69
+ }
70
+
71
+ console.log(chalk.bold(`\nMessage History for #${options.channel}:\n`));
72
+
73
+ // Display messages in reverse order (oldest first)
74
+ messages.reverse().forEach((message: Message) => {
75
+ const timestamp = formatSlackTimestamp(message.ts);
76
+ let author = 'Unknown';
77
+
78
+ if (message.user && users.has(message.user)) {
79
+ author = users.get(message.user)!;
80
+ } else if (message.bot_id) {
81
+ author = 'Bot';
82
+ }
83
+
84
+ console.log(chalk.gray(`[${timestamp}]`) + ' ' + chalk.cyan(author));
85
+ if (message.text) {
86
+ console.log(message.text);
87
+ }
88
+ console.log(''); // Empty line between messages
89
+ });
90
+
91
+ console.log(chalk.green(`✓ Displayed ${messages.length} message(s)`));
92
+ })
93
+ );
94
+
95
+ return historyCommand;
96
+ }
@@ -0,0 +1,52 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { wrapCommand } from '../utils/command-wrapper';
4
+ import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../utils/constants';
5
+ import { createSlackClient } from '../utils/client-factory';
6
+ import { FileError } from '../utils/errors';
7
+ import { SendOptions } from '../types/commands';
8
+ import { extractErrorMessage } from '../utils/error-utils';
9
+ import * as fs from 'fs/promises';
10
+
11
+ export function setupSendCommand(): Command {
12
+ const sendCommand = new Command('send')
13
+ .description('Send a message to a Slack channel')
14
+ .requiredOption('-c, --channel <channel>', 'Target channel name or ID')
15
+ .option('-m, --message <message>', 'Message to send')
16
+ .option('-f, --file <file>', 'File containing message content')
17
+ .option('--profile <profile>', 'Use specific workspace profile')
18
+ .hook('preAction', (thisCommand) => {
19
+ const options = thisCommand.opts();
20
+ if (!options.message && !options.file) {
21
+ thisCommand.error(`Error: ${ERROR_MESSAGES.NO_MESSAGE_OR_FILE}`);
22
+ }
23
+ if (options.message && options.file) {
24
+ thisCommand.error(`Error: ${ERROR_MESSAGES.BOTH_MESSAGE_AND_FILE}`);
25
+ }
26
+ })
27
+ .action(
28
+ wrapCommand(async (options: SendOptions) => {
29
+ // Get message content
30
+ let messageContent: string;
31
+ if (options.file) {
32
+ try {
33
+ messageContent = await fs.readFile(options.file, 'utf-8');
34
+ } catch (error) {
35
+ throw new FileError(
36
+ ERROR_MESSAGES.FILE_READ_ERROR(options.file, extractErrorMessage(error))
37
+ );
38
+ }
39
+ } else {
40
+ messageContent = options.message!; // This is safe because of preAction validation
41
+ }
42
+
43
+ // Send message
44
+ const client = await createSlackClient(options.profile);
45
+ await client.sendMessage(options.channel, messageContent);
46
+
47
+ console.log(chalk.green(`✓ ${SUCCESS_MESSAGES.MESSAGE_SENT(options.channel)}`));
48
+ })
49
+ );
50
+
51
+ return sendCommand;
52
+ }
@@ -0,0 +1,118 @@
1
+ import { Command } from 'commander';
2
+ import { wrapCommand } from '../utils/command-wrapper';
3
+ import { createSlackClient } from '../utils/client-factory';
4
+ import { SlackApiClient, Channel } from '../utils/slack-api-client';
5
+ import { UnreadOptions } from '../types/commands';
6
+ import chalk from 'chalk';
7
+ import { formatSlackTimestamp } from '../utils/date-utils';
8
+ import { formatChannelName } from '../utils/channel-formatter';
9
+
10
+ async function handleSpecificChannelUnread(
11
+ client: SlackApiClient,
12
+ options: UnreadOptions
13
+ ): Promise<void> {
14
+ const result = await client.getChannelUnread(options.channel!);
15
+ const channelName = formatChannelName(result.channel.name);
16
+
17
+ console.log(chalk.bold(`${channelName}: ${result.channel.unread_count || 0} unread messages`));
18
+
19
+ if (!options.countOnly && result.messages.length > 0) {
20
+ console.log('');
21
+ result.messages.forEach((message) => {
22
+ const timestamp = formatSlackTimestamp(message.ts);
23
+ const author = message.user ? result.users.get(message.user) || message.user : 'unknown';
24
+ console.log(`${chalk.gray(timestamp)} ${chalk.cyan(author)}`);
25
+ console.log(message.text || '(no text)');
26
+ console.log('');
27
+ });
28
+ }
29
+ }
30
+
31
+ async function handleAllChannelsUnread(
32
+ client: SlackApiClient,
33
+ options: UnreadOptions
34
+ ): Promise<void> {
35
+ const channels = await client.listUnreadChannels();
36
+
37
+ if (channels.length === 0) {
38
+ console.log(chalk.green('✓ No unread messages'));
39
+ return;
40
+ }
41
+
42
+ // Apply limit
43
+ const limit = parseInt(options.limit || '50', 10);
44
+ const displayChannels = channels.slice(0, limit);
45
+
46
+ if (options.countOnly) {
47
+ displayCountOnly(displayChannels);
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
+ });
95
+ }
96
+
97
+ export function setupUnreadCommand(): Command {
98
+ const unreadCommand = new Command('unread')
99
+ .description('Show unread messages across channels')
100
+ .option('-c, --channel <channel>', 'Show unread for a specific channel')
101
+ .option('--format <format>', 'Output format: table, simple, json', 'table')
102
+ .option('--count-only', 'Show only unread counts', false)
103
+ .option('--limit <number>', 'Maximum number of channels to display', '50')
104
+ .option('--profile <profile>', 'Use specific workspace profile')
105
+ .action(
106
+ wrapCommand(async (options: UnreadOptions) => {
107
+ const client = await createSlackClient(options.profile);
108
+
109
+ if (options.channel) {
110
+ await handleSpecificChannelUnread(client, options);
111
+ } else {
112
+ await handleAllChannelsUnread(client, options);
113
+ }
114
+ })
115
+ );
116
+
117
+ return unreadCommand;
118
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { setupConfigCommand } from './commands/config';
4
+ import { setupSendCommand } from './commands/send';
5
+ import { setupChannelsCommand } from './commands/channels';
6
+ import { setupHistoryCommand } from './commands/history';
7
+ import { setupUnreadCommand } from './commands/unread';
8
+
9
+ const program = new Command();
10
+
11
+ program.name('slack-cli').description('CLI tool to send messages via Slack API').version('1.0.0');
12
+
13
+ program.addCommand(setupConfigCommand());
14
+ program.addCommand(setupSendCommand());
15
+ program.addCommand(setupChannelsCommand());
16
+ program.addCommand(setupHistoryCommand());
17
+ program.addCommand(setupUnreadCommand());
18
+
19
+ program.parse();