@jtalk22/slack-mcp 3.0.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.
- package/README.md +63 -28
- package/docs/SETUP.md +64 -29
- package/docs/TROUBLESHOOTING.md +28 -0
- package/lib/handlers.js +156 -0
- package/lib/slack-client.js +11 -3
- package/lib/token-store.js +6 -5
- package/lib/tools.js +131 -0
- package/package.json +35 -36
- package/public/index.html +10 -6
- package/public/share.html +128 -0
- package/scripts/setup-wizard.js +1 -1
- package/server.json +10 -4
- package/src/server-http.js +16 -1
- package/src/server.js +31 -7
- package/src/web-server.js +119 -4
- package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +0 -67
- package/docs/COMMUNICATION-STYLE.md +0 -66
- package/docs/COMPATIBILITY.md +0 -19
- package/docs/DEPLOYMENT-MODES.md +0 -55
- package/docs/HN-LAUNCH.md +0 -72
- package/docs/INDEX.md +0 -40
- package/docs/INSTALL-PROOF.md +0 -18
- package/docs/LAUNCH-COPY-v3.0.0.md +0 -73
- package/docs/LAUNCH-MATRIX.md +0 -22
- package/docs/LAUNCH-OPS.md +0 -71
- package/docs/RELEASE-HEALTH.md +0 -90
- package/docs/SUPPORT-BOUNDARIES.md +0 -49
- package/docs/USE_CASE_RECIPES.md +0 -69
- package/docs/WEB-API.md +0 -303
- package/docs/images/demo-channel-messages.png +0 -0
- package/docs/images/demo-channels.png +0 -0
- package/docs/images/demo-claude-mobile-360x800.png +0 -0
- package/docs/images/demo-claude-mobile-390x844.png +0 -0
- package/docs/images/demo-main-mobile-360x800.png +0 -0
- package/docs/images/demo-main-mobile-390x844.png +0 -0
- package/docs/images/demo-main.png +0 -0
- package/docs/images/demo-messages.png +0 -0
- package/docs/images/demo-poster.png +0 -0
- package/docs/images/demo-sidebar.png +0 -0
- package/docs/images/diagram-oauth-comparison.svg +0 -80
- package/docs/images/diagram-session-flow.svg +0 -105
- package/docs/images/web-api-mobile-360x800.png +0 -0
- package/docs/images/web-api-mobile-390x844.png +0 -0
- package/public/demo-claude.html +0 -1958
- package/public/demo-video.html +0 -235
- package/public/demo.html +0 -1196
- package/scripts/build-release-health-delta.js +0 -201
- package/scripts/capture-screenshots.js +0 -146
- package/scripts/check-owner-attribution.sh +0 -80
- package/scripts/check-public-language.sh +0 -25
- package/scripts/check-version-parity.js +0 -176
- package/scripts/cloudflare-browser-tool.js +0 -237
- package/scripts/collect-release-health.js +0 -150
- package/scripts/record-demo.js +0 -162
- package/scripts/release-preflight.js +0 -243
- package/scripts/setup-git-hooks.sh +0 -15
- package/scripts/verify-core.js +0 -159
- package/scripts/verify-install-flow.js +0 -193
- package/scripts/verify-web.js +0 -269
|
@@ -1,237 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
4
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
6
|
-
|
|
7
|
-
const REPO =
|
|
8
|
-
process.env.RELEASE_HEALTH_REPO ||
|
|
9
|
-
process.env.GROWTH_REPO ||
|
|
10
|
-
"jtalk22/slack-mcp-server";
|
|
11
|
-
const NPM_PACKAGE =
|
|
12
|
-
process.env.RELEASE_HEALTH_NPM_PACKAGE ||
|
|
13
|
-
process.env.GROWTH_NPM_PACKAGE ||
|
|
14
|
-
"@jtalk22/slack-mcp";
|
|
15
|
-
|
|
16
|
-
function safeGhApi(path) {
|
|
17
|
-
try {
|
|
18
|
-
const out = execSync(`gh api ${path}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
19
|
-
return JSON.parse(out);
|
|
20
|
-
} catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function fetchJson(url) {
|
|
26
|
-
const response = await fetch(url);
|
|
27
|
-
if (!response.ok) {
|
|
28
|
-
throw new Error(`Request failed: ${url} (${response.status})`);
|
|
29
|
-
}
|
|
30
|
-
return response.json();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function toDateSlug(date) {
|
|
34
|
-
const y = date.getFullYear();
|
|
35
|
-
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
36
|
-
const d = String(date.getDate()).padStart(2, "0");
|
|
37
|
-
return `${y}-${m}-${d}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function countNonPrIssues(items) {
|
|
41
|
-
if (!Array.isArray(items)) return 0;
|
|
42
|
-
return items.filter((item) => item && !item.pull_request).length;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function buildMarkdown(data) {
|
|
46
|
-
const lines = [];
|
|
47
|
-
lines.push("# Release Health Snapshot");
|
|
48
|
-
lines.push("");
|
|
49
|
-
lines.push(`- Generated: ${data.generatedAt}`);
|
|
50
|
-
lines.push(`- Repo: \`${REPO}\``);
|
|
51
|
-
lines.push(`- Package: \`${NPM_PACKAGE}\``);
|
|
52
|
-
lines.push("");
|
|
53
|
-
|
|
54
|
-
lines.push("## Install Signals");
|
|
55
|
-
lines.push("");
|
|
56
|
-
lines.push(`- npm downloads (last week): ${data.npm.lastWeek ?? "n/a"}`);
|
|
57
|
-
lines.push(`- npm downloads (last month): ${data.npm.lastMonth ?? "n/a"}`);
|
|
58
|
-
lines.push(`- npm latest version: ${data.npm.latestVersion ?? "n/a"}`);
|
|
59
|
-
lines.push("");
|
|
60
|
-
|
|
61
|
-
lines.push("## GitHub Reach");
|
|
62
|
-
lines.push("");
|
|
63
|
-
lines.push(`- stars: ${data.github.stars ?? "n/a"}`);
|
|
64
|
-
lines.push(`- forks: ${data.github.forks ?? "n/a"}`);
|
|
65
|
-
lines.push(`- open issues: ${data.github.openIssues ?? "n/a"}`);
|
|
66
|
-
lines.push(`- 14d views: ${data.github.viewsCount ?? "n/a"}`);
|
|
67
|
-
lines.push(`- 14d unique visitors: ${data.github.viewsUniques ?? "n/a"}`);
|
|
68
|
-
lines.push(`- 14d clones: ${data.github.clonesCount ?? "n/a"}`);
|
|
69
|
-
lines.push(`- 14d unique cloners: ${data.github.clonesUniques ?? "n/a"}`);
|
|
70
|
-
lines.push(`- deployment-intake submissions (all-time): ${data.github.deploymentIntakeCount ?? "n/a"}`);
|
|
71
|
-
lines.push("");
|
|
72
|
-
|
|
73
|
-
lines.push("## 14-Day Reliability Targets (v3.0.0 Cycle)");
|
|
74
|
-
lines.push("");
|
|
75
|
-
lines.push("- weekly downloads: >= 180");
|
|
76
|
-
lines.push("- qualified deployment-intake submissions: >= 2");
|
|
77
|
-
lines.push("- maintainer support load: <= 2 hours/week");
|
|
78
|
-
lines.push("");
|
|
79
|
-
|
|
80
|
-
lines.push("## Notes");
|
|
81
|
-
lines.push("");
|
|
82
|
-
lines.push("- Update this snapshot daily during active release windows, then weekly.");
|
|
83
|
-
lines.push("- Track deployment-intake quality and support load manually in issue notes.");
|
|
84
|
-
|
|
85
|
-
return `${lines.join("\n")}\n`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function main() {
|
|
89
|
-
const now = new Date();
|
|
90
|
-
const generatedAt = now.toISOString();
|
|
91
|
-
const dateSlug = toDateSlug(now);
|
|
92
|
-
|
|
93
|
-
let npmWeek = null;
|
|
94
|
-
let npmMonth = null;
|
|
95
|
-
let npmMeta = null;
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
npmWeek = await fetchJson(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(NPM_PACKAGE)}`);
|
|
99
|
-
} catch {}
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
npmMonth = await fetchJson(`https://api.npmjs.org/downloads/point/last-month/${encodeURIComponent(NPM_PACKAGE)}`);
|
|
103
|
-
} catch {}
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
npmMeta = await fetchJson(`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE)}`);
|
|
107
|
-
} catch {}
|
|
108
|
-
|
|
109
|
-
const repoInfo = safeGhApi(`repos/${REPO}`) || {};
|
|
110
|
-
const views = safeGhApi(`repos/${REPO}/traffic/views`) || {};
|
|
111
|
-
const clones = safeGhApi(`repos/${REPO}/traffic/clones`) || {};
|
|
112
|
-
const intakeIssues = safeGhApi(`repos/${REPO}/issues?state=all&labels=deployment-intake&per_page=100`) || [];
|
|
113
|
-
|
|
114
|
-
const data = {
|
|
115
|
-
generatedAt,
|
|
116
|
-
npm: {
|
|
117
|
-
lastWeek: npmWeek?.downloads ?? null,
|
|
118
|
-
lastMonth: npmMonth?.downloads ?? null,
|
|
119
|
-
latestVersion: npmMeta?.["dist-tags"]?.latest ?? null,
|
|
120
|
-
},
|
|
121
|
-
github: {
|
|
122
|
-
stars: repoInfo.stargazers_count ?? null,
|
|
123
|
-
forks: repoInfo.forks_count ?? null,
|
|
124
|
-
openIssues: repoInfo.open_issues_count ?? null,
|
|
125
|
-
viewsCount: views.count ?? null,
|
|
126
|
-
viewsUniques: views.uniques ?? null,
|
|
127
|
-
clonesCount: clones.count ?? null,
|
|
128
|
-
clonesUniques: clones.uniques ?? null,
|
|
129
|
-
deploymentIntakeCount: countNonPrIssues(intakeIssues),
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const markdown = buildMarkdown(data);
|
|
134
|
-
|
|
135
|
-
const metricsDir = resolve("docs", "release-health");
|
|
136
|
-
const datedPath = join(metricsDir, `${dateSlug}.md`);
|
|
137
|
-
const latestPath = join(metricsDir, "latest.md");
|
|
138
|
-
|
|
139
|
-
mkdirSync(metricsDir, { recursive: true });
|
|
140
|
-
writeFileSync(datedPath, markdown);
|
|
141
|
-
writeFileSync(latestPath, markdown);
|
|
142
|
-
|
|
143
|
-
console.log(`Wrote ${datedPath}`);
|
|
144
|
-
console.log(`Wrote ${latestPath}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
main().catch((error) => {
|
|
148
|
-
console.error(error.message);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
});
|
package/scripts/record-demo.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Demo video recording script using Playwright
|
|
4
|
-
* Records the Claude Desktop demo in fullscreen mode with auto-play
|
|
5
|
-
*
|
|
6
|
-
* Usage: npm run record-demo
|
|
7
|
-
* Output: docs/videos/demo-claude-TIMESTAMP.webm
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { chromium } from 'playwright';
|
|
11
|
-
import { fileURLToPath } from 'url';
|
|
12
|
-
import { dirname, join } from 'path';
|
|
13
|
-
import { mkdirSync, existsSync, copyFileSync } from 'fs';
|
|
14
|
-
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = dirname(__filename);
|
|
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
|
-
};
|
|
24
|
-
|
|
25
|
-
// Configuration
|
|
26
|
-
const CONFIG = {
|
|
27
|
-
viewport: { width: 1280, height: 800 },
|
|
28
|
-
speed: '0.5', // Slow speed for video recording
|
|
29
|
-
scenarioCount: 5,
|
|
30
|
-
// Title and closing card durations (ms)
|
|
31
|
-
introDuration: 4000, // Title card (3s visible + fade)
|
|
32
|
-
outroDuration: 5500, // Closing card (4s visible + fades)
|
|
33
|
-
// Approximate duration per scenario at 0.5x speed (in ms)
|
|
34
|
-
scenarioDurations: {
|
|
35
|
-
search: 26000, // +1s for transition fade
|
|
36
|
-
thread: 26000,
|
|
37
|
-
list: 21000,
|
|
38
|
-
send: 19000,
|
|
39
|
-
multi: 36000
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const canonicalOutput = argValue('--out') || join(projectRoot, 'docs', 'videos', 'demo-claude.webm');
|
|
44
|
-
const archiveOutput = hasArg('--archive');
|
|
45
|
-
|
|
46
|
-
async function recordDemo() {
|
|
47
|
-
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
48
|
-
console.log('║ Slack MCP Server - Demo Video Recording ║');
|
|
49
|
-
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
50
|
-
console.log();
|
|
51
|
-
|
|
52
|
-
// Ensure videos directory exists
|
|
53
|
-
const videosDir = join(projectRoot, 'docs', 'videos');
|
|
54
|
-
if (!existsSync(videosDir)) {
|
|
55
|
-
mkdirSync(videosDir, { recursive: true });
|
|
56
|
-
console.log(`📁 Created directory: ${videosDir}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
60
|
-
const timestampedOutput = join(videosDir, `demo-claude-${timestamp}.webm`);
|
|
61
|
-
|
|
62
|
-
console.log('🚀 Launching browser...');
|
|
63
|
-
const browser = await chromium.launch({
|
|
64
|
-
headless: true,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const context = await browser.newContext({
|
|
68
|
-
viewport: CONFIG.viewport,
|
|
69
|
-
recordVideo: {
|
|
70
|
-
dir: videosDir,
|
|
71
|
-
size: CONFIG.viewport
|
|
72
|
-
},
|
|
73
|
-
colorScheme: 'dark'
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const page = await context.newPage();
|
|
77
|
-
|
|
78
|
-
// Load the demo
|
|
79
|
-
const demoPath = join(projectRoot, 'public', 'demo-claude.html');
|
|
80
|
-
console.log(`📄 Loading: ${demoPath}`);
|
|
81
|
-
await page.goto(`file://${demoPath}`);
|
|
82
|
-
await page.waitForTimeout(1000);
|
|
83
|
-
|
|
84
|
-
// Hold on initial frame for a few seconds (visible first frame in GIF)
|
|
85
|
-
console.log('⏸️ Holding initial frame (3s)...');
|
|
86
|
-
await page.waitForTimeout(3000);
|
|
87
|
-
|
|
88
|
-
// Set slow speed for video recording
|
|
89
|
-
console.log(`⏱️ Setting speed to ${CONFIG.speed}x...`);
|
|
90
|
-
await page.selectOption('#speedSelect', CONFIG.speed);
|
|
91
|
-
await page.waitForTimeout(300);
|
|
92
|
-
|
|
93
|
-
// Start auto-play BEFORE entering fullscreen (button hidden in fullscreen)
|
|
94
|
-
console.log('▶️ Starting auto-play...');
|
|
95
|
-
await page.click('#autoPlayBtn');
|
|
96
|
-
await page.waitForTimeout(500);
|
|
97
|
-
|
|
98
|
-
// Now enter fullscreen mode
|
|
99
|
-
console.log('🖥️ Entering fullscreen mode...');
|
|
100
|
-
console.log();
|
|
101
|
-
await page.keyboard.press('f');
|
|
102
|
-
await page.waitForTimeout(500);
|
|
103
|
-
|
|
104
|
-
// Wait for title card
|
|
105
|
-
console.log('📺 Showing title card...');
|
|
106
|
-
await page.waitForTimeout(CONFIG.introDuration);
|
|
107
|
-
|
|
108
|
-
// Wait for each scenario
|
|
109
|
-
const scenarios = ['search', 'thread', 'list', 'send', 'multi'];
|
|
110
|
-
let totalWait = 0;
|
|
111
|
-
|
|
112
|
-
for (let i = 0; i < scenarios.length; i++) {
|
|
113
|
-
const scenario = scenarios[i];
|
|
114
|
-
const duration = CONFIG.scenarioDurations[scenario];
|
|
115
|
-
totalWait += duration;
|
|
116
|
-
|
|
117
|
-
console.log(` 📍 Scenario ${i + 1}/${scenarios.length}: ${scenario} (${Math.round(duration/1000)}s)`);
|
|
118
|
-
await page.waitForTimeout(duration);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Wait for closing card
|
|
122
|
-
console.log();
|
|
123
|
-
console.log('🎬 Showing closing screen...');
|
|
124
|
-
await page.waitForTimeout(CONFIG.outroDuration);
|
|
125
|
-
|
|
126
|
-
// Exit fullscreen
|
|
127
|
-
console.log('🔚 Exiting fullscreen...');
|
|
128
|
-
await page.keyboard.press('Escape');
|
|
129
|
-
await page.waitForTimeout(500);
|
|
130
|
-
|
|
131
|
-
// Close context to flush video
|
|
132
|
-
console.log('💾 Saving video...');
|
|
133
|
-
const video = await page.video();
|
|
134
|
-
await context.close();
|
|
135
|
-
await browser.close();
|
|
136
|
-
|
|
137
|
-
// Get the actual video path
|
|
138
|
-
const videoPath = await video.path();
|
|
139
|
-
|
|
140
|
-
console.log();
|
|
141
|
-
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
142
|
-
console.log('║ ✅ Recording Complete! ║');
|
|
143
|
-
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
144
|
-
console.log();
|
|
145
|
-
copyFileSync(videoPath, canonicalOutput);
|
|
146
|
-
console.log(`📹 Canonical video: ${canonicalOutput}`);
|
|
147
|
-
if (archiveOutput) {
|
|
148
|
-
copyFileSync(videoPath, timestampedOutput);
|
|
149
|
-
console.log(`🗂️ Archived copy: ${timestampedOutput}`);
|
|
150
|
-
}
|
|
151
|
-
console.log();
|
|
152
|
-
console.log('Next steps:');
|
|
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');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
recordDemo().catch(err => {
|
|
160
|
-
console.error('❌ Recording failed:', err.message);
|
|
161
|
-
process.exit(1);
|
|
162
|
-
});
|