@jtalk22/slack-mcp 1.2.3 → 2.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.
Files changed (39) hide show
  1. package/README.md +23 -7
  2. package/docs/API.md +11 -4
  3. package/docs/COMMUNICATION-STYLE.md +1 -0
  4. package/docs/COMPATIBILITY.md +19 -0
  5. package/docs/HN-LAUNCH.md +32 -32
  6. package/docs/INDEX.md +10 -1
  7. package/docs/INSTALL-PROOF.md +18 -0
  8. package/docs/LAUNCH-COPY-v2.0.0.md +59 -0
  9. package/docs/LAUNCH-MATRIX.md +20 -0
  10. package/docs/LAUNCH-OPS.md +70 -0
  11. package/docs/RELEASE-HEALTH.md +81 -0
  12. package/docs/SETUP.md +2 -0
  13. package/docs/TROUBLESHOOTING.md +6 -4
  14. package/docs/WEB-API.md +12 -1
  15. package/docs/release-health/2026-02-25.md +33 -0
  16. package/docs/release-health/2026-02-26.md +33 -0
  17. package/docs/release-health/24h-delta.md +21 -0
  18. package/docs/release-health/24h-end.md +33 -0
  19. package/docs/release-health/24h-start.md +33 -0
  20. package/docs/release-health/latest.md +33 -0
  21. package/docs/release-health/launch-log-template.md +21 -0
  22. package/docs/release-health/version-parity.md +21 -0
  23. package/lib/handlers.js +121 -85
  24. package/lib/slack-client.js +37 -17
  25. package/lib/token-store.js +103 -30
  26. package/package.json +26 -39
  27. package/public/demo-claude.html +4 -4
  28. package/public/demo-video.html +2 -2
  29. package/public/demo.html +2 -2
  30. package/scripts/build-release-health-delta.js +201 -0
  31. package/scripts/check-public-language.sh +25 -0
  32. package/scripts/check-version-parity.js +162 -0
  33. package/scripts/collect-release-health.js +150 -0
  34. package/scripts/setup-wizard.js +35 -9
  35. package/scripts/token-cli.js +6 -4
  36. package/scripts/verify-install-flow.js +107 -2
  37. package/src/server-http.js +26 -4
  38. package/src/server.js +23 -7
  39. package/src/web-server.js +61 -23
