@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.
Files changed (39) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/CHANGELOG.md +39 -0
  3. package/dist/commands/unread.d.ts.map +1 -1
  4. package/dist/commands/unread.js +6 -49
  5. package/dist/commands/unread.js.map +1 -1
  6. package/dist/utils/channel-resolver.d.ts +26 -0
  7. package/dist/utils/channel-resolver.d.ts.map +1 -0
  8. package/dist/utils/channel-resolver.js +74 -0
  9. package/dist/utils/channel-resolver.js.map +1 -0
  10. package/dist/utils/constants.d.ts +17 -0
  11. package/dist/utils/constants.d.ts.map +1 -1
  12. package/dist/utils/constants.js +21 -1
  13. package/dist/utils/constants.js.map +1 -1
  14. package/dist/utils/formatters/channel-formatters.d.ts +16 -0
  15. package/dist/utils/formatters/channel-formatters.d.ts.map +1 -0
  16. package/dist/utils/formatters/channel-formatters.js +73 -0
  17. package/dist/utils/formatters/channel-formatters.js.map +1 -0
  18. package/dist/utils/formatters/output-formatter.d.ts +7 -0
  19. package/dist/utils/formatters/output-formatter.d.ts.map +1 -0
  20. package/dist/utils/formatters/output-formatter.js +7 -0
  21. package/dist/utils/formatters/output-formatter.js.map +1 -0
  22. package/dist/utils/profile-config.d.ts.map +1 -1
  23. package/dist/utils/profile-config.js +5 -3
  24. package/dist/utils/profile-config.js.map +1 -1
  25. package/dist/utils/slack-api-client.d.ts +2 -0
  26. package/dist/utils/slack-api-client.d.ts.map +1 -1
  27. package/dist/utils/slack-api-client.js +67 -96
  28. package/dist/utils/slack-api-client.js.map +1 -1
  29. package/package.json +5 -4
  30. package/src/commands/unread.ts +11 -52
  31. package/src/utils/channel-resolver.ts +86 -0
  32. package/src/utils/constants.ts +23 -0
  33. package/src/utils/formatters/channel-formatters.ts +69 -0
  34. package/src/utils/formatters/output-formatter.ts +7 -0
  35. package/src/utils/profile-config.ts +7 -5
  36. package/src/utils/slack-api-client.ts +81 -105
  37. package/tests/commands/unread.test.ts +31 -0
  38. package/tests/utils/channel-resolver.test.ts +157 -0
  39. 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
- // First, resolve channel name to ID if needed
35
- let channelId = channel;
36
- if (!channel.startsWith('C') && !channel.startsWith('D') && !channel.startsWith('G')) {
37
- // It's a name, not an ID - need to find the ID
38
- const channels = await this.listChannels({
39
- types: 'public_channel,private_channel,im,mpim',
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
- // Get unread count for each channel
109
- const channelsWithUnread = await Promise.all(channels.map(async (channel) => {
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
- return {
117
- ...channel,
118
- unread_count: channelInfo.unread_count || 0,
119
- unread_count_display: channelInfo.unread_count_display || 0,
120
- last_read: channelInfo.last_read,
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
- else {
168
- throw new Error(`Channel '${channelNameOrId}' not found. Make sure you are a member of this channel.`);
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":"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"}
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.5",
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
- "chalk": "^4.1.2",
38
- "inquirer": "^8.2.6"
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",
@@ -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, Channel } from '../utils/slack-api-client';
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 || '50', 10);
45
+ const limit = parseInt(options.limit || DEFAULTS.UNREAD_DISPLAY_LIMIT.toString(), 10);
44
46
  const displayChannels = channels.slice(0, limit);
45
47
 
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
- });
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('--limit <number>', 'Maximum number of channels to display', '50')
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();
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ export interface OutputFormatter<T> {
2
+ format(data: T[]): void;
3
+ }
4
+
5
+ export abstract class BaseFormatter<T> implements OutputFormatter<T> {
6
+ abstract format(data: T[]): void;
7
+ }
@@ -130,14 +130,16 @@ export class ProfileConfigManager {
130
130
  }
131
131
  }
132
132
 
133
- private needsMigration(data: any): boolean {
134
- return data.token && !data.profiles;
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: any): Promise<ConfigStore> {
138
+ private async migrateOldConfig(oldData: unknown): Promise<ConfigStore> {
139
+ const data = oldData as { token: string; updatedAt: string };
138
140
  const oldConfig: Config = {
139
- token: oldData.token,
140
- updatedAt: oldData.updatedAt,
141
+ token: data.token,
142
+ updatedAt: data.updatedAt,
141
143
  };
142
144
 
143
145
  const newStore: ConfigStore = {