@jtalk22/slack-mcp 1.2.2 → 1.2.4

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,33 @@
1
+ # Release Health Snapshot
2
+
3
+ - Generated: 2026-02-26T09:32:04.228Z
4
+ - Repo: `jtalk22/slack-mcp-server`
5
+ - Package: `@jtalk22/slack-mcp`
6
+
7
+ ## Install Signals
8
+
9
+ - npm downloads (last week): 532
10
+ - npm downloads (last month): 1011
11
+ - npm latest version: 1.2.3
12
+
13
+ ## GitHub Reach
14
+
15
+ - stars: 13
16
+ - forks: 8
17
+ - open issues: 0
18
+ - 14d views: 498
19
+ - 14d unique visitors: 263
20
+ - 14d clones: 338
21
+ - 14d unique cloners: 113
22
+ - deployment-intake submissions (all-time): 1
23
+
24
+ ## 14-Day Reliability Targets (v1.2.3 Cycle)
25
+
26
+ - weekly downloads: >= 180
27
+ - qualified deployment-intake submissions: >= 2
28
+ - maintainer support load: <= 2 hours/week
29
+
30
+ ## Notes
31
+
32
+ - Update this snapshot daily during active release windows, then weekly.
33
+ - Track deployment-intake quality and support load manually in issue notes.
@@ -0,0 +1,33 @@
1
+ # Release Health Snapshot
2
+
3
+ - Generated: 2026-02-26T09:32:53.328Z
4
+ - Repo: `jtalk22/slack-mcp-server`
5
+ - Package: `@jtalk22/slack-mcp`
6
+
7
+ ## Install Signals
8
+
9
+ - npm downloads (last week): 532
10
+ - npm downloads (last month): 1011
11
+ - npm latest version: 1.2.3
12
+
13
+ ## GitHub Reach
14
+
15
+ - stars: 13
16
+ - forks: 8
17
+ - open issues: 0
18
+ - 14d views: 498
19
+ - 14d unique visitors: 263
20
+ - 14d clones: 338
21
+ - 14d unique cloners: 113
22
+ - deployment-intake submissions (all-time): 1
23
+
24
+ ## 14-Day Reliability Targets (v1.2.3 Cycle)
25
+
26
+ - weekly downloads: >= 180
27
+ - qualified deployment-intake submissions: >= 2
28
+ - maintainer support load: <= 2 hours/week
29
+
30
+ ## Notes
31
+
32
+ - Update this snapshot daily during active release windows, then weekly.
33
+ - Track deployment-intake quality and support load manually in issue notes.
package/lib/handlers.js CHANGED
@@ -8,7 +8,13 @@ import { writeFileSync, readFileSync, existsSync, renameSync, unlinkSync, mkdirS
8
8
  import { execSync } from "child_process";
9
9
  import { homedir, platform } from "os";
10
10
  import { join } from "path";
11
- import { loadTokens, saveTokens, extractFromChrome, isAutoRefreshAvailable } from "./token-store.js";
11
+ import {
12
+ loadTokensReadOnly,
13
+ saveTokens,
14
+ extractFromChrome,
15
+ isAutoRefreshAvailable,
16
+ getLastExtractionError
17
+ } from "./token-store.js";
12
18
  import { slackAPI, resolveUser, formatTimestamp, sleep, checkTokenHealth, getUserCacheStats } from "./slack-client.js";
13
19
 
14
20
  // ============ Utilities ============
@@ -26,6 +32,16 @@ function parseBool(val) {
26
32
  return false;
27
33
  }
28
34
 
35
+ function asMcpJson(payload, isError = false) {
36
+ return {
37
+ content: [{
38
+ type: "text",
39
+ text: JSON.stringify(payload, null, 2)
40
+ }],
41
+ ...(isError ? { isError: true } : {})
42
+ };
43
+ }
44
+
29
45
  /**
30
46
  * Atomic write to prevent file corruption from concurrent writes
31
47
  */
@@ -77,72 +93,75 @@ export async function handleTokenStatus() {
77
93
  const health = await checkTokenHealth({ error: () => {} });
78
94
  const cacheStats = getUserCacheStats();
79
95
  const dmCache = loadDMCache();
80
-
81
- return {
82
- content: [{
83
- type: "text",
84
- text: JSON.stringify({
85
- token: {
86
- status: health.healthy ? "healthy" : health.reason === 'no_tokens' ? "missing" : "warning",
87
- age_hours: health.age_hours,
88
- source: health.source,
89
- updated_at: health.updated_at,
90
- message: health.message
91
- },
92
- auto_refresh: {
93
- enabled: true,
94
- interval: "4 hours",
95
- last_attempt: health.refreshed ? "just now" : "on-demand",
96
- requires: "Slack tab open in Chrome"
97
- },
98
- cache: {
99
- users: cacheStats,
100
- dms: {
101
- count: Object.keys(dmCache.dms || {}).length,
102
- age_hours: dmCache.updated ? Math.round((Date.now() - dmCache.updated) / (60 * 60 * 1000) * 10) / 10 : null
103
- }
104
- }
105
- }, null, 2)
106
- }]
107
- };
96
+ const tokenStatus = health.reason === 'no_tokens'
97
+ ? "missing"
98
+ : health.critical
99
+ ? "critical"
100
+ : health.warning
101
+ ? "warning"
102
+ : "healthy";
103
+
104
+ return asMcpJson({
105
+ status: tokenStatus,
106
+ code: health.reason || (health.critical ? "token_critical" : health.warning ? "token_warning" : "ok"),
107
+ message: health.message,
108
+ next_action: health.reason === 'no_tokens' ? "Run npx -y @jtalk22/slack-mcp --setup" : null,
109
+ token: {
110
+ status: tokenStatus,
111
+ age_hours: health.age_hours,
112
+ source: health.source,
113
+ updated_at: health.updated_at
114
+ },
115
+ auto_refresh: {
116
+ enabled: isAutoRefreshAvailable(),
117
+ interval: "4 hours",
118
+ last_attempt: health.refreshed ? "just_now" : null,
119
+ requires: isAutoRefreshAvailable() ? "Slack tab open in Chrome" : "Not supported on this platform"
120
+ },
121
+ cache: {
122
+ users: cacheStats,
123
+ dms: {
124
+ count: Object.keys(dmCache.dms || {}).length,
125
+ age_hours: dmCache.updated ? Math.round((Date.now() - dmCache.updated) / (60 * 60 * 1000) * 10) / 10 : null
126
+ }
127
+ }
128
+ });
108
129
  }
109
130
 
110
131
  /**
111
132
  * Health check handler
112
133
  */
113
134
  export async function handleHealthCheck() {
114
- const creds = loadTokens();
135
+ const creds = loadTokensReadOnly();
115
136
  if (!creds) {
116
- return {
117
- content: [{
118
- type: "text",
119
- text: "NO CREDENTIALS\n\nOptions:\n1. Open Slack in Chrome, then use slack_refresh_tokens\n2. Run: npx -y @jtalk22/slack-mcp --setup"
120
- }],
121
- isError: true
122
- };
137
+ return asMcpJson({
138
+ status: "error",
139
+ code: "missing_credentials",
140
+ message: "No credentials found",
141
+ next_action: "Run npx -y @jtalk22/slack-mcp --setup"
142
+ }, true);
123
143
  }
124
144
 
125
145
  try {
126
- const result = await slackAPI("auth.test", {});
127
- return {
128
- content: [{
129
- type: "text",
130
- text: JSON.stringify({
131
- status: "OK",
132
- user: result.user,
133
- user_id: result.user_id,
134
- team: result.team,
135
- team_id: result.team_id,
136
- token_source: creds.source,
137
- token_updated: creds.updatedAt || "unknown"
138
- }, null, 2)
139
- }]
140
- };
146
+ const result = await slackAPI("auth.test", {}, { retryOnAuthFail: false });
147
+ return asMcpJson({
148
+ status: "ok",
149
+ code: "ok",
150
+ message: "Slack auth valid",
151
+ user: result.user,
152
+ user_id: result.user_id,
153
+ team: result.team,
154
+ team_id: result.team_id,
155
+ token_source: creds.source,
156
+ token_updated: creds.updatedAt || null
157
+ });
141
158
  } catch (e) {
142
- return {
143
- content: [{ type: "text", text: `AUTH FAILED: ${e.message}` }],
144
- isError: true
145
- };
159
+ return asMcpJson({
160
+ status: "error",
161
+ code: "auth_failed",
162
+ message: e.message,
163
+ next_action: "Run npx -y @jtalk22/slack-mcp --setup"
164
+ }, true);
146
165
  }
147
166
  }
148
167
 
@@ -152,13 +171,12 @@ export async function handleHealthCheck() {
152
171
  export async function handleRefreshTokens() {
153
172
  // Check platform support
154
173
  if (!isAutoRefreshAvailable()) {
155
- return {
156
- content: [{
157
- type: "text",
158
- text: "Auto-refresh is only available on macOS.\n\nOn other platforms, manually update ~/.slack-mcp-tokens.json with:\n{\n \"SLACK_TOKEN\": \"xoxc-...\",\n \"SLACK_COOKIE\": \"xoxd-...\"\n}"
159
- }],
160
- isError: true
161
- };
174
+ return asMcpJson({
175
+ status: "error",
176
+ code: "unsupported_platform",
177
+ message: "Auto-refresh is only available on macOS.",
178
+ next_action: "Manually update ~/.slack-mcp-tokens.json with SLACK_TOKEN and SLACK_COOKIE."
179
+ }, true);
162
180
  }
163
181
 
164
182
  const chromeTokens = extractFromChrome();
@@ -166,31 +184,41 @@ export async function handleRefreshTokens() {
166
184
  saveTokens(chromeTokens.token, chromeTokens.cookie);
167
185
  try {
168
186
  const result = await slackAPI("auth.test", {}, { retryOnAuthFail: false });
169
- return {
170
- content: [{
171
- type: "text",
172
- text: JSON.stringify({
173
- status: "SUCCESS",
174
- message: "Tokens refreshed from Chrome!",
175
- user: result.user,
176
- team: result.team
177
- }, null, 2)
178
- }]
179
- };
187
+ return asMcpJson({
188
+ status: "ok",
189
+ code: "refreshed",
190
+ message: "Tokens refreshed from Chrome.",
191
+ user: result.user,
192
+ team: result.team
193
+ });
180
194
  } catch (e) {
181
- return {
182
- content: [{ type: "text", text: `Extracted but invalid: ${e.message}` }],
183
- isError: true
184
- };
195
+ return asMcpJson({
196
+ status: "error",
197
+ code: "auth_failed_after_refresh",
198
+ message: e.message,
199
+ next_action: "Refresh Slack in Chrome and rerun slack_refresh_tokens."
200
+ }, true);
185
201
  }
186
202
  }
187
- return {
188
- content: [{
189
- type: "text",
190
- 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"
191
- }],
192
- isError: true
193
- };
203
+
204
+ const extractionError = getLastExtractionError();
205
+ if (extractionError?.code === "apple_events_javascript_disabled") {
206
+ return asMcpJson({
207
+ status: "error",
208
+ code: extractionError.code,
209
+ message: extractionError.message,
210
+ detail: extractionError.detail,
211
+ next_action: "In Chrome: View > Developer > Allow JavaScript from Apple Events, then retry."
212
+ }, true);
213
+ }
214
+
215
+ return asMcpJson({
216
+ status: "error",
217
+ code: extractionError?.code || "chrome_extraction_failed",
218
+ message: extractionError?.message || "Could not extract tokens from Chrome.",
219
+ detail: extractionError?.detail || "Ensure Chrome is running with a logged-in Slack tab at app.slack.com.",
220
+ next_action: "Open Slack in Chrome and run slack_refresh_tokens again."
221
+ }, true);
194
222
  }
195
223
 
196
224
  /**
@@ -13,8 +13,8 @@ import { loadTokens, saveTokens, extractFromChrome } from "./token-store.js";
13
13
 
14
14
  // ============ Configuration ============
15
15
 
16
- const TOKEN_WARNING_AGE = 6 * 60 * 60 * 1000; // 6 hours
17
- const TOKEN_CRITICAL_AGE = 10 * 60 * 60 * 1000; // 10 hours
16
+ const TOKEN_WARNING_AGE = 10 * 24 * 60 * 60 * 1000; // 10 days
17
+ const TOKEN_CRITICAL_AGE = 13 * 24 * 60 * 60 * 1000; // 13 days
18
18
  const REFRESH_COOLDOWN = 60 * 60 * 1000; // 1 hour between refresh attempts
19
19
  const USER_CACHE_MAX_SIZE = 500;
20
20
  const USER_CACHE_TTL = 60 * 60 * 1000; // 1 hour
@@ -94,19 +94,21 @@ const userCache = new LRUCache(USER_CACHE_MAX_SIZE, USER_CACHE_TTL);
94
94
  */
95
95
  export async function checkTokenHealth(logger = console) {
96
96
  const silentLogger = { error: () => {}, warn: () => {}, log: () => {} };
97
- const creds = loadTokens(false, silentLogger);
97
+ const creds = loadTokens(false, silentLogger, { autoExtract: false });
98
98
 
99
99
  if (!creds) {
100
100
  return { healthy: false, reason: 'no_tokens', message: 'No credentials found' };
101
101
  }
102
102
 
103
- const tokenAge = creds.updatedAt
104
- ? Date.now() - new Date(creds.updatedAt).getTime()
105
- : Infinity;
106
- const ageHours = Math.round(tokenAge / (60 * 60 * 1000) * 10) / 10;
103
+ const updatedAtMs = creds.updatedAt ? new Date(creds.updatedAt).getTime() : Number.NaN;
104
+ const hasKnownAge = Number.isFinite(updatedAtMs);
105
+ const tokenAge = hasKnownAge ? Date.now() - updatedAtMs : null;
106
+ const ageHours = hasKnownAge
107
+ ? Math.round(tokenAge / (60 * 60 * 1000) * 10) / 10
108
+ : null;
107
109
 
108
110
  // Attempt proactive refresh if token is getting old
109
- if (tokenAge > TOKEN_WARNING_AGE && Date.now() - lastRefreshAttempt > REFRESH_COOLDOWN) {
111
+ if (hasKnownAge && tokenAge > TOKEN_WARNING_AGE && Date.now() - lastRefreshAttempt > REFRESH_COOLDOWN) {
110
112
  lastRefreshAttempt = Date.now();
111
113
  logger.error?.(`Token is ${ageHours}h old, attempting proactive refresh...`);
112
114
 
@@ -127,17 +129,19 @@ export async function checkTokenHealth(logger = console) {
127
129
  }
128
130
 
129
131
  return {
130
- healthy: tokenAge < TOKEN_CRITICAL_AGE,
132
+ healthy: !hasKnownAge || tokenAge < TOKEN_CRITICAL_AGE,
131
133
  age_hours: ageHours,
132
- warning: tokenAge > TOKEN_WARNING_AGE,
133
- critical: tokenAge > TOKEN_CRITICAL_AGE,
134
+ warning: hasKnownAge && tokenAge > TOKEN_WARNING_AGE,
135
+ critical: hasKnownAge && tokenAge > TOKEN_CRITICAL_AGE,
134
136
  source: creds.source,
135
137
  updated_at: creds.updatedAt,
136
- message: tokenAge > TOKEN_CRITICAL_AGE
137
- ? 'Token may expire soon - open Slack in Chrome'
138
- : tokenAge > TOKEN_WARNING_AGE
139
- ? 'Token is getting old - will auto-refresh if Slack tab is open'
140
- : 'Token is healthy'
138
+ message: !hasKnownAge
139
+ ? 'Token age unknown (missing timestamp) - auth can still be valid'
140
+ : tokenAge > TOKEN_CRITICAL_AGE
141
+ ? 'Token may expire soon - open Slack in Chrome'
142
+ : tokenAge > TOKEN_WARNING_AGE
143
+ ? 'Token is getting old - will auto-refresh if Slack tab is open'
144
+ : 'Token is healthy'
141
145
  };
142
146
  }
143
147
 
@@ -21,6 +21,7 @@ const IS_MACOS = platform() === 'darwin';
21
21
 
22
22
  // Refresh lock to prevent concurrent extraction attempts
23
23
  let refreshInProgress = null;
24
+ let lastExtractionError = null;
24
25
 
25
26
  // ============ Keychain Storage (macOS only) ============
26
27
 
@@ -62,7 +63,7 @@ export function getFromFile() {
62
63
  return {
63
64
  token: data.SLACK_TOKEN,
64
65
  cookie: data.SLACK_COOKIE,
65
- updatedAt: data.updated_at
66
+ updatedAt: data.updated_at || data.UPDATED_AT || null
66
67
  };
67
68
  } catch (e) {
68
69
  return null;
@@ -110,13 +111,53 @@ const SLACK_TOKEN_PATHS = [
110
111
  `window.boot_data?.api_token`,
111
112
  ];
112
113
 
114
+ function normalizeExtractionError(error) {
115
+ const raw = String(error?.message || error || "");
116
+
117
+ if (raw.includes("Executing JavaScript through AppleScript is turned off")) {
118
+ return {
119
+ code: "apple_events_javascript_disabled",
120
+ message: "Chrome blocked JavaScript execution from Apple Events.",
121
+ detail: "Enable it in Chrome: View > Developer > Allow JavaScript from Apple Events."
122
+ };
123
+ }
124
+
125
+ if (raw.includes("Application isn't running") || raw.includes("Google Chrome got an error")) {
126
+ return {
127
+ code: "chrome_not_ready",
128
+ message: "Chrome is not ready for token extraction.",
129
+ detail: "Open Google Chrome with an active Slack tab at app.slack.com."
130
+ };
131
+ }
132
+
133
+ if (raw.toLowerCase().includes("timed out")) {
134
+ return {
135
+ code: "chrome_extraction_timeout",
136
+ message: "Chrome token extraction timed out.",
137
+ detail: "Ensure Slack is open in Chrome and retry."
138
+ };
139
+ }
140
+
141
+ return {
142
+ code: "chrome_extraction_failed",
143
+ message: "Chrome token extraction failed.",
144
+ detail: raw || "Unknown extraction error."
145
+ };
146
+ }
147
+
113
148
  /**
114
149
  * Extract tokens from Chrome (macOS only, uses AppleScript)
115
150
  * Returns null on non-macOS platforms
116
151
  */
117
152
  function extractFromChromeInternal() {
153
+ lastExtractionError = null;
118
154
  if (!IS_MACOS) {
119
155
  // AppleScript/osascript is macOS-only
156
+ lastExtractionError = {
157
+ code: "unsupported_platform",
158
+ message: "Chrome auto-extraction is only available on macOS.",
159
+ detail: "Use manual token setup on this platform."
160
+ };
120
161
  return null;
121
162
  }
122
163
 
@@ -138,7 +179,14 @@ function extractFromChromeInternal() {
138
179
  encoding: 'utf-8', timeout: 5000
139
180
  }).trim();
140
181
 
141
- if (!cookie || !cookie.startsWith('xoxd-')) return null;
182
+ if (!cookie || !cookie.startsWith('xoxd-')) {
183
+ lastExtractionError = {
184
+ code: "cookie_not_found",
185
+ message: "Could not extract Slack cookie from Chrome.",
186
+ detail: "Ensure a logged-in Slack tab is open at app.slack.com."
187
+ };
188
+ return null;
189
+ }
142
190
 
143
191
  // Try multiple token extraction paths
144
192
  const tokenPathsJS = SLACK_TOKEN_PATHS.map((path, i) =>
@@ -161,10 +209,18 @@ function extractFromChromeInternal() {
161
209
  encoding: 'utf-8', timeout: 5000
162
210
  }).trim();
163
211
 
164
- if (!token || !token.startsWith('xoxc-')) return null;
212
+ if (!token || !token.startsWith('xoxc-')) {
213
+ lastExtractionError = {
214
+ code: "token_not_found",
215
+ message: "Could not extract Slack token from Chrome.",
216
+ detail: "Refresh Slack in Chrome and retry extraction."
217
+ };
218
+ return null;
219
+ }
165
220
 
166
221
  return { token, cookie };
167
222
  } catch (e) {
223
+ lastExtractionError = normalizeExtractionError(e);
168
224
  return null;
169
225
  }
170
226
  }
@@ -188,6 +244,10 @@ export function extractFromChrome() {
188
244
  }
189
245
  }
190
246
 
247
+ export function getLastExtractionError() {
248
+ return lastExtractionError;
249
+ }
250
+
191
251
  /**
192
252
  * Check if auto-refresh is available on this platform
193
253
  */
@@ -197,9 +257,8 @@ export function isAutoRefreshAvailable() {
197
257
 
198
258
  // ============ Main Token Loader ============
199
259
 
200
- export function loadTokens(forceRefresh = false, logger = console) {
201
- // Priority 1: Environment variables
202
- if (!forceRefresh && process.env.SLACK_TOKEN && process.env.SLACK_COOKIE) {
260
+ function getStoredTokens() {
261
+ if (process.env.SLACK_TOKEN && process.env.SLACK_COOKIE) {
203
262
  return {
204
263
  token: process.env.SLACK_TOKEN,
205
264
  cookie: process.env.SLACK_COOKIE,
@@ -207,37 +266,46 @@ export function loadTokens(forceRefresh = false, logger = console) {
207
266
  };
208
267
  }
209
268
 
210
- // Priority 2: Token file
211
- if (!forceRefresh) {
212
- const fileTokens = getFromFile();
213
- if (fileTokens?.token && fileTokens?.cookie) {
214
- return {
215
- token: fileTokens.token,
216
- cookie: fileTokens.cookie,
217
- source: "file",
218
- updatedAt: fileTokens.updatedAt
219
- };
220
- }
269
+ const fileTokens = getFromFile();
270
+ if (fileTokens?.token && fileTokens?.cookie) {
271
+ return {
272
+ token: fileTokens.token,
273
+ cookie: fileTokens.cookie,
274
+ source: "file",
275
+ updatedAt: fileTokens.updatedAt
276
+ };
277
+ }
278
+
279
+ const keychainToken = getFromKeychain("token");
280
+ const keychainCookie = getFromKeychain("cookie");
281
+ if (keychainToken && keychainCookie) {
282
+ return {
283
+ token: keychainToken,
284
+ cookie: keychainCookie,
285
+ source: "keychain"
286
+ };
221
287
  }
222
288
 
223
- // Priority 3: Keychain
289
+ return null;
290
+ }
291
+
292
+ export function loadTokensReadOnly() {
293
+ return getStoredTokens();
294
+ }
295
+
296
+ export function loadTokens(forceRefresh = false, logger = console, options = {}) {
297
+ const { autoExtract = true } = options;
224
298
  if (!forceRefresh) {
225
- const keychainToken = getFromKeychain("token");
226
- const keychainCookie = getFromKeychain("cookie");
227
- if (keychainToken && keychainCookie) {
228
- return {
229
- token: keychainToken,
230
- cookie: keychainCookie,
231
- source: "keychain"
232
- };
233
- }
299
+ const storedTokens = getStoredTokens();
300
+ if (storedTokens) return storedTokens;
234
301
  }
235
302
 
236
- // Priority 4: Chrome auto-extract
237
- logger.error("Attempting Chrome auto-extraction...");
303
+ if (!autoExtract) return null;
304
+
305
+ logger.error?.("Attempting Chrome auto-extraction...");
238
306
  const chromeTokens = extractFromChrome();
239
307
  if (chromeTokens) {
240
- logger.error("Successfully extracted tokens from Chrome!");
308
+ logger.error?.("Successfully extracted tokens from Chrome!");
241
309
  saveTokens(chromeTokens.token, chromeTokens.cookie);
242
310
  return {
243
311
  token: chromeTokens.token,
@@ -246,6 +314,11 @@ export function loadTokens(forceRefresh = false, logger = console) {
246
314
  };
247
315
  }
248
316
 
317
+ if (lastExtractionError?.code === "apple_events_javascript_disabled") {
318
+ logger.error?.(lastExtractionError.message);
319
+ logger.error?.(lastExtractionError.detail);
320
+ }
321
+
249
322
  return null;
250
323
  }
251
324
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jtalk22/slack-mcp",
3
3
  "mcpName": "io.github.jtalk22/slack-mcp-server",
4
- "version": "1.2.2",
4
+ "version": "1.2.4",
5
5
  "description": "Session-based Slack access for Claude - DMs, channels, search, and threads. Local-first with your existing Slack session.",
6
6
  "type": "module",
7
7
  "main": "src/server.js",
@@ -24,7 +24,9 @@
24
24
  "tokens:auto": "node scripts/token-cli.js auto",
25
25
  "tokens:clear": "node scripts/token-cli.js clear",
26
26
  "screenshot": "node scripts/capture-screenshots.js",
27
- "record-demo": "node scripts/record-demo.js"
27
+ "record-demo": "node scripts/record-demo.js",
28
+ "metrics:release-health": "node scripts/collect-release-health.js",
29
+ "metrics:release-health:delta": "node scripts/build-release-health-delta.js"
28
30
  },
29
31
  "keywords": [
30
32
  "mcp",
@@ -33,50 +35,27 @@
33
35
  "slack",
34
36
  "slack-api",
35
37
  "slack-mcp",
36
- "slack-bot",
37
38
  "slack-integration",
38
39
  "claude",
39
40
  "claude-desktop",
40
41
  "claude-code",
41
- "anthropic",
42
+ "cursor",
43
+ "llm",
42
44
  "ai",
43
45
  "ai-assistant",
44
- "ai-agent",
45
- "llm",
46
- "llm-tools",
47
- "chatbot",
48
- "dm",
49
- "direct-messages",
50
- "channels",
51
- "workspace",
52
46
  "automation",
53
47
  "workflow-automation",
54
- "browser-tokens",
55
- "no-oauth",
56
- "cli",
57
48
  "developer-tools",
58
- "devtools",
49
+ "cli",
59
50
  "productivity",
60
51
  "messaging",
61
- "chat",
62
- "model-context-protocol",
63
- "chatgpt",
64
- "cursor",
65
- "open-source",
66
- "openai",
67
- "gpt",
68
- "gemini",
69
- "copilot",
70
- "ai-tools",
71
- "personal-assistant",
72
52
  "message-search",
73
- "slack-integration",
74
- "workspace-automation"
53
+ "direct-messages",
54
+ "channels",
55
+ "workspace",
56
+ "session-based",
57
+ "open-source"
75
58
  ],
76
- "funding": {
77
- "type": "individual",
78
- "url": "https://github.com/sponsors/jtalk22"
79
- },
80
59
  "author": "jtalk22",
81
60
  "license": "MIT",
82
61
  "repository": {