@@ -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,16 +1,16 @@
1
1
  {
2
2
  "name": "@jtalk22/slack-mcp",
3
3
  "mcpName": "io.github.jtalk22/slack-mcp-server",
4
- "version": "1.2.3",
4
+ "version": "2.0.0",
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",
8
8
  "bin": {
9
- "slack-mcp": "./src/cli.js",
10
- "slack-mcp-server": "./src/server.js",
11
- "slack-mcp-http": "./src/server-http.js",
12
- "slack-mcp-web": "./src/web-server.js",
13
- "slack-mcp-setup": "./scripts/setup-wizard.js"
9
+ "slack-mcp": "src/cli.js",
10
+ "slack-mcp-server": "src/server.js",
11
+ "slack-mcp-http": "src/server-http.js",
12
+ "slack-mcp-web": "src/web-server.js",
13
+ "slack-mcp-setup": "scripts/setup-wizard.js"
14
14
  },
15
15
  "scripts": {
16
16
  "start": "node src/server.js",
@@ -24,7 +24,10 @@
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",
30
+ "verify:version-parity": "node scripts/check-version-parity.js"
28
31
  },
29
32
  "keywords": [
30
33
  "mcp",
@@ -33,55 +36,39 @@
33
36
  "slack",
34
37
  "slack-api",
35
38
  "slack-mcp",
36
- "slack-bot",
37
39
  "slack-integration",
38
40
  "claude",
39
41
  "claude-desktop",
40
42
  "claude-code",
41
- "anthropic",
43
+ "cursor",
44
+ "llm",
42
45
  "ai",
43
46
  "ai-assistant",
44
- "ai-agent",
45
- "llm",
46
- "llm-tools",
47
- "chatbot",
48
- "dm",
49
- "direct-messages",
50
- "channels",
51
- "workspace",
52
47
  "automation",
53
48
  "workflow-automation",
54
- "browser-tokens",
55
- "no-oauth",
56
- "cli",
57
49
  "developer-tools",
58
- "devtools",
50
+ "cli",
59
51
  "productivity",
60
52
  "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
53
  "message-search",
73
- "slack-integration",
74
- "workspace-automation"
54
+ "direct-messages",
55
+ "channels",
56
+ "workspace",
57
+ "session-based",
58
+ "session-mirroring",
59
+ "slack-dm",
60
+ "slack-threads",
61
+ "workspace-search",
62
+ "mcp-tools",
63
+ "mcp-stdio",
64
+ "mcp-http",
65
+ "open-source"
75
66
  ],
76
- "funding": {
77
- "type": "individual",
78
- "url": "https://github.com/sponsors/jtalk22"
79
- },
80
67
  "author": "jtalk22",
81
68
  "license": "MIT",
82
69
  "repository": {
83
70
  "type": "git",
84
- "url": "https://github.com/jtalk22/slack-mcp-server.git"
71
+ "url": "git+https://github.com/jtalk22/slack-mcp-server.git"
85
72
  },
86
73
  "homepage": "https://jtalk22.github.io/slack-mcp-server/public/demo.html",
87
74
  "bugs": {
@@ -1152,16 +1152,16 @@
1152
1152
  <div class="links">
1153
1153
  <a href="https://www.npmjs.com/package/@jtalk22/slack-mcp" target="_blank" rel="noopener noreferrer">Install</a>
1154
1154
  <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/SETUP.md" target="_blank" rel="noopener noreferrer">Setup Guide</a>
1155
- <a href="https://github.com/jtalk22/slack-mcp-server#30-second-proof" target="_blank" rel="noopener noreferrer">30-Second Proof</a>
1155
+ <a href="https://github.com/jtalk22/slack-mcp-server#30-second-compatibility-check" target="_blank" rel="noopener noreferrer">30-Second Check</a>
1156
1156
  </div>
1157
1157
  <div class="note">
1158
- Free local-first path with optional team rollout guidance in <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/DEPLOYMENT-MODES.md" target="_blank" rel="noopener noreferrer">Deployment Modes</a>.
1158
+ Free local-first path with team rollout references in <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/DEPLOYMENT-MODES.md" target="_blank" rel="noopener noreferrer">Deployment Modes</a>.
1159
1159
  </div>
1160
1160
  </div>
1161
1161
  <header class="page-header">
1162
1162
  <h1>
1163
1163
  <span>Slack MCP Server</span>
1164
- <span class="badge">🔧 MCP Demo v1.2.3</span>
1164
+ <span class="badge">🔧 MCP Demo v2.0.0</span>
1165
1165
  </h1>
1166
1166
  <p>See how Claude uses MCP tools to access your Slack workspace</p>
1167
1167
  </header>
@@ -1233,7 +1233,7 @@
1233
1233
  <div class="title-logo">💬</div>
1234
1234
  <h1>Slack MCP Server</h1>
1235
1235
  <p class="title-tagline">Full Slack access for Claude Desktop</p>
1236
- <p class="title-version">v1.2.3 • @jtalk22</p>
1236
+ <p class="title-version">v2.0.0 • @jtalk22</p>
1237
1237
  </div>
1238
1238
 
1239
1239
  <!-- Scenario Caption Overlay -->
@@ -147,10 +147,10 @@
147
147
  <div class="links">
148
148
  <a href="https://www.npmjs.com/package/@jtalk22/slack-mcp" target="_blank" rel="noopener noreferrer">Install</a>
149
149
  <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/SETUP.md" target="_blank" rel="noopener noreferrer">Setup Guide</a>
150
- <a href="https://github.com/jtalk22/slack-mcp-server#30-second-proof" target="_blank" rel="noopener noreferrer">30-Second Proof</a>
150
+ <a href="https://github.com/jtalk22/slack-mcp-server#30-second-compatibility-check" target="_blank" rel="noopener noreferrer">30-Second Check</a>
151
151
  </div>
152
152
  <div class="note">
153
- Free local-first path with optional team rollout guidance in <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/DEPLOYMENT-MODES.md" target="_blank" rel="noopener noreferrer">Deployment Modes</a>.
153
+ Free local-first path with team rollout references in <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/DEPLOYMENT-MODES.md" target="_blank" rel="noopener noreferrer">Deployment Modes</a>.
154
154
  </div>
155
155
  </div>
156
156
 
package/public/demo.html CHANGED
@@ -637,10 +637,10 @@
637
637
  <div class="cta-links">
638
638
  <a href="https://www.npmjs.com/package/@jtalk22/slack-mcp" target="_blank" rel="noopener noreferrer">Install</a>
639
639
  <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/SETUP.md" target="_blank" rel="noopener noreferrer">Setup Guide</a>
640
- <a href="https://github.com/jtalk22/slack-mcp-server#30-second-proof" target="_blank" rel="noopener noreferrer">30-Second Proof</a>
640
+ <a href="https://github.com/jtalk22/slack-mcp-server#30-second-compatibility-check" target="_blank" rel="noopener noreferrer">30-Second Check</a>
641
641
  </div>
642
642
  <div class="cta-note">
643
- Free local-first path with optional team rollout guidance in <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/DEPLOYMENT-MODES.md" target="_blank" rel="noopener noreferrer">Deployment Modes</a>.
643
+ Free local-first path with team rollout references in <a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/DEPLOYMENT-MODES.md" target="_blank" rel="noopener noreferrer">Deployment Modes</a>.
644
644
  </div>
645
645
  </div>
646
646
 
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, relative, resolve } from "node:path";
5
+
6
+ const METRICS = [
7
+ "npm downloads (last week)",
8
+ "npm downloads (last month)",
9
+ "stars",
10
+ "forks",
11
+ "open issues",
12
+ "14d views",
13
+ "14d unique visitors",
14
+ "14d clones",
15
+ "14d unique cloners",
16
+ "deployment-intake submissions (all-time)",
17
+ ];
18
+
19
+ const DEFAULT_AFTER = resolve("docs", "release-health", "latest.md");
20
+ const DEFAULT_OUT = resolve("docs", "release-health", "automation-delta.md");
21
+ const TARGETS = {
22
+ "npm downloads (last week)": { operator: ">=", value: 180 },
23
+ "deployment-intake submissions (all-time)": { operator: ">=", value: 2 },
24
+ };
25
+
26
+ function escapeRegex(input) {
27
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
+ }
29
+
30
+ function parseArgs(argv) {
31
+ const args = { before: null, after: DEFAULT_AFTER, out: DEFAULT_OUT };
32
+ for (let i = 0; i < argv.length; i += 1) {
33
+ const arg = argv[i];
34
+ if (arg === "--before") {
35
+ if (!argv[i + 1]) throw new Error("Missing value for --before");
36
+ args.before = argv[i + 1];
37
+ i += 1;
38
+ continue;
39
+ }
40
+ if (arg === "--after") {
41
+ if (!argv[i + 1]) throw new Error("Missing value for --after");
42
+ args.after = argv[i + 1];
43
+ i += 1;
44
+ continue;
45
+ }
46
+ if (arg === "--out") {
47
+ if (!argv[i + 1]) throw new Error("Missing value for --out");
48
+ args.out = argv[i + 1];
49
+ i += 1;
50
+ continue;
51
+ }
52
+ throw new Error(`Unknown argument: ${arg}`);
53
+ }
54
+ return args;
55
+ }
56
+
57
+ function readSnapshot(path) {
58
+ if (!path || !existsSync(path)) {
59
+ return { path, metrics: new Map(), generatedAt: null };
60
+ }
61
+
62
+ const content = readFileSync(path, "utf8");
63
+ const generatedAtMatch = content.match(/^- Generated:\s+(.+)$/m);
64
+ const metrics = new Map();
65
+
66
+ for (const label of METRICS) {
67
+ const metricRegex = new RegExp(`^- ${escapeRegex(label)}:\\s+(.+)$`, "m");
68
+ const match = content.match(metricRegex);
69
+ if (!match) continue;
70
+
71
+ const raw = match[1].trim();
72
+ if (raw.toLowerCase() === "n/a") {
73
+ metrics.set(label, null);
74
+ continue;
75
+ }
76
+
77
+ const parsed = Number(raw.replace(/,/g, ""));
78
+ metrics.set(label, Number.isFinite(parsed) ? parsed : null);
79
+ }
80
+
81
+ return {
82
+ path,
83
+ metrics,
84
+ generatedAt: generatedAtMatch ? generatedAtMatch[1].trim() : null,
85
+ };
86
+ }
87
+
88
+ function formatValue(value) {
89
+ return value === null || value === undefined ? "n/a" : `${value}`;
90
+ }
91
+
92
+ function formatDelta(beforeValue, afterValue) {
93
+ if (!Number.isFinite(beforeValue) || !Number.isFinite(afterValue)) {
94
+ return "n/a";
95
+ }
96
+ const delta = afterValue - beforeValue;
97
+ if (delta > 0) return `+${delta}`;
98
+ return `${delta}`;
99
+ }
100
+
101
+ function evaluateTarget(metricLabel, value) {
102
+ const target = TARGETS[metricLabel];
103
+ if (!target || !Number.isFinite(value)) {
104
+ return null;
105
+ }
106
+
107
+ if (target.operator === ">=") {
108
+ return value >= target.value;
109
+ }
110
+ return null;
111
+ }
112
+
113
+ function displayPath(path) {
114
+ if (!path) return "(none found)";
115
+ const relPath = relative(process.cwd(), path);
116
+ if (relPath && !relPath.startsWith("..")) {
117
+ return relPath;
118
+ }
119
+ return path;
120
+ }
121
+
122
+ function renderMarkdown(beforeSnapshot, afterSnapshot, rows) {
123
+ const lines = [];
124
+ lines.push("# Automated Release Health Delta");
125
+ lines.push("");
126
+ lines.push(
127
+ `- Baseline snapshot: ${
128
+ beforeSnapshot.path && existsSync(beforeSnapshot.path) ? `\`${displayPath(beforeSnapshot.path)}\`` : "`(none found)`"
129
+ }`
130
+ );
131
+ lines.push(`- Current snapshot: \`${displayPath(afterSnapshot.path)}\``);
132
+ lines.push("");
133
+ lines.push("| Metric | Baseline | Current | Delta |");
134
+ lines.push("|---|---:|---:|---:|");
135
+
136
+ for (const row of rows) {
137
+ lines.push(
138
+ `| ${row.metric} | ${formatValue(row.beforeValue)} | ${formatValue(row.afterValue)} | ${formatDelta(
139
+ row.beforeValue,
140
+ row.afterValue
141
+ )} |`
142
+ );
143
+ }
144
+
145
+ lines.push("");
146
+ lines.push("## Target Checks");
147
+ lines.push("");
148
+
149
+ for (const metric of Object.keys(TARGETS)) {
150
+ const value = afterSnapshot.metrics.get(metric) ?? null;
151
+ const passed = evaluateTarget(metric, value);
152
+ const target = TARGETS[metric];
153
+ const status = passed === null ? "n/a" : passed ? "pass" : "miss";
154
+ lines.push(`- ${metric} ${target.operator} ${target.value}: ${status} (current: ${formatValue(value)})`);
155
+ }
156
+
157
+ if (beforeSnapshot.generatedAt || afterSnapshot.generatedAt) {
158
+ lines.push("");
159
+ lines.push("## Snapshot Times");
160
+ lines.push("");
161
+ if (beforeSnapshot.generatedAt) {
162
+ lines.push(`- Baseline generated: ${beforeSnapshot.generatedAt}`);
163
+ }
164
+ if (afterSnapshot.generatedAt) {
165
+ lines.push(`- Current generated: ${afterSnapshot.generatedAt}`);
166
+ }
167
+ }
168
+
169
+ return `${lines.join("\n")}\n`;
170
+ }
171
+
172
+ function main() {
173
+ const args = parseArgs(process.argv.slice(2));
174
+ const beforeSnapshot = readSnapshot(args.before ? resolve(args.before) : null);
175
+ const afterPath = resolve(args.after);
176
+
177
+ if (!existsSync(afterPath)) {
178
+ throw new Error(`Current snapshot not found: ${afterPath}`);
179
+ }
180
+
181
+ const afterSnapshot = readSnapshot(afterPath);
182
+ const rows = METRICS.map((metric) => ({
183
+ metric,
184
+ beforeValue: beforeSnapshot.metrics.get(metric) ?? null,
185
+ afterValue: afterSnapshot.metrics.get(metric) ?? null,
186
+ }));
187
+
188
+ const outputPath = resolve(args.out);
189
+ mkdirSync(dirname(outputPath), { recursive: true });
190
+ const markdown = renderMarkdown(beforeSnapshot, afterSnapshot, rows);
191
+ writeFileSync(outputPath, markdown);
192
+
193
+ console.log(`Wrote ${outputPath}`);
194
+ }
195
+
196
+ try {
197
+ main();
198
+ } catch (error) {
199
+ console.error(error.message);
200
+ process.exit(1);
201
+ }
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+
6
+ # Terms that tend to read as hype/manipulative for technical audiences.
7
+ DISALLOWED='(?i)(\bstealth\b|\bgrowth\b|social-proof|social proof|share kit|\bviral\b|growth loop|\bgrindset\b|\bdominate\b|hack growth|edge line|launch-ready)'
8
+
9
+ SCAN_PATHS=(
10
+ "$ROOT/README.md"
11
+ "$ROOT/docs"
12
+ "$ROOT/public"
13
+ "$ROOT/.github/ISSUE_REPLY_TEMPLATE.md"
14
+ "$ROOT/.github/RELEASE_NOTES_TEMPLATE.md"
15
+ "$ROOT/docs/COMMUNICATION-STYLE.md"
16
+ )
17
+
18
+ echo "Scanning public-facing text for disallowed wording..."
19
+
20
+ if rg -Nni "$DISALLOWED" "${SCAN_PATHS[@]}"; then
21
+ echo "Disallowed public wording found. Use neutral reliability/compatibility language."
22
+ exit 1
23
+ fi
24
+
25
+ echo "Public wording check passed."