@jtalk22/slack-mcp 2.0.0 → 3.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 (55) hide show
  1. package/README.md +112 -64
  2. package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +67 -0
  3. package/docs/DEPLOYMENT-MODES.md +10 -3
  4. package/docs/HN-LAUNCH.md +47 -36
  5. package/docs/INDEX.md +4 -1
  6. package/docs/INSTALL-PROOF.md +5 -5
  7. package/docs/LAUNCH-COPY-v3.0.0.md +73 -0
  8. package/docs/LAUNCH-MATRIX.md +4 -2
  9. package/docs/LAUNCH-OPS.md +24 -23
  10. package/docs/RELEASE-HEALTH.md +9 -0
  11. package/docs/TROUBLESHOOTING.md +27 -0
  12. package/docs/WEB-API.md +13 -4
  13. package/docs/images/demo-channel-messages.png +0 -0
  14. package/docs/images/demo-channels.png +0 -0
  15. package/docs/images/demo-claude-mobile-360x800.png +0 -0
  16. package/docs/images/demo-claude-mobile-390x844.png +0 -0
  17. package/docs/images/demo-main-mobile-360x800.png +0 -0
  18. package/docs/images/demo-main-mobile-390x844.png +0 -0
  19. package/docs/images/demo-main.png +0 -0
  20. package/docs/images/demo-poster.png +0 -0
  21. package/docs/images/demo-sidebar.png +0 -0
  22. package/docs/images/web-api-mobile-360x800.png +0 -0
  23. package/docs/images/web-api-mobile-390x844.png +0 -0
  24. package/package.json +14 -6
  25. package/public/demo-claude.html +83 -10
  26. package/public/demo-video.html +33 -4
  27. package/public/demo.html +136 -2
  28. package/public/index.html +132 -69
  29. package/scripts/capture-screenshots.js +103 -53
  30. package/scripts/check-version-parity.js +25 -11
  31. package/scripts/cloudflare-browser-tool.js +237 -0
  32. package/scripts/collect-release-health.js +1 -1
  33. package/scripts/record-demo.js +22 -9
  34. package/scripts/release-preflight.js +243 -0
  35. package/scripts/setup-wizard.js +1 -1
  36. package/scripts/verify-install-flow.js +2 -1
  37. package/scripts/verify-web.js +49 -1
  38. package/server.json +47 -0
  39. package/smithery.yaml +34 -0
  40. package/src/server-http.js +98 -5
  41. package/src/server.js +18 -6
  42. package/src/web-server.js +5 -3
  43. package/docs/LAUNCH-COPY-v2.0.0.md +0 -59
  44. package/docs/images/demo-claude-v1.2.gif +0 -0
  45. package/docs/images/demo-readme.gif +0 -0
  46. package/docs/release-health/2026-02-25.md +0 -33
  47. package/docs/release-health/2026-02-26.md +0 -33
  48. package/docs/release-health/24h-delta.md +0 -21
  49. package/docs/release-health/24h-end.md +0 -33
  50. package/docs/release-health/24h-start.md +0 -33
  51. package/docs/release-health/latest.md +0 -33
  52. package/docs/release-health/launch-log-template.md +0 -21
  53. package/docs/release-health/version-parity.md +0 -21
  54. package/docs/videos/.gitkeep +0 -0
  55. package/docs/videos/demo-claude-v1.2.webm +0 -0
@@ -1,17 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Screenshot capture script using Playwright
4
- * Captures polished screenshots of the demo UI for README/docs
4
+ * Captures desktop + mobile screenshots for README/docs
5
5
  */
6
6
 
7
7
  import { chromium } from 'playwright';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { dirname, join } from 'path';
10
- import { readFileSync } from 'fs';
11
10
 
12
11
  const __filename = fileURLToPath(import.meta.url);
13
12
  const __dirname = dirname(__filename);
14
13
  const projectRoot = join(__dirname, '..');
