@jtalk22/slack-mcp 3.1.0 → 3.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.
Files changed (65) hide show
  1. package/README.md +45 -13
  2. package/docs/SETUP.md +64 -29
  3. package/docs/TROUBLESHOOTING.md +28 -0
  4. package/lib/handlers.js +156 -0
  5. package/lib/slack-client.js +11 -3
  6. package/lib/token-store.js +6 -5
  7. package/lib/tools.js +131 -0
  8. package/package.json +15 -8
  9. package/public/index.html +10 -6
  10. package/public/share.html +6 -5
  11. package/scripts/setup-wizard.js +1 -1
  12. package/server.json +8 -2
  13. package/src/server-http.js +16 -1
  14. package/src/server.js +31 -7
  15. package/src/web-server.js +117 -4
  16. package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +0 -67
  17. package/docs/COMMUNICATION-STYLE.md +0 -66
  18. package/docs/COMPATIBILITY.md +0 -19
  19. package/docs/DEPLOYMENT-MODES.md +0 -55
  20. package/docs/HN-LAUNCH.md +0 -72
  21. package/docs/INDEX.md +0 -41
  22. package/docs/INSTALL-PROOF.md +0 -18
  23. package/docs/LAUNCH-COPY-v3.0.0.md +0 -101
  24. package/docs/LAUNCH-MATRIX.md +0 -22
  25. package/docs/LAUNCH-OPS.md +0 -71
  26. package/docs/RELEASE-HEALTH.md +0 -77
  27. package/docs/SUPPORT-BOUNDARIES.md +0 -49
  28. package/docs/USE_CASE_RECIPES.md +0 -69
  29. package/docs/WEB-API.md +0 -303
  30. package/docs/images/demo-channel-messages.png +0 -0
  31. package/docs/images/demo-channels.png +0 -0
  32. package/docs/images/demo-claude-mobile-360x800.png +0 -0
  33. package/docs/images/demo-claude-mobile-390x844.png +0 -0
  34. package/docs/images/demo-claude-mobile-poster.png +0 -0
  35. package/docs/images/demo-main-mobile-360x800.png +0 -0
  36. package/docs/images/demo-main-mobile-390x844.png +0 -0
  37. package/docs/images/demo-main.png +0 -0
  38. package/docs/images/demo-messages.png +0 -0
  39. package/docs/images/demo-poster.png +0 -0
  40. package/docs/images/demo-sidebar.png +0 -0
  41. package/docs/images/diagram-oauth-comparison.svg +0 -80
  42. package/docs/images/diagram-session-flow.svg +0 -105
  43. package/docs/images/social-preview-v3.png +0 -0
  44. package/docs/images/web-api-mobile-360x800.png +0 -0
  45. package/docs/images/web-api-mobile-390x844.png +0 -0
  46. package/public/demo-claude.html +0 -1974
  47. package/public/demo-video.html +0 -244
  48. package/public/demo.html +0 -1196
  49. package/scripts/build-mobile-demo.js +0 -168
  50. package/scripts/build-release-health-delta.js +0 -201
  51. package/scripts/build-social-preview.js +0 -189
  52. package/scripts/capture-screenshots.js +0 -152
  53. package/scripts/check-owner-attribution.sh +0 -131
  54. package/scripts/check-public-language.sh +0 -26
  55. package/scripts/check-version-parity.js +0 -218
  56. package/scripts/cloudflare-browser-tool.js +0 -237
  57. package/scripts/collect-release-health.js +0 -162
  58. package/scripts/impact-push-v3.js +0 -781
  59. package/scripts/record-demo.js +0 -163
  60. package/scripts/release-preflight.js +0 -247
  61. package/scripts/setup-git-hooks.sh +0 -15
  62. package/scripts/update-github-social-preview.js +0 -208
  63. package/scripts/verify-core.js +0 -159
  64. package/scripts/verify-install-flow.js +0 -193
  65. package/scripts/verify-web.js +0 -273
