@jtalk22/slack-mcp 1.0.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.
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Tool Handlers
3
+ *
4
+ * Implementation of all MCP tool handlers.
5
+ */
6
+
7
+ import { writeFileSync } from "fs";
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ import { loadTokens, saveTokens, extractFromChrome } from "./token-store.js";
11
+ import { slackAPI, resolveUser, formatTimestamp, sleep } from "./slack-client.js";
12
+
13
+ /**
14
+ * Health check handler
15
+ */
16
+ export async function handleHealthCheck() {
17
+ const creds = loadTokens();
18
+ if (!creds) {
19
+ return {
20
+ content: [{
21
+ type: "text",
22
+ text: "NO CREDENTIALS\n\nOptions:\n1. Open Slack in Chrome, then use slack_refresh_tokens\n2. Run: ~/slack-mcp-server/scripts/refresh-tokens.sh"
23
+ }],
24
+ isError: true
25
+ };
26
+ }
27
+
28
+ try {
29
+ const result = await slackAPI("auth.test", {});
30
+ return {
31
+ content: [{
32
+ type: "text",
33
+ text: JSON.stringify({
34
+ status: "OK",
35
+ user: result.user,
36
+ user_id: result.user_id,
37
+ team: result.team,
38
+ team_id: result.team_id,
39
+ token_source: creds.source,
40
+ token_updated: creds.updatedAt || "unknown"
41
+ }, null, 2)
42
+ }]
43
+ };
44
+ } catch (e) {
45
+ return {
46
+ content: [{ type: "text", text: `AUTH FAILED: ${e.message}` }],
47
+ isError: true
48
+ };
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Refresh tokens handler
54
+ */
55
+ export async function handleRefreshTokens() {
56
+ const chromeTokens = extractFromChrome();
57
+ if (chromeTokens) {
58
+ saveTokens(chromeTokens.token, chromeTokens.cookie);
59
+ try {
60
+ const result = await slackAPI("auth.test", {}, { retryOnAuthFail: false });
61
+ return {
62
+ content: [{
63
+ type: "text",
64
+ text: JSON.stringify({
65
+ status: "SUCCESS",
66
+ message: "Tokens refreshed from Chrome!",
67
+ user: result.user,
68
+ team: result.team
69
+ }, null, 2)
70
+ }]
71
+ };
72
+ } catch (e) {
73
+ return {
74
+ content: [{ type: "text", text: `Extracted but invalid: ${e.message}` }],
75
+ isError: true
76
+ };
77
+ }
78
+ }
79
+ return {
80
+ content: [{
81
+ type: "text",
82
+ text: "Could not extract from Chrome.\n\nMake sure:\n1. Chrome is running\n2. Slack tab is open (app.slack.com)\n3. You're logged into Slack"
83
+ }],
84
+ isError: true
85
+ };
86
+ }
87
+
88
+ /**
89
+ * List conversations handler
90
+ */
91
+ export async function handleListConversations(args) {
92
+ const types = args.types || "im,mpim";
93
+ const wantsDMs = types.includes("im") || types.includes("mpim");
94
+
95
+ const result = await slackAPI("conversations.list", {
96
+ types: types,
97
+ limit: args.limit || 100,
98
+ exclude_archived: true
99
+ });
100
+
101
+ const conversations = await Promise.all((result.channels || []).map(async (c) => {
102
+ let displayName = c.name;
103
+ if (c.is_im && c.user) {
104
+ displayName = await resolveUser(c.user);
105
+ }
106
+ return {
107
+ id: c.id,
108
+ name: displayName,
109
+ type: c.is_im ? "dm" : c.is_mpim ? "group_dm" : c.is_private ? "private_channel" : "public_channel",
110
+ user_id: c.user
111
+ };
112
+ }));
113
+
114
+ // xoxc tokens often don't return IMs via conversations.list
115
+ // So we manually add DMs by opening them with known users
116
+ if (wantsDMs) {
117
+ try {
118
+ const usersResult = await slackAPI("users.list", { limit: 100 });
119
+ for (const user of (usersResult.members || [])) {
120
+ if (user.is_bot || user.id === "USLACKBOT" || user.deleted) continue;
121
+
122
+ // Try to open DM with each user to get channel ID
123
+ try {
124
+ const dmResult = await slackAPI("conversations.open", { users: user.id });
125
+ if (dmResult.channel && dmResult.channel.id) {
126
+ const channelId = dmResult.channel.id;
127
+ // Only add if not already in list
128
+ if (!conversations.find(c => c.id === channelId)) {
129
+ conversations.push({
130
+ id: channelId,
131
+ name: user.real_name || user.name,
132
+ type: "dm",
133
+ user_id: user.id
134
+ });
135
+ }
136
+ }
137
+ } catch (e) {
138
+ // Skip users we can't DM
139
+ }
140
+ }
141
+ } catch (e) {
142
+ // If users.list fails, continue with what we have
143
+ }
144
+ }
145
+
146
+ return {
147
+ content: [{
148
+ type: "text",
149
+ text: JSON.stringify({ count: conversations.length, conversations }, null, 2)
150
+ }]
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Conversations history handler
156
+ */
157
+ export async function handleConversationsHistory(args) {
158
+ const resolveUsers = args.resolve_users !== false;
159
+ const result = await slackAPI("conversations.history", {
160
+ channel: args.channel_id,
161
+ limit: args.limit || 50,
162
+ oldest: args.oldest,
163
+ latest: args.latest,
164
+ inclusive: true
165
+ });
166
+
167
+ const messages = await Promise.all((result.messages || []).map(async (msg) => {
168
+ const userName = resolveUsers ? await resolveUser(msg.user) : msg.user;
169
+ return {
170
+ ts: msg.ts,
171
+ user: userName,
172
+ user_id: msg.user,
173
+ text: msg.text || "",
174
+ datetime: formatTimestamp(msg.ts),
175
+ has_thread: !!msg.thread_ts && msg.reply_count > 0,
176
+ reply_count: msg.reply_count
177
+ };
178
+ }));
179
+
180
+ return {
181
+ content: [{
182
+ type: "text",
183
+ text: JSON.stringify({
184
+ channel: args.channel_id,
185
+ message_count: messages.length,
186
+ has_more: result.has_more,
187
+ messages
188
+ }, null, 2)
189
+ }]
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Full conversation export handler
195
+ */
196
+ export async function handleGetFullConversation(args) {
197
+ const maxMessages = Math.min(args.max_messages || 2000, 10000);
198
+ const includeThreads = args.include_threads !== false;
199
+ const allMessages = [];
200
+ let cursor;
201
+ let hasMore = true;
202
+
203
+ // Fetch all messages with pagination
204
+ while (hasMore && allMessages.length < maxMessages) {
205
+ const result = await slackAPI("conversations.history", {
206
+ channel: args.channel_id,
207
+ limit: Math.min(100, maxMessages - allMessages.length),
208
+ oldest: args.oldest,
209
+ latest: args.latest,
210
+ cursor,
211
+ inclusive: true
212
+ });
213
+
214
+ for (const msg of result.messages || []) {
215
+ const userName = await resolveUser(msg.user);
216
+ const message = {
217
+ ts: msg.ts,
218
+ user: userName,
219
+ user_id: msg.user,
220
+ text: msg.text || "",
221
+ datetime: formatTimestamp(msg.ts),
222
+ replies: []
223
+ };
224
+
225
+ // Fetch thread replies if present
226
+ if (includeThreads && msg.reply_count > 0) {
227
+ try {
228
+ const threadResult = await slackAPI("conversations.replies", {
229
+ channel: args.channel_id,
230
+ ts: msg.ts
231
+ });
232
+ // Skip first message (parent)
233
+ for (const reply of (threadResult.messages || []).slice(1)) {
234
+ const replyUserName = await resolveUser(reply.user);
235
+ message.replies.push({
236
+ ts: reply.ts,
237
+ user: replyUserName,
238
+ text: reply.text || "",
239
+ datetime: formatTimestamp(reply.ts)
240
+ });
241
+ }
242
+ await sleep(50); // Rate limit
243
+ } catch (e) {
244
+ // Skip thread on error
245
+ }
246
+ }
247
+
248
+ allMessages.push(message);
249
+ }
250
+
251
+ hasMore = result.has_more && result.response_metadata?.next_cursor;
252
+ cursor = result.response_metadata?.next_cursor;
253
+ if (hasMore) await sleep(100);
254
+ }
255
+
256
+ // Sort chronologically
257
+ allMessages.sort((a, b) => parseFloat(a.ts) - parseFloat(b.ts));
258
+
259
+ const output = {
260
+ channel: args.channel_id,
261
+ exported_at: new Date().toISOString(),
262
+ total_messages: allMessages.length,
263
+ date_range: {
264
+ oldest: args.oldest ? formatTimestamp(args.oldest) : "beginning",
265
+ latest: args.latest ? formatTimestamp(args.latest) : "now"
266
+ },
267
+ messages: allMessages
268
+ };
269
+
270
+ // Save to file if requested
271
+ if (args.output_file) {
272
+ const outputPath = args.output_file.startsWith('/')
273
+ ? args.output_file
274
+ : join(homedir(), args.output_file);
275
+ writeFileSync(outputPath, JSON.stringify(output, null, 2));
276
+ output.saved_to = outputPath;
277
+ }
278
+
279
+ return { content: [{ type: "text", text: JSON.stringify(output, null, 2) }] };
280
+ }
281
+
282
+ /**
283
+ * Search messages handler
284
+ */
285
+ export async function handleSearchMessages(args) {
286
+ const result = await slackAPI("search.messages", {
287
+ query: args.query,
288
+ count: args.count || 20,
289
+ sort: "timestamp",
290
+ sort_dir: "desc"
291
+ });
292
+
293
+ const matches = await Promise.all((result.messages?.matches || []).map(async (m) => ({
294
+ ts: m.ts,
295
+ channel: m.channel?.name || m.channel?.id,
296
+ channel_id: m.channel?.id,
297
+ user: await resolveUser(m.user),
298
+ text: m.text,
299
+ datetime: formatTimestamp(m.ts),
300
+ permalink: m.permalink
301
+ })));
302
+
303
+ return {
304
+ content: [{
305
+ type: "text",
306
+ text: JSON.stringify({
307
+ query: args.query,
308
+ total: result.messages?.total || 0,
309
+ matches
310
+ }, null, 2)
311
+ }]
312
+ };
313
+ }
314
+
315
+ /**
316
+ * User info handler
317
+ */
318
+ export async function handleUsersInfo(args) {
319
+ const result = await slackAPI("users.info", { user: args.user_id });
320
+ const user = result.user;
321
+ return {
322
+ content: [{
323
+ type: "text",
324
+ text: JSON.stringify({
325
+ id: user.id,
326
+ name: user.name,
327
+ real_name: user.real_name,
328
+ display_name: user.profile?.display_name,
329
+ email: user.profile?.email,
330
+ title: user.profile?.title,
331
+ status_text: user.profile?.status_text,
332
+ status_emoji: user.profile?.status_emoji,
333
+ timezone: user.tz,
334
+ is_bot: user.is_bot,
335
+ is_admin: user.is_admin
336
+ }, null, 2)
337
+ }]
338
+ };
339
+ }
340
+
341
+ /**
342
+ * Send message handler
343
+ */
344
+ export async function handleSendMessage(args) {
345
+ const result = await slackAPI("chat.postMessage", {
346
+ channel: args.channel_id,
347
+ text: args.text,
348
+ thread_ts: args.thread_ts
349
+ });
350
+
351
+ return {
352
+ content: [{
353
+ type: "text",
354
+ text: JSON.stringify({
355
+ status: "sent",
356
+ channel: result.channel,
357
+ ts: result.ts,
358
+ thread_ts: args.thread_ts,
359
+ message: result.message?.text
360
+ }, null, 2)
361
+ }]
362
+ };
363
+ }
364
+
365
+ /**
366
+ * Get thread handler
367
+ */
368
+ export async function handleGetThread(args) {
369
+ const result = await slackAPI("conversations.replies", {
370
+ channel: args.channel_id,
371
+ ts: args.thread_ts
372
+ });
373
+
374
+ const messages = await Promise.all((result.messages || []).map(async (msg) => ({
375
+ ts: msg.ts,
376
+ user: await resolveUser(msg.user),
377
+ user_id: msg.user,
378
+ text: msg.text || "",
379
+ datetime: formatTimestamp(msg.ts),
380
+ is_parent: msg.ts === args.thread_ts
381
+ })));
382
+
383
+ return {
384
+ content: [{
385
+ type: "text",
386
+ text: JSON.stringify({
387
+ channel: args.channel_id,
388
+ thread_ts: args.thread_ts,
389
+ message_count: messages.length,
390
+ messages
391
+ }, null, 2)
392
+ }]
393
+ };
394
+ }
395
+
396
+ /**
397
+ * List users handler
398
+ */
399
+ export async function handleListUsers(args) {
400
+ const result = await slackAPI("users.list", {
401
+ limit: args.limit || 100
402
+ });
403
+
404
+ const users = (result.members || [])
405
+ .filter(u => !u.deleted && !u.is_bot)
406
+ .map(u => ({
407
+ id: u.id,
408
+ name: u.name,
409
+ real_name: u.real_name,
410
+ display_name: u.profile?.display_name,
411
+ email: u.profile?.email,
412
+ is_admin: u.is_admin
413
+ }));
414
+
415
+ return {
416
+ content: [{
417
+ type: "text",
418
+ text: JSON.stringify({ count: users.length, users }, null, 2)
419
+ }]
420
+ };
421
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Slack API Client
3
+ *
4
+ * Handles all Slack API communication with:
5
+ * - Automatic token refresh on auth failure
6
+ * - User name caching
7
+ * - Rate limiting
8
+ */
9
+
10
+ import { loadTokens, saveTokens, extractFromChrome } from "./token-store.js";
11
+
12
+ // User cache to avoid repeated API calls
13
+ const userCache = new Map();
14
+
15
+ /**
16
+ * Make an authenticated Slack API call
17
+ */
18
+ export async function slackAPI(method, params = {}, options = {}) {
19
+ const { retryOnAuthFail = true, retryCount = 0, maxRetries = 3, logger = console } = options;
20
+
21
+ const creds = loadTokens(false, logger);
22
+ if (!creds) {
23
+ throw new Error("No credentials available. Run refresh-tokens.sh or open Slack in Chrome.");
24
+ }
25
+
26
+ const response = await fetch(`https://slack.com/api/${method}`, {
27
+ method: "POST",
28
+ headers: {
29
+ "Authorization": `Bearer ${creds.token}`,
30
+ "Cookie": `d=${creds.cookie}`,
31
+ "Content-Type": "application/json; charset=utf-8",
32
+ },
33
+ body: JSON.stringify(params),
34
+ });
35
+
36
+ const data = await response.json();
37
+
38
+ if (!data.ok) {
39
+ // Handle rate limiting with exponential backoff
40
+ if (data.error === "ratelimited" && retryCount < maxRetries) {
41
+ const retryAfter = parseInt(response.headers.get("Retry-After") || "5", 10);
42
+ const backoff = Math.min(retryAfter * 1000, 30000) * (retryCount + 1);
43
+ logger.error(`Rate limited on ${method}, waiting ${backoff}ms before retry ${retryCount + 1}/${maxRetries}`);
44
+ await sleep(backoff);
45
+ return slackAPI(method, params, { ...options, retryCount: retryCount + 1 });
46
+ }
47
+
48
+ // Handle auth errors with auto-retry
49
+ if ((data.error === "invalid_auth" || data.error === "token_expired") && retryOnAuthFail) {
50
+ logger.error("Token expired, attempting Chrome auto-extraction...");
51
+ const chromeTokens = extractFromChrome();
52
+ if (chromeTokens) {
53
+ saveTokens(chromeTokens.token, chromeTokens.cookie);
54
+ // Retry the request
55
+ return slackAPI(method, params, { ...options, retryOnAuthFail: false });
56
+ }
57
+ throw new Error(`${data.error} - Tokens expired. Open Slack in Chrome and use slack_refresh_tokens.`);
58
+ }
59
+ throw new Error(data.error || "Slack API error");
60
+ }
61
+
62
+ return data;
63
+ }
64
+
65
+ /**
66
+ * Resolve user ID to real name (with caching)
67
+ */
68
+ export async function resolveUser(userId, options = {}) {
69
+ if (!userId) return "unknown";
70
+ if (userCache.has(userId)) return userCache.get(userId);
71
+
72
+ try {
73
+ const result = await slackAPI("users.info", { user: userId }, options);
74
+ const name = result.user?.real_name || result.user?.name || userId;
75
+ userCache.set(userId, name);
76
+ return name;
77
+ } catch (e) {
78
+ userCache.set(userId, userId);
79
+ return userId;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Clear the user cache
85
+ */
86
+ export function clearUserCache() {
87
+ userCache.clear();
88
+ }
89
+
90
+ /**
91
+ * Get user cache stats
92
+ */
93
+ export function getUserCacheStats() {
94
+ return {
95
+ size: userCache.size,
96
+ entries: Array.from(userCache.entries())
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Format a Slack timestamp to ISO string
102
+ */
103
+ export function formatTimestamp(ts) {
104
+ return new Date(parseFloat(ts) * 1000).toISOString();
105
+ }
106
+
107
+ /**
108
+ * Convert ISO date to Slack timestamp
109
+ */
110
+ export function toSlackTimestamp(isoDate) {
111
+ return (new Date(isoDate).getTime() / 1000).toString();
112
+ }
113
+
114
+ /**
115
+ * Sleep for rate limiting
116
+ */
117
+ export function sleep(ms) {
118
+ return new Promise(resolve => setTimeout(resolve, ms));
119
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Token Storage Module
3
+ *
4
+ * Multi-layer token persistence:
5
+ * 1. Environment variables (highest priority)
6
+ * 2. Token file (~/.slack-mcp-tokens.json)
7
+ * 3. macOS Keychain (most secure)
8
+ * 4. Chrome auto-extraction (fallback)
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, existsSync } from "fs";
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ import { execSync } from "child_process";
15
+
16
+ const TOKEN_FILE = join(homedir(), ".slack-mcp-tokens.json");
17
+ const KEYCHAIN_SERVICE = "slack-mcp-server";
18
+
19
+ // ============ Keychain Storage ============
20
+
21
+ export function getFromKeychain(key) {
22
+ try {
23
+ const result = execSync(
24
+ `security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}" -w 2>/dev/null`,
25
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
26
+ );
27
+ return result.trim();
28
+ } catch (e) {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ export function saveToKeychain(key, value) {
34
+ try {
35
+ // Delete existing entry
36
+ try {
37
+ execSync(`security delete-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}" 2>/dev/null`, { stdio: 'pipe' });
38
+ } catch (e) { /* ignore */ }
39
+
40
+ // Add new entry
41
+ execSync(`security add-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}" -w "${value}"`, { stdio: 'pipe' });
42
+ return true;
43
+ } catch (e) {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ // ============ File Storage ============
49
+
50
+ export function getFromFile() {
51
+ if (!existsSync(TOKEN_FILE)) return null;
52
+ try {
53
+ const data = JSON.parse(readFileSync(TOKEN_FILE, "utf-8"));
54
+ return {
55
+ token: data.SLACK_TOKEN,
56
+ cookie: data.SLACK_COOKIE,
57
+ updatedAt: data.updated_at
58
+ };
59
+ } catch (e) {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ export function saveToFile(token, cookie) {
65
+ const data = {
66
+ SLACK_TOKEN: token,
67
+ SLACK_COOKIE: cookie,
68
+ updated_at: new Date().toISOString()
69
+ };
70
+ writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2));
71
+ try {
72
+ execSync(`chmod 600 "${TOKEN_FILE}"`);
73
+ } catch (e) { /* ignore on non-unix */ }
74
+ }
75
+
76
+ // ============ Chrome Extraction ============
77
+
78
+ export function extractFromChrome() {
79
+ try {
80
+ // Extract cookie
81
+ const cookieScript = `
82
+ tell application "Google Chrome"
83
+ repeat with w in windows
84
+ repeat with t in tabs of w
85
+ if URL of t contains "slack.com" then
86
+ return execute t javascript "document.cookie.split('; ').find(c => c.startsWith('d='))?.split('=')[1] || ''"
87
+ end if
88
+ end repeat
89
+ end repeat
90
+ return ""
91
+ end tell
92
+ `;
93
+ const cookie = execSync(`osascript -e '${cookieScript.replace(/'/g, "'\"'\"'")}'`, {
94
+ encoding: 'utf-8', timeout: 5000
95
+ }).trim();
96
+
97
+ if (!cookie || !cookie.startsWith('xoxd-')) return null;
98
+
99
+ // Extract token
100
+ const tokenScript = `
101
+ tell application "Google Chrome"
102
+ repeat with w in windows
103
+ repeat with t in tabs of w
104
+ if URL of t contains "slack.com" then
105
+ return execute t javascript "try{JSON.parse(localStorage.localConfig_v2).teams[Object.keys(JSON.parse(localStorage.localConfig_v2).teams)[0]].token}catch(e){''}"
106
+ end if
107
+ end repeat
108
+ end repeat
109
+ return ""
110
+ end tell
111
+ `;
112
+ const token = execSync(`osascript -e '${tokenScript.replace(/'/g, "'\"'\"'")}'`, {
113
+ encoding: 'utf-8', timeout: 5000
114
+ }).trim();
115
+
116
+ if (!token || !token.startsWith('xoxc-')) return null;
117
+
118
+ return { token, cookie };
119
+ } catch (e) {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ // ============ Main Token Loader ============
125
+
126
+ export function loadTokens(forceRefresh = false, logger = console) {
127
+ // Priority 1: Environment variables
128
+ if (!forceRefresh && process.env.SLACK_TOKEN && process.env.SLACK_COOKIE) {
129
+ return {
130
+ token: process.env.SLACK_TOKEN,
131
+ cookie: process.env.SLACK_COOKIE,
132
+ source: "environment"
133
+ };
134
+ }
135
+
136
+ // Priority 2: Token file
137
+ if (!forceRefresh) {
138
+ const fileTokens = getFromFile();
139
+ if (fileTokens?.token && fileTokens?.cookie) {
140
+ return {
141
+ token: fileTokens.token,
142
+ cookie: fileTokens.cookie,
143
+ source: "file",
144
+ updatedAt: fileTokens.updatedAt
145
+ };
146
+ }
147
+ }
148
+
149
+ // Priority 3: Keychain
150
+ if (!forceRefresh) {
151
+ const keychainToken = getFromKeychain("token");
152
+ const keychainCookie = getFromKeychain("cookie");
153
+ if (keychainToken && keychainCookie) {
154
+ return {
155
+ token: keychainToken,
156
+ cookie: keychainCookie,
157
+ source: "keychain"
158
+ };
159
+ }
160
+ }
161
+
162
+ // Priority 4: Chrome auto-extract
163
+ logger.error("Attempting Chrome auto-extraction...");
164
+ const chromeTokens = extractFromChrome();
165
+ if (chromeTokens) {
166
+ logger.error("Successfully extracted tokens from Chrome!");
167
+ saveTokens(chromeTokens.token, chromeTokens.cookie);
168
+ return {
169
+ token: chromeTokens.token,
170
+ cookie: chromeTokens.cookie,
171
+ source: "chrome-auto"
172
+ };
173
+ }
174
+
175
+ return null;
176
+ }
177
+
178
+ export function saveTokens(token, cookie) {
179
+ saveToFile(token, cookie);
180
+ saveToKeychain("token", token);
181
+ saveToKeychain("cookie", cookie);
182
+ }
183
+
184
+ export { TOKEN_FILE, KEYCHAIN_SERVICE };