14
+ const imagesDir = join(projectRoot, 'docs', 'images');
15
+
16
+ const viewports = [
17
+ { width: 390, height: 844, suffix: '390x844' },
18
+ { width: 360, height: 800, suffix: '360x800' }
19
+ ];
20
+
21
+ async function openPage(browser, filePath, viewport) {
22
+ const context = await browser.newContext({
23
+ viewport,
24
+ deviceScaleFactor: 2,
25
+ colorScheme: 'dark'
26
+ });
27
+ const page = await context.newPage();
28
+ await page.goto(`file://${filePath}`);
29
+ await page.waitForTimeout(1000);
30
+ return { context, page };
31
+ }
15
32
 
16
33
  async function captureScreenshots() {
17
34
  console.log('Launching browser...');
@@ -20,68 +37,94 @@ async function captureScreenshots() {
20
37
  headless: true
21
38
  });
22
39
 
23
- const context = await browser.newContext({
24
- viewport: { width: 1400, height: 900 },
25
- deviceScaleFactor: 2, // Retina quality
26
- colorScheme: 'dark'
27
- });
28
-
29
- const page = await context.newPage();
30
-
31
- // Load the demo.html file directly
32
40
  const demoPath = join(projectRoot, 'public', 'demo.html');
33
- const demoHtml = readFileSync(demoPath, 'utf-8');
34
-
35
- // Serve it as a data URL or file URL
36
- await page.goto(`file://${demoPath}`);
41
+ const demoClaudePath = join(projectRoot, 'public', 'demo-claude.html');
42
+ const indexPath = join(projectRoot, 'public', 'index.html');
37
43
 