@@ -1,168 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawnSync } from "node:child_process";
4
- import { existsSync, mkdirSync, statSync } from "node:fs";
5
- import { dirname, join, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const ROOT = resolve(__dirname, "..");
10
-
11
- const argv = process.argv.slice(2);
12
- const hasArg = (flag) => argv.includes(flag);
13
- const argValue = (flag) => {
14
- const idx = argv.indexOf(flag);
15
- return idx >= 0 && idx + 1 < argv.length ? argv[idx + 1] : null;
16
- };
17
-
18
- const input = resolve(argValue("--in") || join(ROOT, "docs", "videos", "demo-claude.webm"));
19
- const outputVideo = resolve(
20
- argValue("--out-video") || join(ROOT, "docs", "videos", "demo-claude-mobile-20s.mp4")
21
- );
22
- const outputPoster = resolve(
23
- argValue("--out-poster") || join(ROOT, "docs", "images", "demo-claude-mobile-poster.png")
24
- );
25
- const outputGif = resolve(
26
- argValue("--out-gif") || join(ROOT, "docs", "images", "demo-claude-mobile-20s.gif")
27
- );
28
- const validationDir = resolve(
29
- argValue("--validation-dir") || join(ROOT, "output", "release-health", "mobile-first3-frames")
30
- );
31
-
32
- // Start from a frame where tool execution is already visible.
33
- const startSeconds = Number(argValue("--start") || 8);
34
- const durationSeconds = Number(argValue("--duration") || 20);
35
-
36
- function run(label, args) {
37
- console.log(`\n▶ ${label}`);
38
- const result = spawnSync("ffmpeg", args, { stdio: "inherit" });
39
- if (result.status !== 0) {
40
- throw new Error(`ffmpeg failed during: ${label}`);
41
- }
42
- }
43
-
44
- function hasCommand(name, args = ["--version"]) {
45
- const probe = spawnSync(name, args, { stdio: "ignore" });
46
- return probe.status === 0;
47
- }
48
-
49
- function ensureFfmpeg() {
50
- const probe = spawnSync("ffmpeg", ["-version"], { stdio: "ignore" });
51
- if (probe.status !== 0) {
52
- throw new Error("ffmpeg is required but not available in PATH");
53
- }
54
- }
55
-
56
- function formatSize(path) {
57
- const bytes = statSync(path).size;
58
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
59
- }
60
-
61
- if (!existsSync(input)) {
62
- console.error(`Missing input video: ${input}`);
63
- process.exit(1);
64
- }
65
-
66
- ensureFfmpeg();
67
- mkdirSync(dirname(outputVideo), { recursive: true });
68
- mkdirSync(dirname(outputPoster), { recursive: true });
69
- mkdirSync(dirname(outputGif), { recursive: true });
70
-
71
- const verticalComposite =
72
- "[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=22:10[bg];" +
73
- "[0:v]scale=1040:-2:force_original_aspect_ratio=decrease[fg];" +
74
- "[bg][fg]overlay=(W-w)/2:(H-h)/2[vout]";
75
-
76
- run("Build 9:16 mobile clip", [
77
- "-y",
78
- "-ss",
79
- String(startSeconds),
80
- "-t",
81
- String(durationSeconds),
82
- "-i",
83
- input,
84
- "-filter_complex",
85
- verticalComposite,
86
- "-map",
87
- "[vout]",
88
- "-c:v",
89
- "libx264",
90
- "-preset",
91
- "medium",
92
- "-crf",
93
- "20",
94
- "-pix_fmt",
95
- "yuv420p",
96
- "-movflags",
97
- "+faststart",
98
- "-an",
99
- outputVideo,
100
- ]);
101
-
102
- run("Build mobile poster", [
103
- "-y",
104
- "-ss",
105
- String(startSeconds + 2),
106
- "-i",
107
- input,
108
- "-vframes",
109
- "1",
110
- "-update",
111
- "1",
112
- "-filter_complex",
113
- verticalComposite,
114
- "-map",
115
- "[vout]",
116
- outputPoster,
117
- ]);
118
-
119
- if (hasArg("--gif")) {
120
- if (hasCommand("gifski")) {
121
- console.log("\n▶ Build optional mobile GIF preview (gifski)");
122
- const gifResult = spawnSync(
123
- "gifski",
124
- ["--fps", "12", "--width", "540", "--quality", "88", "--output", outputGif, outputVideo],
125
- { stdio: "inherit" }
126
- );
127
- if (gifResult.status !== 0) {
128
- throw new Error("gifski failed while building optional mobile GIF preview");
129
- }
130
- } else {
131
- run("Build optional mobile GIF preview (ffmpeg fallback)", [
132
- "-y",
133
- "-i",
134
- outputVideo,
135
- "-vf",
136
- "fps=12,scale=540:-2:flags=lanczos,split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse=dither=sierra2_4a",
137
- outputGif,
138
- ]);
139
- }
140
- }
141
-
142
- if (hasArg("--validate-first3")) {
143
- mkdirSync(validationDir, { recursive: true });
144
- for (let second = 0; second < 3; second += 1) {
145
- run(`Capture validation frame @ +${second}s`, [
146
- "-y",
147
- "-ss",
148
- String(second),
149
- "-i",
150
- outputVideo,
151
- "-frames:v",
152
- "1",
153
- "-update",
154
- "1",
155
- join(validationDir, `frame-${second}s.png`),
156
- ]);
157
- }
158
- }
159
-
160
- console.log("\n✅ Mobile demo artifacts ready");
161
- console.log(`- Video: ${outputVideo} (${formatSize(outputVideo)})`);
162
- console.log(`- Poster: ${outputPoster} (${formatSize(outputPoster)})`);
163
- if (hasArg("--gif") && existsSync(outputGif)) {
164
- console.log(`- GIF: ${outputGif} (${formatSize(outputGif)})`);
165
- }
166
- if (hasArg("--validate-first3")) {
167
- console.log(`- Validation frames: ${validationDir}`);
168
- }
@@ -1,201 +0,0 @@
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("output", "release-health", "latest.md");
20
- const DEFAULT_OUT = resolve("output", "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
- }
@@ -1,189 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { chromium } from "playwright";
4
- import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
5
- import { dirname, join, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const ROOT = resolve(__dirname, "..");
10
-
11
- const WIDTH = 1280;
12
- const HEIGHT = 640;
13
- const MAX_BYTES = 1_000_000;
14
-
15
- function parseArg(flag) {
16
- const idx = process.argv.indexOf(flag);
17
- return idx >= 0 && idx + 1 < process.argv.length ? process.argv[idx + 1] : null;
18
- }
19
-
20
- const outputPath = resolve(parseArg("--out") || join(ROOT, "docs", "images", "social-preview-v3.png"));
21
- const sourcePath = resolve(parseArg("--source") || join(ROOT, "docs", "images", "demo-poster.png"));
22
-
23
- if (!existsSync(sourcePath)) {
24
- console.error(`Missing source image: ${sourcePath}`);
25
- process.exit(1);
26
- }
27
-
28
- mkdirSync(dirname(outputPath), { recursive: true });
29
-
30
- const sourceDataUri = `data:image/png;base64,${readFileSync(sourcePath).toString("base64")}`;
31
-
32
- const html = `<!doctype html>
33
- <html lang="en">
34
- <head>
35
- <meta charset="utf-8">
36
- <meta name="viewport" content="width=device-width, initial-scale=1">
37
- <title>Slack MCP Server v3.0.0 Social Preview</title>
38
- <style>
39
- :root {
40
- --bg-a: #0f1f4c;
41
- --bg-b: #0a1538;
42
- --line: rgba(141, 168, 233, 0.3);
43
- --text: #ecf4ff;
44
- --muted: #b3c4e8;
45
- }
46
- * {
47
- box-sizing: border-box;
48
- margin: 0;
49
- padding: 0;
50
- font-family: "Space Grotesk", "IBM Plex Sans", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
51
- }
52
- body {
53
- width: ${WIDTH}px;
54
- height: ${HEIGHT}px;
55
- overflow: hidden;
56
- color: var(--text);
57
- background:
58
- radial-gradient(920px 460px at 8% 0%, #2a4f98 0%, transparent 58%),
59
- radial-gradient(980px 500px at 100% 100%, #0f4686 0%, transparent 63%),
60
- linear-gradient(130deg, var(--bg-a), var(--bg-b));
61
- padding: 24px 28px;
62
- }
63
- .card {
64
- width: 100%;
65
- height: 100%;
66
- border-radius: 16px;
67
- border: 1px solid var(--line);
68
- background: linear-gradient(165deg, rgba(16, 34, 82, 0.76), rgba(9, 20, 54, 0.92));
69
- box-shadow: 0 18px 36px rgba(0, 0, 0, 0.3);
70
- padding: 14px 16px;
71
- display: grid;
72
- grid-template-rows: auto 1fr auto;
73
- gap: 10px;
74
- }
75
- .top {
76
- display: flex;
77
- justify-content: space-between;
78
- align-items: center;
79
- font-size: 19px;
80
- font-weight: 600;
81
- letter-spacing: -0.01em;
82
- color: #e9f2ff;
83
- }
84
- .pill {
85
- font-size: 17px;
86
- background: rgba(88, 121, 191, 0.24);
87
- border: 1px solid rgba(137, 167, 227, 0.5);
88
- color: #dce9ff;
89
- border-radius: 999px;
90
- padding: 4px 10px 5px;
91
- font-weight: 600;
92
- }
93
- .image-frame {
94
- border-radius: 13px;
95
- border: 1px solid rgba(147, 173, 240, 0.32);
96
- overflow: hidden;
97
- background: #0a1438;
98
- height: 474px;
99
- }
100
- .image-frame img {
101
- display: block;
102
- width: 100%;
103
- height: 100%;
104
- object-fit: cover;
105
- object-position: top center;
106
- filter: saturate(1.03);
107
- }
108
- .bottom {
109
- border-top: 1px solid rgba(130, 156, 220, 0.24);
110
- padding-top: 8px;
111
- display: grid;
112
- grid-template-columns: 1fr auto;
113
- align-items: center;
114
- gap: 12px;
115
- }
116
- .subhead {
117
- font-size: 28px;
118
- font-weight: 620;
119
- line-height: 1.08;
120
- letter-spacing: -0.02em;
121
- max-width: 930px;
122
- }
123
- .detail {
124
- margin-top: 5px;
125
- font-size: 18px;
126
- color: var(--muted);
127
- letter-spacing: -0.012em;
128
- font-weight: 500;
129
- }
130
- .attribution {
131
- text-align: right;
132
- font-size: 17px;
133
- color: #d6e3ff;
134
- font-weight: 560;
135
- line-height: 1.2;
136
- }
137
- .attribution .mail {
138
- font-size: 14px;
139
- color: #a8bbe6;
140
- font-weight: 500;
141
- }
142
- </style>
143
- </head>
144
- <body>
145
- <main class="card">
146
- <header class="top">
147
- <span>Slack MCP Server</span>
148
- <span class="pill">v3.0.0</span>
149
- </header>
150
- <section class="image-frame">
151
- <img src="${sourceDataUri}" alt="Slack MCP live demo frame">
152
- </section>
153
- <footer class="bottom">
154
- <div>
155
- <div class="subhead">Session-based Slack MCP for Claude and MCP clients.</div>
156
- <div class="detail">Local-first stdio/web. Secure-default hosted HTTP in v3.</div>
157
- </div>
158
- <div class="attribution">
159
- <div>jtalk22</div>
160
- <div class="mail">james@revasser.nyc</div>
161
- </div>
162
- </footer>
163
- </main>
164
- </body>
165
- </html>`;
166
-
167
- const browser = await chromium.launch({ headless: true });
168
- const context = await browser.newContext({
169
- viewport: { width: WIDTH, height: HEIGHT },
170
- deviceScaleFactor: 1,
171
- colorScheme: "dark",
172
- });
173
-
174
- const page = await context.newPage();
175
- await page.setContent(html, { waitUntil: "networkidle" });
176
- await page.waitForTimeout(300);
177
- await page.screenshot({ path: outputPath, type: "png" });
178
-
179
- await context.close();
180
- await browser.close();
181
-
182
- const size = statSync(outputPath).size;
183
- console.log(`Wrote ${outputPath}`);
184
- console.log(`Size: ${size} bytes`);
185
-
186
- if (size > MAX_BYTES) {
187
- console.error(`Image exceeds ${MAX_BYTES} bytes target.`);
188
- process.exit(1);
189
- }
@@ -1,152 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Screenshot capture script using Playwright
4
- * Captures desktop + mobile screenshots for README/docs
5
- */
6
-
7
- import { chromium } from 'playwright';
8
- import { fileURLToPath } from 'url';
9
- import { dirname, join } from 'path';
10
-
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = dirname(__filename);
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
- }
32
-
33
- async function captureScreenshots() {
34
- console.log('Launching browser...');
35
-
36
- const browser = await chromium.launch({
37
- headless: true
38
- });
39
-
40
- const demoPath = join(projectRoot, 'public', 'demo.html');
41
- const demoClaudePath = join(projectRoot, 'public', 'demo-claude.html');
42
- const indexPath = join(projectRoot, 'public', 'index.html');
43
-
44
- // Desktop captures from demo.html
45
- {
46
- const { context, page } = await openPage(browser, demoPath, { width: 1400, height: 900 });
47
-
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
- });
53
-
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
- });
74
-
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 }
80
- });
81
-
82
- await context.close();
83
- }
84
-
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
- // Wait for transient scenario caption animation to settle.
90
- await page.waitForTimeout(2600);
91
- await page.evaluate(() => {
92
- const caption = document.getElementById('scenarioCaption');
93
- if (caption) caption.classList.remove('visible');
94
- });
95
- await page.screenshot({
96
- path: join(imagesDir, 'demo-poster.png'),
97
- clip: { x: 0, y: 0, width: 1280, height: 800 }
98
- });
99
- await context.close();
100
- }
101
-
102
- // Mobile captures for web, demo, and claude demo pages
103
- for (const viewport of viewports) {
104
- const label = `${viewport.width}x${viewport.height}`;
105
- console.log(`Capturing mobile screenshots (${label})...`);
106
-
107
- {
108
- const { context, page } = await openPage(browser, demoPath, viewport);
109
- await page.screenshot({
110
- path: join(imagesDir, `demo-main-mobile-${viewport.suffix}.png`),
111
- fullPage: true
112
- });
113
- await context.close();
114
- }
115
-
116
- {
117
- const { context, page } = await openPage(browser, demoClaudePath, viewport);
118
- await page.screenshot({
119
- path: join(imagesDir, `demo-claude-mobile-${viewport.suffix}.png`),
120
- fullPage: true
121
- });
122
- await context.close();
123
- }
124
-
125
- {
126
- const { context, page } = await openPage(browser, indexPath, viewport);
127
- await page.screenshot({
128
- path: join(imagesDir, `web-api-mobile-${viewport.suffix}.png`),
129
- fullPage: true
130
- });
131
- await context.close();
132
- }
133
- }
134
-
135
- await browser.close();
136
-
137
- console.log('\nScreenshots saved to docs/images/');
138
- console.log(' - demo-main.png');
139
- console.log(' - demo-messages.png');
140
- console.log(' - demo-sidebar.png');
141
- console.log(' - demo-channels.png');
142
- console.log(' - demo-channel-messages.png');
143
- console.log(' - demo-poster.png');
144
- console.log(' - demo-main-mobile-390x844.png');
145
- console.log(' - demo-main-mobile-360x800.png');
146
- console.log(' - demo-claude-mobile-390x844.png');
147
- console.log(' - demo-claude-mobile-360x800.png');
148
- console.log(' - web-api-mobile-390x844.png');
149
- console.log(' - web-api-mobile-360x800.png');
150
- }
151
-
152
- captureScreenshots().catch(console.error);