@jtalk22/slack-mcp 1.2.4 → 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.
- package/README.md +113 -61
- package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +67 -0
- package/docs/DEPLOYMENT-MODES.md +10 -3
- package/docs/HN-LAUNCH.md +45 -36
- package/docs/INDEX.md +9 -0
- package/docs/INSTALL-PROOF.md +18 -0
- package/docs/LAUNCH-COPY-v3.0.0.md +73 -0
- package/docs/LAUNCH-MATRIX.md +22 -0
- package/docs/LAUNCH-OPS.md +71 -0
- package/docs/RELEASE-HEALTH.md +24 -0
- package/docs/TROUBLESHOOTING.md +27 -0
- package/docs/WEB-API.md +13 -4
- 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-poster.png +0 -0
- package/docs/images/demo-sidebar.png +0 -0
- package/docs/images/web-api-mobile-360x800.png +0 -0
- package/docs/images/web-api-mobile-390x844.png +0 -0
- package/lib/handlers.js +14 -6
- package/lib/slack-client.js +17 -1
- package/package.json +28 -12
- package/public/demo-claude.html +83 -10
- package/public/demo-video.html +33 -4
- package/public/demo.html +136 -2
- package/public/index.html +132 -69
- package/scripts/capture-screenshots.js +103 -53
- package/scripts/check-version-parity.js +176 -0
- package/scripts/cloudflare-browser-tool.js +237 -0
- package/scripts/collect-release-health.js +1 -1
- package/scripts/record-demo.js +22 -9
- package/scripts/release-preflight.js +243 -0
- package/scripts/setup-wizard.js +12 -2
- package/scripts/verify-install-flow.js +38 -2
- package/scripts/verify-web.js +49 -1
- package/server.json +47 -0
- package/smithery.yaml +34 -0
- package/src/server-http.js +123 -8
- package/src/server.js +36 -8
- package/src/web-server.js +60 -20
- package/docs/images/demo-claude-v1.2.gif +0 -0
- package/docs/images/demo-readme.gif +0 -0
- package/docs/release-health/2026-02-25.md +0 -33
- package/docs/release-health/2026-02-26.md +0 -33
- package/docs/release-health/24h-delta.md +0 -21
- package/docs/release-health/24h-end.md +0 -33
- package/docs/release-health/24h-start.md +0 -33
- package/docs/release-health/latest.md +0 -33
- package/docs/videos/.gitkeep +0 -0
- 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
|
|
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
|
|
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
|
-
//
|
|
39
|
-
|
|
44
|
+
// Desktop captures from demo.html
|
|
45
|
+
{
|
|
46
|
+
const { context, page } = await openPage(browser, demoPath, { width: 1400, height: 900 });
|
|
40
47
|
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
await
|
|
64
|
-
path: join(imagesDir, 'demo-
|
|
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
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const repoRoot = join(__dirname, "..");
|
|
9
|
+
|
|
10
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
|
|
11
|
+
const serverMeta = JSON.parse(readFileSync(join(repoRoot, "server.json"), "utf8"));
|
|
12
|
+
|
|
13
|
+
const outputArg = process.argv.includes("--out")
|
|
14
|
+
? process.argv[process.argv.indexOf("--out") + 1]
|
|
15
|
+
: "docs/release-health/version-parity.md";
|
|
16
|
+
const allowPropagation = process.argv.includes("--allow-propagation");
|
|
17
|
+
|
|
18
|
+
const mcpServerName = serverMeta.name;
|
|
19
|
+
const smitheryEndpoint = "https://server.smithery.ai/jtalk22/slack-mcp-server";
|
|
20
|
+
const smitheryListingUrl = "https://smithery.ai/server/jtalk22/slack-mcp-server";
|
|
21
|
+
|
|
22
|
+
async function fetchJson(url) {
|
|
23
|
+
const res = await fetch(url);
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
throw new Error(`HTTP ${res.status} for ${url}`);
|
|
26
|
+
}
|
|
27
|
+
return res.json();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function fetchStatus(url) {
|
|
31
|
+
const res = await fetch(url);
|
|
32
|
+
const text = await res.text().catch(() => "");
|
|
33
|
+
return { ok: res.ok, status: res.status, text };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function row(surface, version, status, note = "") {
|
|
37
|
+
return `| ${surface} | ${version || "n/a"} | ${status} | ${note} |`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const localVersion = pkg.version;
|
|
42
|
+
const localServerVersion = serverMeta.version;
|
|
43
|
+
const localServerPkgVersion = serverMeta.packages?.[0]?.version || null;
|
|
44
|
+
|
|
45
|
+
let npmVersion = null;
|
|
46
|
+
let mcpRegistryVersion = null;
|
|
47
|
+
let smitheryReachable = null;
|
|
48
|
+
let smitheryStatus = null;
|
|
49
|
+
let npmError = null;
|
|
50
|
+
let mcpError = null;
|
|
51
|
+
let smitheryError = null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const npmMeta = await fetchJson(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`);
|
|
55
|
+
npmVersion = npmMeta?.["dist-tags"]?.latest || null;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
npmError = String(error?.message || error);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const registry = await fetchJson(
|
|
62
|
+
`https://registry.modelcontextprotocol.io/v0/servers/${encodeURIComponent(mcpServerName)}/versions/latest`
|
|
63
|
+
);
|
|
64
|
+
mcpRegistryVersion = registry?.server?.version || null;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
mcpError = String(error?.message || error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
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
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
smitheryError = String(error?.message || error);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parityChecks = [
|
|
87
|
+
{ name: "package.json vs server.json", ok: localVersion === localServerVersion },
|
|
88
|
+
{ name: "package.json vs server.json package", ok: localVersion === localServerPkgVersion },
|
|
89
|
+
{ name: "npm latest", ok: npmVersion === localVersion },
|
|
90
|
+
{ name: "MCP registry latest", ok: mcpRegistryVersion === localVersion },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const externalMismatches = parityChecks
|
|
94
|
+
.filter((check) => !check.ok && (check.name === "npm latest" || check.name === "MCP registry latest"));
|
|
95
|
+
const hardFailures = parityChecks
|
|
96
|
+
.filter((check) => !check.ok && check.name !== "npm latest" && check.name !== "MCP registry latest");
|
|
97
|
+
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const lines = [
|
|
100
|
+
"# Version Parity Report",
|
|
101
|
+
"",
|
|
102
|
+
`- Generated: ${now}`,
|
|
103
|
+
`- Local target version: ${localVersion}`,
|
|
104
|
+
"",
|
|
105
|
+
"## Surface Matrix",
|
|
106
|
+
"",
|
|
107
|
+
"| Surface | Version | Status | Notes |",
|
|
108
|
+
"|---|---|---|---|",
|
|
109
|
+
row(
|
|
110
|
+
"package.json",
|
|
111
|
+
localVersion,
|
|
112
|
+
"ok"
|
|
113
|
+
),
|
|
114
|
+
row(
|
|
115
|
+
"server.json (root)",
|
|
116
|
+
localServerVersion,
|
|
117
|
+
localServerVersion === localVersion ? "ok" : "mismatch"
|
|
118
|
+
),
|
|
119
|
+
row(
|
|
120
|
+
"server.json (package entry)",
|
|
121
|
+
localServerPkgVersion,
|
|
122
|
+
localServerPkgVersion === localVersion ? "ok" : "mismatch"
|
|
123
|
+
),
|
|
124
|
+
row(
|
|
125
|
+
"npm dist-tag latest",
|
|
126
|
+
npmVersion,
|
|
127
|
+
npmVersion === localVersion ? "ok" : "mismatch",
|
|
128
|
+
npmError ? `fetch_error: ${npmError}` : ""
|
|
129
|
+
),
|
|
130
|
+
row(
|
|
131
|
+
"MCP Registry latest",
|
|
132
|
+
mcpRegistryVersion,
|
|
133
|
+
mcpRegistryVersion === localVersion ? "ok" : "mismatch",
|
|
134
|
+
mcpError ? `fetch_error: ${mcpError}` : ""
|
|
135
|
+
),
|
|
136
|
+
row(
|
|
137
|
+
"Smithery endpoint",
|
|
138
|
+
"n/a",
|
|
139
|
+
smitheryReachable ? "reachable" : "unreachable",
|
|
140
|
+
smitheryError
|
|
141
|
+
? `check_error: ${smitheryError}`
|
|
142
|
+
: `status: ${smitheryStatus ?? "unknown"}; version check is manual.`
|
|
143
|
+
),
|
|
144
|
+
"",
|
|
145
|
+
"## Interpretation",
|
|
146
|
+
"",
|
|
147
|
+
hardFailures.length === 0
|
|
148
|
+
? "- Local metadata parity: pass."
|
|
149
|
+
: `- Local metadata parity: fail (${hardFailures.map((f) => f.name).join(", ")}).`,
|
|
150
|
+
externalMismatches.length === 0
|
|
151
|
+
? "- External parity: pass."
|
|
152
|
+
: `- External parity mismatch: ${externalMismatches.map((f) => f.name).join(", ")}.`,
|
|
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."),
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const outPath = join(repoRoot, outputArg);
|
|
161
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
162
|
+
writeFileSync(outPath, `${lines.join("\n")}\n`, "utf8");
|
|
163
|
+
console.log(`Wrote ${outputArg}`);
|
|
164
|
+
|
|
165
|
+
if (hardFailures.length > 0) {
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
if (!allowPropagation && externalMismatches.length > 0) {
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
main().catch((error) => {
|
|
174
|
+
console.error(error);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
});
|
|
@@ -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 (
|
|
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");
|
package/scripts/record-demo.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
|
142
|
-
console.log(
|
|
143
|
-
console.log(
|
|
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 => {
|