38
- // Wait for content to render
39
- await page.waitForTimeout(1000);
44
+ // Desktop captures from demo.html
45
+ {
46
+ const { context, page } = await openPage(browser, demoPath, { width: 1400, height: 900 });
40
47
 
41
- const imagesDir = join(projectRoot, 'docs', 'images');
48
+ console.log('Capturing desktop demo screenshots...');
49
+ await page.screenshot({
50
+ path: join(imagesDir, 'demo-main.png'),
51
+ clip: { x: 0, y: 0, width: 1400, height: 800 }
52
+ });
42
53
 
43
- // Screenshot 1: Full UI with DMs
44
- console.log('Capturing main UI screenshot...');
45
- await page.screenshot({
46
- path: join(imagesDir, 'demo-main.png'),
47
- clip: { x: 0, y: 0, width: 1400, height: 800 }
48
- });
54
+ const mainPanel = await page.$('.main-panel');
55
+ if (mainPanel) {
56
+ await mainPanel.screenshot({
57
+ path: join(imagesDir, 'demo-messages.png')
58
+ });
59
+ }
60
+
61
+ const sidebar = await page.$('.sidebar');
62
+ if (sidebar) {
63
+ await sidebar.screenshot({
64
+ path: join(imagesDir, 'demo-sidebar.png')
65
+ });
66
+ }
67
+
68
+ await page.evaluate(() => runScenario('listChannels'));
69
+ await page.waitForTimeout(2600);
70
+ await page.screenshot({
71
+ path: join(imagesDir, 'demo-channels.png'),
72
+ clip: { x: 0, y: 0, width: 1400, height: 800 }
73
+ });
49
74
 
50
- // Screenshot 2: Conversation view (zoomed)
51
- console.log('Capturing conversation screenshot...');
52
- const mainPanel = await page.$('.main-panel');
53
- if (mainPanel) {
54
- await mainPanel.screenshot({
55
- path: join(imagesDir, 'demo-messages.png')
75
+ await page.click('.conversation-item:first-child');
76
+ await page.waitForTimeout(600);
77
+ await page.screenshot({
78
+ path: join(imagesDir, 'demo-channel-messages.png'),
79
+ clip: { x: 0, y: 0, width: 1400, height: 800 }
56
80
  });
81
+
82
+ await context.close();
57
83
  }
58
84
 
59
- // Screenshot 3: Sidebar with conversations
60
- console.log('Capturing sidebar screenshot...');
61
- const sidebar = await page.$('.sidebar');
62
- if (sidebar) {
63
- await sidebar.screenshot({
64
- path: join(imagesDir, 'demo-sidebar.png')
85
+ // Poster from the Claude demo
86
+ {
87
+ const { context, page } = await openPage(browser, demoClaudePath, { width: 1280, height: 800 });
88
+ console.log('Capturing poster image...');
89
+ await page.screenshot({
90
+ path: join(imagesDir, 'demo-poster.png'),
91
+ clip: { x: 0, y: 0, width: 1280, height: 800 }
65
92
  });
93
+ await context.close();
66
94
  }
67
95
 
68
- // Screenshot 4: Switch to channels view
69
- console.log('Capturing channels view...');
70
- await page.click('.tabs button:nth-child(2)'); // Click Channels tab
71
- await page.waitForTimeout(300);
72
- await page.screenshot({
73
- path: join(imagesDir, 'demo-channels.png'),
74
- clip: { x: 0, y: 0, width: 1400, height: 800 }
75
- });
76
-
77
- // Screenshot 5: Engineering channel messages
78
- console.log('Capturing channel messages...');
79
- await page.click('.conversation-item:first-child'); // Click first channel
80
- await page.waitForTimeout(300);
81
- await page.screenshot({
82
- path: join(imagesDir, 'demo-channel-messages.png'),
83
- clip: { x: 0, y: 0, width: 1400, height: 800 }
84
- });
96
+ // Mobile captures for web, demo, and claude demo pages
97
+ for (const viewport of viewports) {
98
+ const label = `${viewport.width}x${viewport.height}`;
99
+ console.log(`Capturing mobile screenshots (${label})...`);
100
+
101
+ {
102
+ const { context, page } = await openPage(browser, demoPath, viewport);
103
+ await page.screenshot({
104
+ path: join(imagesDir, `demo-main-mobile-${viewport.suffix}.png`),
105
+ fullPage: true
106
+ });
107
+ await context.close();
108
+ }
109
+
110
+ {
111
+ const { context, page } = await openPage(browser, demoClaudePath, viewport);
112
+ await page.screenshot({
113
+ path: join(imagesDir, `demo-claude-mobile-${viewport.suffix}.png`),
114
+ fullPage: true
115
+ });
116
+ await context.close();
117
+ }
118
+
119
+ {
120
+ const { context, page } = await openPage(browser, indexPath, viewport);
121
+ await page.screenshot({
122
+ path: join(imagesDir, `web-api-mobile-${viewport.suffix}.png`),
123
+ fullPage: true
124
+ });
125
+ await context.close();
126
+ }
127
+ }
85
128
 
86
129
  await browser.close();
87
130
 
@@ -91,6 +134,13 @@ async function captureScreenshots() {
91
134
  console.log(' - demo-sidebar.png');
92
135
  console.log(' - demo-channels.png');
93
136
  console.log(' - demo-channel-messages.png');
137
+ console.log(' - demo-poster.png');
138
+ console.log(' - demo-main-mobile-390x844.png');
139
+ console.log(' - demo-main-mobile-360x800.png');
140
+ console.log(' - demo-claude-mobile-390x844.png');
141
+ console.log(' - demo-claude-mobile-360x800.png');
142
+ console.log(' - web-api-mobile-390x844.png');
143
+ console.log(' - web-api-mobile-360x800.png');
94
144
  }
95
145
 
96
146
  captureScreenshots().catch(console.error);
@@ -17,6 +17,7 @@ const allowPropagation = process.argv.includes("--allow-propagation");
17
17
 
18
18
  const mcpServerName = serverMeta.name;
19
19
  const smitheryEndpoint = "https://server.smithery.ai/jtalk22/slack-mcp-server";
20
+ const smitheryListingUrl = "https://smithery.ai/server/jtalk22/slack-mcp-server";
20
21
 
21
22
  async function fetchJson(url) {
22
23
  const res = await fetch(url);
@@ -26,12 +27,10 @@ async function fetchJson(url) {
26
27
  return res.json();
27
28
  }
28
29
 
29
- async function fetchText(url) {
30
+ async function fetchStatus(url) {
30
31
  const res = await fetch(url);
31
- if (!res.ok) {
32
- throw new Error(`HTTP ${res.status} for ${url}`);
33
- }
34
- return res.text();
32
+ const text = await res.text().catch(() => "");
33
+ return { ok: res.ok, status: res.status, text };
35
34
  }
36
35
 
37
36
  function row(surface, version, status, note = "") {
@@ -46,6 +45,7 @@ async function main() {
46
45
  let npmVersion = null;
47
46
  let mcpRegistryVersion = null;
48
47
  let smitheryReachable = null;
48
+ let smitheryStatus = null;
49
49
  let npmError = null;
50
50
  let mcpError = null;
51
51
  let smitheryError = null;
@@ -67,8 +67,18 @@ async function main() {
67
67
  }
68
68
 
69
69
  try {
70
- const html = await fetchText(smitheryEndpoint);
71
- smitheryReachable = html.length > 0;
70
+ const apiResult = await fetchStatus(smitheryEndpoint);
71
+ smitheryStatus = apiResult.status;
72
+ if (apiResult.ok) {
73
+ smitheryReachable = true;
74
+ } else if (apiResult.status === 401 || apiResult.status === 403) {
75
+ // Auth-gated endpoint still indicates the listing endpoint is live.
76
+ smitheryReachable = true;
77
+ } else {
78
+ const listingResult = await fetchStatus(smitheryListingUrl);
79
+ smitheryStatus = `${apiResult.status} (api), ${listingResult.status} (listing)`;
80
+ smitheryReachable = listingResult.ok && listingResult.text.length > 0;
81
+ }
72
82
  } catch (error) {
73
83
  smitheryError = String(error?.message || error);
74
84
  }
@@ -127,7 +137,9 @@ async function main() {
127
137
  "Smithery endpoint",
128
138
  "n/a",
129
139
  smitheryReachable ? "reachable" : "unreachable",
130
- smitheryError ? `check_error: ${smitheryError}` : "Version check is manual."
140
+ smitheryError
141
+ ? `check_error: ${smitheryError}`
142
+ : `status: ${smitheryStatus ?? "unknown"}; version check is manual.`
131
143
  ),
132
144
  "",
133
145
  "## Interpretation",
@@ -138,9 +150,11 @@ async function main() {
138
150
  externalMismatches.length === 0
139
151
  ? "- External parity: pass."
140
152
  : `- External parity mismatch: ${externalMismatches.map((f) => f.name).join(", ")}.`,
141
- allowPropagation && externalMismatches.length > 0
142
- ? "- Propagation mode enabled: external mismatch accepted temporarily."
143
- : "- Propagation mode disabled: external mismatch is a release gate failure.",
153
+ externalMismatches.length === 0
154
+ ? "- Propagation mode: not needed (external parity is already aligned)."
155
+ : (allowPropagation
156
+ ? "- Propagation mode enabled: external mismatch accepted temporarily."
157
+ : "- Propagation mode disabled: external mismatch is a release gate failure."),
144
158
  ];
145
159
 
146
160
  const outPath = join(repoRoot, outputArg);
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+
6
+ const ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID;
7
+ const API_TOKEN = process.env.CLOUDFLARE_API_TOKEN || process.env.CF_TERRAFORM_TOKEN;
8
+
9
+ const ENDPOINTS = new Map([
10
+ ["content", "content"],
11
+ ["markdown", "markdown"],
12
+ ["links", "links"],
13
+ ["snapshot", "snapshot"],
14
+ ["scrape", "scrape"],
15
+ ["json", "json"],
16
+ ["screenshot", "screenshot"],
17
+ ["pdf", "pdf"],
18
+ ]);
19
+
20
+ function usage() {
21
+ console.error(`Usage:
22
+ node scripts/cloudflare-browser-tool.js verify
23
+ node scripts/cloudflare-browser-tool.js <mode> <url> [options]
24
+
25
+ Modes:
26
+ content Rendered HTML
27
+ markdown Rendered markdown
28
+ links Extract links
29
+ snapshot HTML with inlined resources
30
+ scrape Extract CSS selectors (use --selectors "h1,.card")
31
+ json AI-structured extraction (use --schema '{"title":"string"}')
32
+ screenshot Capture PNG/JPEG (use --out ./page.png)
33
+ pdf Capture PDF (use --out ./page.pdf)
34
+
35
+ Options:
36
+ --wait-until <value> load|domcontentloaded|networkidle0|networkidle2
37
+ --selectors <csv> CSS selectors for scrape mode
38
+ --schema <json> JSON schema object for json mode
39
+ --out <path> Output path for screenshot/pdf
40
+ --full-page fullPage screenshot (default true)
41
+ --type <png|jpeg> screenshot type (default png)
42
+ `);
43
+ }
44
+
45
+ function getOption(args, name) {
46
+ const idx = args.indexOf(name);
47
+ if (idx === -1 || idx + 1 >= args.length) return null;
48
+ return args[idx + 1];
49
+ }
50
+
51
+ function hasFlag(args, name) {
52
+ return args.includes(name);
53
+ }
54
+
55
+ async function verifyToken() {
56
+ if (!API_TOKEN) {
57
+ throw new Error("Missing API token. Set CLOUDFLARE_API_TOKEN or CF_TERRAFORM_TOKEN.");
58
+ }
59
+
60
+ const checks = [];
61
+ if (ACCOUNT_ID) {
62
+ checks.push({
63
+ source: "account",
64
+ url: `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/tokens/verify`,
65
+ });
66
+ }
67
+ checks.push({
68
+ source: "user",
69
+ url: "https://api.cloudflare.com/client/v4/user/tokens/verify",
70
+ });
71
+
72
+ const failures = [];
73
+ for (const check of checks) {
74
+ const res = await fetch(check.url, {
75
+ headers: {
76
+ Authorization: `Bearer ${API_TOKEN}`,
77
+ "Content-Type": "application/json",
78
+ },
79
+ });
80
+
81
+ const raw = await res.text();
82
+ let data = null;
83
+ try {
84
+ data = raw ? JSON.parse(raw) : null;
85
+ } catch {
86
+ failures.push({
87
+ source: check.source,
88
+ status: res.status,
89
+ message: `Non-JSON response: ${raw.slice(0, 200)}`,
90
+ });
91
+ continue;
92
+ }
93
+
94
+ if (res.ok && data?.success) {
95
+ return {
96
+ source: check.source,
97
+ result: data.result,
98
+ };
99
+ }
100
+
101
+ failures.push({
102
+ source: check.source,
103
+ status: res.status,
104
+ message: JSON.stringify(data?.errors || data || raw),
105
+ });
106
+ }
107
+
108
+ throw new Error(`Token verify failed: ${JSON.stringify(failures)}`);
109
+ }
110
+
111
+ async function callBrowserApi(mode, url, options) {
112
+ if (!ACCOUNT_ID) {
113
+ throw new Error("Missing CLOUDFLARE_ACCOUNT_ID.");
114
+ }
115
+ if (!API_TOKEN) {
116
+ throw new Error("Missing API token. Set CLOUDFLARE_API_TOKEN or CF_TERRAFORM_TOKEN.");
117
+ }
118
+
119
+ const endpoint = ENDPOINTS.get(mode);
120
+ if (!endpoint) {
121
+ throw new Error(`Unsupported mode: ${mode}`);
122
+ }
123
+
124
+ const body = {
125
+ url,
126
+ };
127
+
128
+ if (options.waitUntil) {
129
+ body.waitUntil = options.waitUntil;
130
+ }
131
+ if (mode === "scrape" && options.selectors?.length) {
132
+ body.selectors = options.selectors;
133
+ }
134
+ if (mode === "json" && options.schema) {
135
+ body.schema = options.schema;
136
+ }
137
+ if (mode === "screenshot") {
138
+ body.screenshotOptions = {
139
+ type: options.type || "png",
140
+ fullPage: options.fullPage,
141
+ };
142
+ }
143
+ if (mode === "pdf") {
144
+ body.pdfOptions = {
145
+ printBackground: true,
146
+ format: "A4",
147
+ };
148
+ }
149
+
150
+ const res = await fetch(
151
+ `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/browser-rendering/${endpoint}`,
152
+ {
153
+ method: "POST",
154
+ headers: {
155
+ Authorization: `Bearer ${API_TOKEN}`,
156
+ "Content-Type": "application/json",
157
+ },
158
+ body: JSON.stringify(body),
159
+ }
160
+ );
161
+
162
+ if (!res.ok) {
163
+ const text = await res.text().catch(() => "");
164
+ throw new Error(`Browser Rendering API failed (${res.status}): ${text || res.statusText}`);
165
+ }
166
+
167
+ return res;
168
+ }
169
+
170
+ async function main() {
171
+ const [, , mode, url, ...rest] = process.argv;
172
+
173
+ if (!mode) {
174
+ usage();
175
+ process.exit(1);
176
+ }
177
+
178
+ if (mode === "verify") {
179
+ const tokenInfo = await verifyToken();
180
+ console.log(
181
+ JSON.stringify(
182
+ {
183
+ status: "ok",
184
+ verify_source: tokenInfo.source,
185
+ token_status: tokenInfo.result?.status || "unknown",
186
+ token_id: tokenInfo.result?.id || null,
187
+ account_id_present: Boolean(ACCOUNT_ID),
188
+ },
189
+ null,
190
+ 2
191
+ )
192
+ );
193
+ return;
194
+ }
195
+
196
+ if (!url) {
197
+ usage();
198
+ process.exit(1);
199
+ }
200
+
201
+ const options = {
202
+ waitUntil: getOption(rest, "--wait-until") || undefined,
203
+ selectors: (getOption(rest, "--selectors") || "")
204
+ .split(",")
205
+ .map((v) => v.trim())
206
+ .filter(Boolean),
207
+ schema: getOption(rest, "--schema") ? JSON.parse(getOption(rest, "--schema")) : undefined,
208
+ out: getOption(rest, "--out") || undefined,
209
+ type: getOption(rest, "--type") || "png",
210
+ fullPage: !hasFlag(rest, "--no-full-page"),
211
+ };
212
+
213
+ const res = await callBrowserApi(mode, url, options);
214
+
215
+ if (mode === "screenshot" || mode === "pdf") {
216
+ const outputPath = resolve(options.out || (mode === "pdf" ? "./cloudflare-page.pdf" : "./cloudflare-page.png"));
217
+ const buffer = Buffer.from(await res.arrayBuffer());
218
+ writeFileSync(outputPath, buffer);
219
+ console.log(JSON.stringify({ status: "ok", mode, output: outputPath, bytes: buffer.length }, null, 2));
220
+ return;
221
+ }
222
+
223
+ const contentType = res.headers.get("content-type") || "";
224
+ if (contentType.includes("application/json")) {
225
+ const json = await res.json();
226
+ console.log(JSON.stringify(json, null, 2));
227
+ return;
228
+ }
229
+
230
+ const text = await res.text();
231
+ console.log(text);
232
+ }
233
+
234
+ main().catch((error) => {
235
+ console.error(error instanceof Error ? error.message : String(error));
236
+ process.exit(1);
237
+ });
@@ -70,7 +70,7 @@ function buildMarkdown(data) {
70
70
  lines.push(`- deployment-intake submissions (all-time): ${data.github.deploymentIntakeCount ?? "n/a"}`);
71
71
  lines.push("");
72
72
 
73
- lines.push("## 14-Day Reliability Targets (v2.0.0 Cycle)");
73
+ lines.push("## 14-Day Reliability Targets (v3.0.0 Cycle)");
74
74
  lines.push("");
75
75
  lines.push("- weekly downloads: >= 180");
76
76
  lines.push("- qualified deployment-intake submissions: >= 2");
@@ -10,11 +10,17 @@
10
10
  import { chromium } from 'playwright';
11
11
  import { fileURLToPath } from 'url';
12
12
  import { dirname, join } from 'path';
13
- import { mkdirSync, existsSync } from 'fs';
13
+ import { mkdirSync, existsSync, copyFileSync } from 'fs';
14
14
 
15
15
  const __filename = fileURLToPath(import.meta.url);
16
16
  const __dirname = dirname(__filename);
17
17
  const projectRoot = join(__dirname, '..');
18
+ const argv = process.argv.slice(2);
19
+ const hasArg = (flag) => argv.includes(flag);
20
+ const argValue = (flag) => {
21
+ const idx = argv.indexOf(flag);
22
+ return idx >= 0 && idx + 1 < argv.length ? argv[idx + 1] : null;
23
+ };
18
24
 
19
25
  // Configuration
20
26
  const CONFIG = {
@@ -34,6 +40,9 @@ const CONFIG = {
34
40
  }
35
41
  };
36
42
 
43
+ const canonicalOutput = argValue('--out') || join(projectRoot, 'docs', 'videos', 'demo-claude.webm');
44
+ const archiveOutput = hasArg('--archive');
45
+
37
46
  async function recordDemo() {
38
47
  console.log('╔════════════════════════════════════════════════════════════╗');
39
48
  console.log('║ Slack MCP Server - Demo Video Recording ║');
@@ -47,13 +56,12 @@ async function recordDemo() {
47
56
  console.log(`📁 Created directory: ${videosDir}`);
48
57
  }
49
58
 
50
- // Generate timestamped filename
51
59
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
52
- const videoFilename = `demo-claude-${timestamp}.webm`;
60
+ const timestampedOutput = join(videosDir, `demo-claude-${timestamp}.webm`);
53
61
 
54
62
  console.log('🚀 Launching browser...');
55
63
  const browser = await chromium.launch({
56
- headless: false, // Need visible browser for recording
64
+ headless: true,
57
65
  });
58
66
 
59
67
  const context = await browser.newContext({
@@ -134,13 +142,18 @@ async function recordDemo() {
134
142
  console.log('║ ✅ Recording Complete! ║');
135
143
  console.log('╚════════════════════════════════════════════════════════════╝');
136
144
  console.log();
137
- console.log(`📹 Video saved: ${videoPath}`);
145
+ copyFileSync(videoPath, canonicalOutput);
146
+ console.log(`📹 Canonical video: ${canonicalOutput}`);
147
+ if (archiveOutput) {
148
+ copyFileSync(videoPath, timestampedOutput);
149
+ console.log(`🗂️ Archived copy: ${timestampedOutput}`);
150
+ }
138
151
  console.log();
139
152
  console.log('Next steps:');
140
- console.log(' 1. Review the video in a media player');
141
- console.log(' 2. Convert to GIF: npm run gif (requires gifski)');
142
- console.log(' 3. Or with FFmpeg:');
143
- console.log(` ffmpeg -i "${videoPath}" -vf "fps=15,scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" docs/images/demo-claude.gif`);
153
+ console.log(' 1. Review the canonical video in a media player');
154
+ console.log(' 2. Convert to GIF with FFmpeg:');
155
+ console.log(` ffmpeg -i "${canonicalOutput}" -vf "fps=15,scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" docs/images/demo-claude.gif`);
156
+ console.log(' 3. Re-run with --archive to keep timestamped historical outputs');
144
157
  }
145
158
 
146
159
  recordDemo().catch(err => {