@jtalk22/slack-mcp 4.1.0 → 4.2.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-blue)](https://registry.modelcontextprotocol.io)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- Give your AI agent full Slack access. No app registration, no admin approval, no OAuth. One command, 16 tools, works with any MCP client.
7
+ Give your AI agent full Slack access — and structured workflow output the AI can actually use. No app registration, no admin approval, no OAuth. One command, 21 tools, works with any MCP client.
8
8
 
9
9
  ```bash
10
10
  npx -y @jtalk22/slack-mcp --setup
@@ -24,7 +24,7 @@ Slack's official MCP server requires a registered app, admin approval, and [does
24
24
 
25
25
  This server uses your browser's session tokens instead. If you can see it in Slack, your AI agent can see it too. No app install, no scopes, no admin.
26
26
 
27
- **Stealth Mode:** Session tokens leave zero footprint in your workspace admin panel. No bot user appears, no app install shows up, no audit trail. Your AI agent operates with the same invisibility as your browser tab.
27
+ **Session-token transport:** No bot user appears in the workspace admin panel, no app install shows up, no audit trail entry is created. Your AI agent operates with the same workspace footprint as your browser tab — nothing more, nothing less.
28
28
 
29
29
  ![OAuth vs Chrome DB Decryption](docs/images/diagram-oauth-comparison.svg)
30
30
 
@@ -39,8 +39,28 @@ This server uses your browser's session tokens instead. If you can see it in Sla
39
39
  | Works with Gemini CLI | No | **Yes** |
40
40
  | Works with Codex CLI | No | **Yes** |
41
41
  | Setup time | ~30 min | **~2 min** |
42
- | Tools | Limited | **16** |
43
- | Visible to admins | Yes | **No — Stealth Mode** |
42
+ | Tools | Limited | **21** |
43
+ | Visible to admins | Yes | **No — session-token transport** |
44
+
45
+ ## Workflow Primitives (new in 4.2)
46
+
47
+ Save a workflow profile that binds a `workflow_kind` to channels + priority people + retention + cadence. Stored locally at `~/.slack-mcp-workflows.json`. The hosted brain at [mcp.revasserlabs.com](https://mcp.revasserlabs.com) reads these profiles and returns **structured JSON per workflow_kind** — downstream automation (Linear, Notion, status dashboards) consumes the JSON directly.
48
+
49
+ | `workflow_kind` | Returns (structured JSON) |
50
+ |---|---|
51
+ | `incident_room` | `{incident_summary, timeline, open_risks, owner_gaps, next_actions}` |
52
+ | `exec_brief` | `{summary, decisions, risks, asks, action_items}` |
53
+ | `support_inbox` | `{open_threads, ack_lag, owner_gaps, escalations, next_actions}` |
54
+ | `product_launch_watch` | `{launch_signals, feedback_themes, blockers, metrics, next_actions}` |
55
+ | `custom` | `{summary, highlights, open_questions, next_actions}` |
56
+
57
+ Six prebuilt templates ship with the package:
58
+
59
+ ```bash
60
+ npx -y @jtalk22/slack-mcp --apply-template oncall-handoff --channels C012345,C067890
61
+ ```
62
+
63
+ Available templates: `oncall-handoff`, `support-triage`, `exec-monday`, `sprint-tracker`, `customer-feedback`, `incident-room`. The structural primitives (`slack_workflow_save`, `slack_workflows`) are free forever in OSS; the hosted brain is `$0` to start (no card) and `$9/mo` Pro for unlimited AI tools (scheduled morning catch-up DM rolling out Q2 2026).
44
64
 
45
65
  ## Quick Start per Client
46
66
 
@@ -129,11 +149,18 @@ Or via CLI: `codex mcp add slack -- npx -y @jtalk22/slack-mcp`
129
149
  | `slack_add_reaction` | Add an emoji reaction to a message | **destructive** |
130
150
  | `slack_remove_reaction` | Remove an emoji reaction from a message | **destructive** |
131
151
  | `slack_conversations_mark` | Mark a conversation as read | **destructive** |
152
+ | `slack_workflow_save` | Save a workflow profile (channels, kind, retention, cadence) to `~/.slack-mcp-workflows.json` | local-write |
153
+ | `slack_workflows` | List saved workflow profiles | read-only |
154
+ | `slack_smart_search` | Semantic search across indexed channels — hosted brain | hosted-stub† |
155
+ | `slack_catch_me_up` | AI-summarized digest of unreads + priority threads — hosted brain | hosted-stub† |
156
+ | `slack_triage` | Prioritized action queue across channels — hosted brain | hosted-stub† |
132
157
 
133
- 12 read-only, 4 write-path. All carry [MCP safety annotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#annotations).
158
+ 21 tools total: 12 read-only Slack, 4 write-path Slack, 2 workflow profile primitives (1 local-write, 1 read-only), 3 hosted stubs. All carry [MCP safety annotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#annotations).
134
159
 
135
160
  \* `slack_refresh_tokens` modifies local token file only.
136
161
 
162
+ † Hosted stubs return a structured upgrade payload (`signup_url`, `free_tier_quota`, `pro_value_prop`) — no Slack write occurs from OSS. Activate the brain at [mcp.revasserlabs.com](https://mcp.revasserlabs.com) (free tier, no card).
163
+
137
164
  ## Install
138
165
 
139
166
  **Node.js 20+**
@@ -230,7 +257,15 @@ On macOS, tokens are auto-extracted from Chrome — `env` block is optional.
230
257
  <details>
231
258
  <summary><strong>Claude Web / Remote MCP</strong></summary>
232
259
 
233
- Hosted version with permanent OAuth tokens coming soon. See [mcp.revasserlabs.com](https://mcp.revasserlabs.com) for updates.
260
+ Hosted tiers at [mcp.revasserlabs.com](https://mcp.revasserlabs.com):
261
+
262
+ | Tier | Price | What it owns |
263
+ |------|-------|-------------|
264
+ | Self-host | Free (MIT) | Local stdio, all 21 tools (16 read/write Slack + 2 workflow profile primitives + 3 discoverable upgrade stubs to hosted brain) |
265
+ | Hosted Free | $0 (no card) | Email signup, 1 workspace, 10 smart_search/mo + 3 catch_me_up/mo + 5 triage/day. All 5 workflow profile types. 7-day index retention. |
266
+ | Pro | $9/mo | Unlimited AI tools, **scheduled morning catch-up DM** *(rolling out Q2 2026, 8am workspace tz)*, permanent OAuth, 90-day Vectorize, 2 workspaces |
267
+ | Team | $49/mo flat | Pro + shared workflow profiles + audit log + 24h support + scheduled catch-up to channel + 5 workspaces |
268
+ | Ops | from $199/mo (custom) | SLA, custom retention, SOC2 evidence path, multi-tenant isolation, 10+ workspaces, dedicated workflow tuning |
234
269
 
235
270
  </details>
236
271
 
@@ -270,6 +305,16 @@ Session tokens (`xoxc-` + `xoxd-`) from your browser. If you can see it in Slack
270
305
 
271
306
  Tokens expire. The server notices before you do — proactive health monitoring, automatic refresh on macOS, warnings when tokens age out. File writes are atomic (temp file → chmod → rename) to prevent corruption. Concurrent refresh attempts are mutex-locked.
272
307
 
308
+ ## What's New in 4.2.0
309
+
310
+ - **Workflow primitives** — `slack_workflow_save` + `slack_workflows` bind a `workflow_kind` (`incident_room`, `exec_brief`, `support_inbox`, `product_launch_watch`, `custom`) to channels, priority people, retention, and cadence. The hosted brain returns structured JSON per kind — `incident_room` returns `{incident_summary, timeline, open_risks, owner_gaps, next_actions}`, `exec_brief` returns `{summary, decisions, risks, asks, action_items}`. Downstream automation (Linear, Notion, dashboards) consumes the JSON directly.
311
+ - **Discoverable upgrade stubs** — `slack_smart_search`, `slack_catch_me_up`, `slack_triage` appear in OSS as upgrade payloads pointing at the hosted brain. Response shape is `{signup_url, free_tier_quota, pro_value_prop}` — no interruptions, the AI routes the user cleanly.
312
+ - **Six prebuilt templates** — apply with `npx -y @jtalk22/slack-mcp --apply-template <name> --channels C012,C034`. Names: `oncall-handoff`, `support-triage`, `exec-monday`, `sprint-tracker`, `customer-feedback`, `incident-room`. Read them, fork them, edit them — they're JSON profiles.
313
+ - **Setup wizard hosted bridge** — six in-wizard moments surface the hosted free tier (no card) where it matches the user's pain. Stays out of the way otherwise.
314
+ - **Prior reliability fixes carried forward** — LevelDB token extraction, multi-profile enumeration, and explicit SIGTERM/SIGINT/SIGHUP/stdin shutdown handlers ship in 4.2.0 too.
315
+
316
+ Full release notes on [GitHub releases/latest](https://github.com/jtalk22/slack-mcp-server/releases/latest).
317
+
273
318
  ## Hosted HTTP Mode
274
319
 
275
320
  For remote MCP endpoints (Cloudflare Worker, VPS, etc.):
@@ -326,4 +371,4 @@ Not affiliated with Slack Technologies, Inc. Uses browser session credentials
326
371
 
327
372
  ---
328
373
 
329
- Hosted version with semantic search, AI summaries, and permanent OAuth coming soon at [mcp.revasserlabs.com](https://mcp.revasserlabs.com)
374
+ Hosted version live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com): Free tier (no card), $9/mo Pro, $49/mo Team flat, Ops from $199/mo. Hosted owns the AI brain (smart_search, catch_me_up, triage), the scheduled morning catch-up DM at 8am workspace time *(rolling out Q2 2026)*, permanent OAuth (no 2-week token rotation), 90-day Vectorize retention, and shared workflow profiles. The OSS package owns local stdio + all 16 read/write Slack tools + workflow profile primitives (slack_workflow_save, slack_workflows). The 3 paid stubs (slack_smart_search, slack_catch_me_up, slack_triage) appear in OSS as discoverable upgrade prompts.
@@ -0,0 +1,56 @@
1
+ # Deployment Modes
2
+
3
+ Use this guide to choose the right operating mode before rollout.
4
+
5
+ ## Quick Chooser
6
+
7
+ - Choose `stdio` for personal self-hosted use in Claude Desktop/Claude Code.
8
+ - Choose local `web` for browser workflows and manual Slack browsing.
9
+ - Choose hosted HTTP only when you need remote execution and can handle token operations.
10
+ - Choose Smithery/Worker only when your consumers require registry-hosted MCP transport.
11
+ - A managed **Hosted** version is live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com) — Free tier (no card), Pro $9/mo, Team $49/mo flat, Ops from $199/mo. See [pricing](https://mcp.revasserlabs.com/pricing).
12
+
13
+ ## Mode Matrix
14
+
15
+ | Mode | Start Command | Best For | Auth Material | Exposure | Notes |
16
+ |------|---------------|----------|---------------|----------|-------|
17
+ | Local MCP (`stdio`) | `npx -y @jtalk22/slack-mcp` | Individual daily usage in Claude | `SLACK_TOKEN` + `SLACK_COOKIE` via token file/env | Local process | Lowest ops burden. Free. 21 tools (16 read/write + 2 workflow profile primitives + 3 discoverable upgrade stubs to hosted). |
18
+ | Local Web UI (`web`) | `npx -y @jtalk22/slack-mcp web` | Browser-first usage, manual search/send | Same as above + generated API key | `localhost` by default | Useful when MCP is not available |
19
+ | Hosted MCP (`http`) | `node src/server-http.js` | Controlled hosted integration | Env-injected Slack token/cookie + HTTP bearer token | Remote endpoint | `/mcp` is bearer-protected by default; configure CORS allowlist |
20
+ | Smithery/Worker | `wrangler deploy` + Smithery publish flow | Registry distribution for hosted consumers | Query/env token handoff | Remote endpoint | Keep worker version parity with npm release |
21
+
22
+ ## Team Deployment Guidance
23
+
24
+ If you are deploying for more than one operator:
25
+
26
+ 1. Start with one maintainer on local `stdio`.
27
+ 2. Document token lifecycle and rotation ownership.
28
+ 3. Define support window and incident contact before enabling hosted mode.
29
+ 4. Validate `/health` and MCP initialize responses on every release.
30
+
31
+ ## Release Checklist by Mode
32
+
33
+ ### Local `stdio`
34
+
35
+ 1. `npx -y @jtalk22/slack-mcp --status`
36
+ 2. `npx -y @jtalk22/slack-mcp --help`
37
+ 3. Confirm tool list in Claude client.
38
+
39
+ ### Local `web`
40
+
41
+ 1. `npx -y @jtalk22/slack-mcp web`
42
+ 2. Verify API key generation at `~/.slack-mcp-api-key`.
43
+ 3. Verify `/health`, `/conversations`, and `/search` endpoints.
44
+
45
+ ### Hosted (`http` or Worker)
46
+
47
+ 1. Verify `version` parity across `package.json`, server metadata, and health responses.
48
+ 2. Verify HTTP auth behavior:
49
+ - missing `SLACK_MCP_HTTP_AUTH_TOKEN` returns `503`
50
+ - bad bearer token returns `401`
51
+ - valid bearer token succeeds
52
+ 3. Verify CORS behavior:
53
+ - denied by default
54
+ - allowed origins work when listed in `SLACK_MCP_HTTP_ALLOWED_ORIGINS`
55
+ 4. Confirm `slack_get_thread`, `slack_search_messages`, and `slack_users_info` behavior.
56
+ 5. Confirm token handling mode (ephemeral vs env persistence) is documented.
@@ -31,7 +31,9 @@ If `--version` fails here, the issue is install/runtime path, not Slack credenti
31
31
 
32
32
  ## Hosted Version
33
33
 
34
- A hosted version with permanent OAuth tokens, semantic search, and AI summaries is coming soon at [mcp.revasserlabs.com](https://mcp.revasserlabs.com). The current release is self-hosted only.
34
+ The hosted version is live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com). Free tier (no card) ships 10 smart_search/mo + 3 catch_me_up/mo + 5 triage/day + all 5 workflow profile types. Pro at $9/mo unlocks unlimited AI tools, the scheduled morning catch-up DM at 8am workspace time *(rolling out Q2 2026)*, permanent OAuth (no 2-week token rotation), and 90-day Vectorize retention. Team at $49/mo flat covers 5 workspaces with shared workflow profiles and audit log. Ops engagement starts at $199/mo (custom) for 10+ workspace organizations with SLA, custom retention, SOC2 evidence, or multi-tenant isolation.
35
+
36
+ The OSS package keeps the local-machine path. The hosted version adds the AI brain (smart_search, catch_me_up, triage) — these tools also appear in the OSS package as discoverable upgrade stubs that point at the hosted signup.
35
37
 
36
38
  ---
37
39
 
package/lib/handlers.js CHANGED
@@ -16,6 +16,11 @@ import {
16
16
  getLastExtractionError
17
17
  } from "./token-store.js";
18
18
  import { slackAPI, resolveUser, formatTimestamp, sleep, checkTokenHealth, getUserCacheStats } from "./slack-client.js";
19
+ import {
20
+ saveProfile as workflowSaveProfile,
21
+ listProfiles as workflowListProfiles,
22
+ ALLOWED_WORKFLOW_KINDS_LIST,
23
+ } from "./workflow-store.js";
19
24
 
20
25
  // ============ Utilities ============
21
26
 
@@ -773,3 +778,90 @@ export async function handleUsersSearch(args) {
773
778
  users: allUsers.slice(0, limit)
774
779
  });
775
780
  }
781
+
782
+ // ============ Workflow Profile Primitives (OSS) ============
783
+
784
+ /**
785
+ * Save (or update) a workflow profile to ~/.slack-mcp-workflows.json
786
+ * Profile binds a workflow_kind to channels + priority_people + retention + cadence.
787
+ */
788
+ export async function handleWorkflowSave(args) {
789
+ const safeArgs = args || {};
790
+ const result = workflowSaveProfile({
791
+ profile_name: safeArgs.profile_name,
792
+ workflow_kind: safeArgs.workflow_kind,
793
+ channels: safeArgs.channels,
794
+ priority_people: safeArgs.priority_people,
795
+ retention_mode: safeArgs.retention_mode,
796
+ summary_cadence: safeArgs.summary_cadence,
797
+ });
798
+ if (!result.ok) {
799
+ return asMcpJson({ error: "invalid_workflow_profile", errors: result.errors }, true);
800
+ }
801
+ return asMcpJson({
802
+ ok: true,
803
+ profile_name: result.profile_name,
804
+ profile: result.profile,
805
+ note: "Profile saved locally. Hosted free tier syncs profiles automatically when you connect your account; OSS-only mode keeps them on your machine.",
806
+ });
807
+ }
808
+
809
+ /**
810
+ * List saved workflow profiles, optionally filtered by workflow_kind.
811
+ */
812
+ export async function handleWorkflows(args) {
813
+ const result = workflowListProfiles({ workflow_kind: args && args.workflow_kind });
814
+ if (!result.ok) {
815
+ return asMcpJson({ error: "invalid_workflow_filter", errors: result.errors }, true);
816
+ }
817
+ return asMcpJson({
818
+ ok: true,
819
+ count: result.profiles.length,
820
+ workflow_kinds: ALLOWED_WORKFLOW_KINDS_LIST,
821
+ profiles: result.profiles,
822
+ });
823
+ }
824
+
825
+ // ============ Hosted-Only AI Tool Stubs ============
826
+ // These three tools appear in the OSS package's MCP tool list so users
827
+ // discover them. The handlers return a structured upgrade message —
828
+ // actual execution happens on the hosted worker (mcp.revasserlabs.com).
829
+
830
+ const HOSTED_UPGRADE_PAYLOAD = {
831
+ error: "tool_requires_hosted",
832
+ message: "This tool needs hosted mode (Vectorize + Workers AI). Get free monthly credits at mcp.revasserlabs.com — no card required.",
833
+ signup_url: "https://mcp.revasserlabs.com/signup",
834
+ upgrade_url: "https://mcp.revasserlabs.com/pricing",
835
+ free_tier_quota: "10 smart_search + 3 catch_me_up per month, 5 triage per day",
836
+ pro_value_prop: "Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM at 8am workspace time rolling out Q2 2026).",
837
+ };
838
+
839
+ export async function handleSmartSearch(args) {
840
+ return asMcpJson(
841
+ {
842
+ ...HOSTED_UPGRADE_PAYLOAD,
843
+ requested: { tool: "slack_smart_search", args: args || {} },
844
+ },
845
+ true
846
+ );
847
+ }
848
+
849
+ export async function handleCatchMeUp(args) {
850
+ return asMcpJson(
851
+ {
852
+ ...HOSTED_UPGRADE_PAYLOAD,
853
+ requested: { tool: "slack_catch_me_up", args: args || {} },
854
+ },
855
+ true
856
+ );
857
+ }
858
+
859
+ export async function handleTriage(args) {
860
+ return asMcpJson(
861
+ {
862
+ ...HOSTED_UPGRADE_PAYLOAD,
863
+ requested: { tool: "slack_triage", args: args || {} },
864
+ },
865
+ true
866
+ );
867
+ }
@@ -18,5 +18,5 @@ export const PUBLIC_METADATA = Object.freeze({
18
18
  cloudSupportUrl: "https://mcp.revasserlabs.com/support",
19
19
  cloudStatusUrl: "https://mcp.revasserlabs.com/status",
20
20
  supportEmail: "support@revasserlabs.com",
21
- selfHostedToolCount: 16,
21
+ selfHostedToolCount: 21,
22
22
  });
@@ -63,7 +63,7 @@ function shareLinks() {
63
63
  }
64
64
 
65
65
  function shareNote() {
66
- return `<strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives ${PUBLIC_METADATA.selfHostedToolCount} tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf. Hosted version coming soon at <a href="${PUBLIC_METADATA.canonicalSiteUrl}">mcp.revasserlabs.com</a>.`;
66
+ return `<strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives ${PUBLIC_METADATA.selfHostedToolCount} tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf. Hosted free tier (no card) live at <a href="${PUBLIC_METADATA.canonicalSiteUrl}">mcp.revasserlabs.com</a> — Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM rolling out Q2 2026).`;
67
67
  }
68
68
 
69
69
  function demoLinks() {
@@ -75,7 +75,7 @@ function demoLinks() {
75
75
  }
76
76
 
77
77
  function demoNote() {
78
- return `Self-host free for ${PUBLIC_METADATA.selfHostedToolCount} tools with session-based auth. Works with Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf, and any other MCP client. No OAuth app, no admin approval. Hosted version coming soon at <a href="${PUBLIC_METADATA.canonicalSiteUrl}" target="_blank" rel="noopener noreferrer">mcp.revasserlabs.com</a>.`;
78
+ return `Self-host free for ${PUBLIC_METADATA.selfHostedToolCount} tools with session-based auth. Works with Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf, and any other MCP client. No OAuth app, no admin approval. Hosted free tier (no card) live at <a href="${PUBLIC_METADATA.canonicalSiteUrl}" target="_blank" rel="noopener noreferrer">mcp.revasserlabs.com</a> — Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM rolling out Q2 2026).`;
79
79
  }
80
80
 
81
81
  function demoFooterLinks() {
@@ -113,8 +113,8 @@ function commonTokens() {
113
113
  SELF_HOSTED_TOOL_COUNT: String(PUBLIC_METADATA.selfHostedToolCount),
114
114
  CLOUD_MANAGED_TOOL_COUNT: "15",
115
115
  TEAM_AI_WORKFLOW_COUNT: "3",
116
- CLOUD_SOLO_PRICE: "coming soon",
117
- CLOUD_TEAM_PRICE: "coming soon",
116
+ CLOUD_SOLO_PRICE: "$9/mo",
117
+ CLOUD_TEAM_PRICE: "$49/mo",
118
118
  CLOUD_TURNKEY_LAUNCH_PRICE: "contact us",
119
119
  CLOUD_MANAGED_RELIABILITY_PRICE: "contact us",
120
120
  SUPPORT_EMAIL: PUBLIC_METADATA.supportEmail,
@@ -9,12 +9,19 @@
9
9
  * - Proactive token health checking
10
10
  */
11
11
 
12
- import { loadTokens, saveTokens, extractFromChrome } from "./token-store.js";
12
+ import {
13
+ loadTokens,
14
+ saveTokens,
15
+ extractFromChrome,
16
+ getLastExtractionError,
17
+ saveAutoHealTelemetry,
18
+ } from "./token-store.js";
13
19
 
14
20
  // ============ Configuration ============
15
21
 
16
22
  const TOKEN_WARNING_AGE = 10 * 24 * 60 * 60 * 1000; // 10 days
17
23
  const TOKEN_CRITICAL_AGE = 13 * 24 * 60 * 60 * 1000; // 13 days
24
+ const STUCK_THRESHOLD_MS = 24 * 60 * 60 * 1000; // Escalate to 'stuck' after 24h of repeated auto-heal failures
18
25
  const REFRESH_COOLDOWN = 60 * 60 * 1000; // 1 hour between refresh attempts
19
26
  const USER_CACHE_MAX_SIZE = 500;
20
27
  const USER_CACHE_TTL = 60 * 60 * 1000; // 1 hour
@@ -113,14 +120,21 @@ export async function checkTokenHealth(logger = console) {
113
120
  ? Math.round(tokenAge / (60 * 60 * 1000) * 10) / 10
114
121
  : null;
115
122
 
123
+ // Read auto-heal telemetry (only populated when source is "file")
124
+ let lastAutoHealAttempt = creds.lastAutoHealAttempt || null;
125
+ let lastAutoHealError = creds.lastAutoHealError || null;
126
+ let stuckSince = creds.stuckSince || null;
127
+
116
128
  // Attempt proactive refresh if token is getting old
117
129
  if (hasKnownAge && tokenAge > TOKEN_WARNING_AGE && Date.now() - lastRefreshAttempt > REFRESH_COOLDOWN) {
118
130
  lastRefreshAttempt = Date.now();
131
+ const attemptAt = new Date().toISOString();
119
132
  logger.error?.(`Token is ${ageHours}h old, attempting proactive refresh...`);
120
133
 
121
134
  const newTokens = extractFromChrome();
122
135
  if (newTokens) {
123
136
  saveTokens(newTokens.token, newTokens.cookie);
137
+ saveAutoHealTelemetry({ attemptAt, error: null });
124
138
  logger.error?.('Proactively refreshed tokens from Chrome');
125
139
  return {
126
140
  healthy: true,
@@ -129,35 +143,59 @@ export async function checkTokenHealth(logger = console) {
129
143
  age_known: true,
130
144
  age_state: 'fresh',
131
145
  source: 'chrome-auto',
146
+ last_auto_heal_attempt: attemptAt,
147
+ last_auto_heal_error: null,
148
+ stuck_since: null,
132
149
  message: 'Tokens refreshed successfully'
133
150
  };
134
151
  } else {
135
- logger.error?.('Could not refresh from Chrome (is Slack tab open?)');
152
+ const extractionError = getLastExtractionError();
153
+ const errorCode = extractionError?.code || 'chrome_extraction_failed';
154
+ saveAutoHealTelemetry({ attemptAt, error: errorCode });
155
+ lastAutoHealAttempt = attemptAt;
156
+ if (lastAutoHealError !== errorCode) {
157
+ stuckSince = attemptAt;
158
+ }
159
+ lastAutoHealError = errorCode;
160
+ logger.error?.(`Could not refresh from Chrome: ${extractionError?.message || 'unknown error'}`);
136
161
  }
137
162
  }
138
163
 
164
+ const stuckSinceMs = stuckSince ? new Date(stuckSince).getTime() : Number.NaN;
165
+ const isStuck = Number.isFinite(stuckSinceMs)
166
+ && (Date.now() - stuckSinceMs) > STUCK_THRESHOLD_MS
167
+ && !!lastAutoHealError;
168
+
139
169
  return {
140
170
  healthy: !hasKnownAge || tokenAge < TOKEN_CRITICAL_AGE,
141
171
  age_hours: ageHours,
142
172
  age_known: hasKnownAge,
143
- age_state: !hasKnownAge
144
- ? 'unknown'
145
- : tokenAge > TOKEN_CRITICAL_AGE
146
- ? 'critical'
147
- : tokenAge > TOKEN_WARNING_AGE
148
- ? 'warning'
149
- : 'healthy',
173
+ age_state: isStuck
174
+ ? 'stuck'
175
+ : !hasKnownAge
176
+ ? 'unknown'
177
+ : tokenAge > TOKEN_CRITICAL_AGE
178
+ ? 'critical'
179
+ : tokenAge > TOKEN_WARNING_AGE
180
+ ? 'warning'
181
+ : 'healthy',
150
182
  warning: hasKnownAge && tokenAge > TOKEN_WARNING_AGE,
151
183
  critical: hasKnownAge && tokenAge > TOKEN_CRITICAL_AGE,
184
+ stuck: isStuck,
152
185
  source: creds.source,
153
186
  updated_at: creds.updatedAt,
154
- message: !hasKnownAge
155
- ? 'Token age unknown (missing timestamp) - auth can still be valid'
156
- : tokenAge > TOKEN_CRITICAL_AGE
157
- ? 'Token may expire soon - open Slack in Chrome'
158
- : tokenAge > TOKEN_WARNING_AGE
159
- ? 'Token is getting old - will auto-refresh if Slack tab is open'
160
- : 'Token is healthy'
187
+ last_auto_heal_attempt: lastAutoHealAttempt,
188
+ last_auto_heal_error: lastAutoHealError,
189
+ stuck_since: stuckSince,
190
+ message: isStuck
191
+ ? `Auto-heal has been failing since ${stuckSince} (last error: ${lastAutoHealError}). Open Chrome > View > Developer > Allow JavaScript from Apple Events, then run npm run tokens:auto.`
192
+ : !hasKnownAge
193
+ ? 'Token age unknown (missing timestamp) - auth can still be valid'
194
+ : tokenAge > TOKEN_CRITICAL_AGE
195
+ ? 'Token may expire soon - open Slack in Chrome'
196
+ : tokenAge > TOKEN_WARNING_AGE
197
+ ? 'Token is getting old - will auto-refresh if Slack tab is open'
198
+ : 'Token is healthy'
161
199
  };
162
200
  }
163
201
 
@@ -250,13 +288,28 @@ export async function slackAPI(method, params = {}, options = {}) {
250
288
  // Handle auth errors with auto-retry
251
289
  if ((data.error === "invalid_auth" || data.error === "token_expired") && retryOnAuthFail) {
252
290
  logger.error?.("Token expired, attempting Chrome auto-extraction...");
291
+ const attemptAt = new Date().toISOString();
253
292
  const chromeTokens = extractFromChrome();
254
293
  if (chromeTokens) {
255
294
  saveTokens(chromeTokens.token, chromeTokens.cookie);
295
+ saveAutoHealTelemetry({ attemptAt, error: null });
256
296
  // Retry the request
257
297
  return slackAPI(method, params, { ...options, retryOnAuthFail: false });
258
298
  }
259
- throw new Error(`${data.error} - Tokens expired. Open Slack in Chrome and use slack_refresh_tokens.`);
299
+ const extractionError = getLastExtractionError() || {
300
+ code: 'chrome_extraction_failed',
301
+ message: 'Auto-heal attempted but no structured error surfaced.',
302
+ detail: null
303
+ };
304
+ saveAutoHealTelemetry({ attemptAt, error: extractionError.code });
305
+ const err = new Error(
306
+ `Slack auth failed (${data.error}) and auto-heal could not refresh tokens: ${extractionError.message}`
307
+ );
308
+ err.code = 'token_auth_failed';
309
+ err.slack_error = data.error;
310
+ err.extraction_error = extractionError;
311
+ err.next_action = 'Open http://localhost:3000 and click Refresh, OR run `npm run tokens:auto` with Slack open in Chrome, OR check Chrome > View > Developer > Allow JavaScript from Apple Events.';
312
+ throw err;
260
313
  }
261
314
  throw new Error(data.error || "Slack API error");
262
315
  }