@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,131 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- EXPECTED_NAME="${EXPECTED_GIT_NAME:-jtalk22}"
5
- EXPECTED_EMAIL="${EXPECTED_GIT_EMAIL:-james@revasser.nyc}"
6
- ALLOW_GITHUB_WEB_COMMITTER="${ALLOW_GITHUB_WEB_COMMITTER:-0}"
7
- BANNED_REGEX='(?i)(co-authored-by|generated with|\bclaude\b|\bgpt\b|\bcopilot\b|\bgemini\b|\bai\b)'
8
-
9
- die() {
10
- echo "ERROR: $*" >&2
11
- exit 1
12
- }
13
-
14
- contains_banned_markers() {
15
- local text="$1"
16
- if command -v rg >/dev/null 2>&1; then
17
- rg -Niq "$BANNED_REGEX" <<<"$text"
18
- else
19
- grep -Eiq '(Co-authored-by|Generated with|Claude|GPT|Copilot|Gemini)' <<<"$text" \
20
- || grep -Eiq '(^|[^[:alnum:]_])[Aa][Ii]([^[:alnum:]_]|$)' <<<"$text"
21
- fi
22
- }
23
-
24
- is_allowed_owner_author() {
25
- local name="$1"
26
- local email="$2"
27
-
28
- if [[ "$name" != "$EXPECTED_NAME" ]]; then
29
- return 1
30
- fi
31
-
32
- if [[ "$email" == "$EXPECTED_EMAIL" ]]; then
33
- return 0
34
- fi
35
-
36
- if [[ "$ALLOW_GITHUB_WEB_COMMITTER" == "1" ]]; then
37
- if [[ "$email" == "${EXPECTED_NAME}@users.noreply.github.com" || "$email" =~ ^[0-9]+\+${EXPECTED_NAME}@users\.noreply\.github\.com$ ]]; then
38
- return 0
39
- fi
40
- fi
41
-
42
- return 1
43
- }
44
-
45
- is_allowed_owner_committer() {
46
- local committer_name="$1"
47
- local committer_email="$2"
48
- local author_name="$3"
49
- local author_email="$4"
50
-
51
- if [[ "$committer_name" == "$EXPECTED_NAME" && "$committer_email" == "$EXPECTED_EMAIL" ]]; then
52
- return 0
53
- fi
54
-
55
- if [[ "$ALLOW_GITHUB_WEB_COMMITTER" == "1" ]]; then
56
- if [[ "$committer_name" == "$EXPECTED_NAME" ]]; then
57
- if [[ "$committer_email" == "${EXPECTED_NAME}@users.noreply.github.com" || "$committer_email" =~ ^[0-9]+\+${EXPECTED_NAME}@users\.noreply\.github\.com$ ]]; then
58
- if is_allowed_owner_author "$author_name" "$author_email"; then
59
- return 0
60
- fi
61
- fi
62
- fi
63
-
64
- if [[ "$committer_name" == "GitHub" && "$committer_email" == "noreply@github.com" ]]; then
65
- if is_allowed_owner_author "$author_name" "$author_email"; then
66
- return 0
67
- fi
68
- fi
69
- fi
70
-
71
- return 1
72
- }
73
-
74
- if [[ "${SKIP_LOCAL_CONFIG_CHECK:-0}" != "1" ]]; then
75
- local_name="$(git config --get user.name || true)"
76
- local_email="$(git config --get user.email || true)"
77
-
78
- [[ -n "$local_name" ]] || die "Missing repo-local git user.name"
79
- [[ -n "$local_email" ]] || die "Missing repo-local git user.email"
80
-
81
- [[ "$local_name" == "$EXPECTED_NAME" ]] \
82
- || die "Repo-local user.name is '$local_name' (expected '$EXPECTED_NAME')"
83
- [[ "$local_email" == "$EXPECTED_EMAIL" ]] \
84
- || die "Repo-local user.email is '$local_email' (expected '$EXPECTED_EMAIL')"
85
- fi
86
-
87
- default_range="HEAD"
88
- if git rev-parse --verify origin/main >/dev/null 2>&1; then
89
- default_range="origin/main..HEAD"
90
- fi
91
-
92
- range="${1:-${GIT_CHECK_RANGE:-$default_range}}"
93
-
94
- git rev-list --count "$range" >/dev/null 2>&1 || die "Invalid commit range: $range"
95
- commit_count="$(git rev-list --count "$range")"
96
-
97
- if [[ "$commit_count" -eq 0 ]]; then
98
- echo "No commits to validate in range '$range'."
99
- exit 0
100
- fi
101
-
102
- errors=0
103
-
104
- while IFS= read -r sha; do
105
- author_name="$(git show -s --format=%an "$sha")"
106
- author_email="$(git show -s --format=%ae "$sha")"
107
- committer_name="$(git show -s --format=%cn "$sha")"
108
- committer_email="$(git show -s --format=%ce "$sha")"
109
- body="$(git show -s --format=%B "$sha")"
110
-
111
- if ! is_allowed_owner_author "$author_name" "$author_email"; then
112
- echo "Commit $sha has author '$author_name <$author_email>' (expected '$EXPECTED_NAME <$EXPECTED_EMAIL>')." >&2
113
- errors=1
114
- fi
115
-
116
- if ! is_allowed_owner_committer "$committer_name" "$committer_email" "$author_name" "$author_email"; then
117
- echo "Commit $sha has committer '$committer_name <$committer_email>' (expected '$EXPECTED_NAME <$EXPECTED_EMAIL>')." >&2
118
- errors=1
119
- fi
120
-
121
- if contains_banned_markers "$body"; then
122
- echo "Commit $sha contains disallowed attribution markers in commit message." >&2
123
- errors=1
124
- fi
125
- done < <(git rev-list "$range")
126
-
127
- if [[ "$errors" -ne 0 ]]; then
128
- exit 1
129
- fi
130
-
131
- echo "Owner-only attribution check passed for $commit_count commit(s) in '$range'."
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
-
6
- # Terms that tend to read as hype/manipulative for technical audiences.
7
- DISALLOWED='(?i)(\bstealth\b|\bgrowth\b|social-proof|social proof|share kit|\bviral\b|growth loop|\bgrindset\b|\bdominate\b|hack growth|edge line|launch-ready)'
8
-
9
- SCAN_PATHS=(
10
- "$ROOT/README.md"
11
- "$ROOT/index.html"
12
- "$ROOT/docs"
13
- "$ROOT/public"
14
- "$ROOT/.github/ISSUE_REPLY_TEMPLATE.md"
15
- "$ROOT/.github/RELEASE_NOTES_TEMPLATE.md"
16
- "$ROOT/docs/COMMUNICATION-STYLE.md"
17
- )
18
-
19
- echo "Scanning public-facing text for disallowed wording..."
20
-
21
- if rg -Nni "$DISALLOWED" "${SCAN_PATHS[@]}"; then
22
- echo "Disallowed public wording found. Use neutral reliability/compatibility language."
23
- exit 1
24
- fi
25
-
26
- echo "Public wording check passed."
@@ -1,218 +0,0 @@
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
- : process.argv.includes("--public")
16
- ? "docs/release-health/version-parity.md"
17
- : "output/release-health/version-parity.md";
18
- const allowPropagation = process.argv.includes("--allow-propagation");
19
-
20
- const mcpServerName = serverMeta.name;
21
- const expectedWebsiteUrl = serverMeta.websiteUrl;
22
- const expectedDescriptionPrefix = serverMeta.description;
23
- const smitheryEndpoint = "https://server.smithery.ai/jtalk22/slack-mcp-server";
24
- const smitheryListingUrl = "https://smithery.ai/server/jtalk22/slack-mcp-server";
25
-
26
- async function fetchJson(url) {
27
- const res = await fetch(url);
28
- if (!res.ok) {
29
- throw new Error(`HTTP ${res.status} for ${url}`);
30
- }
31
- return res.json();
32
- }
33
-
34
- async function fetchStatus(url) {
35
- const res = await fetch(url);
36
- const text = await res.text().catch(() => "");
37
- return { ok: res.ok, status: res.status, text };
38
- }
39
-
40
- function row(surface, version, status, note = "") {
41
- return `| ${surface} | ${version || "n/a"} | ${status} | ${note} |`;
42
- }
43
-
44
- async function main() {
45
- const localVersion = pkg.version;
46
- const localServerVersion = serverMeta.version;
47
- const localServerPkgVersion = serverMeta.packages?.[0]?.version || null;
48
-
49
- let npmVersion = null;
50
- let mcpRegistryVersion = null;
51
- let mcpRegistryWebsiteUrl = null;
52
- let mcpRegistryDescription = null;
53
- let smitheryReachable = null;
54
- let smitheryStatus = null;
55
- let npmError = null;
56
- let mcpError = null;
57
- let smitheryError = null;
58
-
59
- try {
60
- const npmMeta = await fetchJson(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`);
61
- npmVersion = npmMeta?.["dist-tags"]?.latest || null;
62
- } catch (error) {
63
- npmError = String(error?.message || error);
64
- }
65
-
66
- try {
67
- const registry = await fetchJson(
68
- `https://registry.modelcontextprotocol.io/v0/servers/${encodeURIComponent(mcpServerName)}/versions/latest`
69
- );
70
- mcpRegistryVersion = registry?.server?.version || null;
71
- mcpRegistryWebsiteUrl = registry?.server?.websiteUrl || null;
72
- mcpRegistryDescription = registry?.server?.description || null;
73
- } catch (error) {
74
- mcpError = String(error?.message || error);
75
- }
76
-
77
- try {
78
- const apiResult = await fetchStatus(smitheryEndpoint);
79
- smitheryStatus = apiResult.status;
80
- if (apiResult.ok) {
81
- smitheryReachable = true;
82
- } else if (apiResult.status === 401 || apiResult.status === 403) {
83
- // Auth-gated endpoint still indicates the listing endpoint is live.
84
- smitheryReachable = true;
85
- } else {
86
- const listingResult = await fetchStatus(smitheryListingUrl);
87
- smitheryStatus = `${apiResult.status} (api), ${listingResult.status} (listing)`;
88
- smitheryReachable = listingResult.ok && listingResult.text.length > 0;
89
- }
90
- } catch (error) {
91
- smitheryError = String(error?.message || error);
92
- }
93
-
94
- const parityChecks = [
95
- { name: "package.json vs server.json", ok: localVersion === localServerVersion },
96
- { name: "package.json vs server.json package", ok: localVersion === localServerPkgVersion },
97
- { name: "npm latest", ok: npmVersion === localVersion },
98
- { name: "MCP registry latest", ok: mcpRegistryVersion === localVersion },
99
- { name: "MCP registry websiteUrl", ok: mcpRegistryWebsiteUrl === expectedWebsiteUrl },
100
- {
101
- name: "MCP registry description prefix",
102
- ok: typeof mcpRegistryDescription === "string" && mcpRegistryDescription.startsWith(expectedDescriptionPrefix),
103
- },
104
- ];
105
-
106
- const externalMismatchNames = new Set([
107
- "npm latest",
108
- "MCP registry latest",
109
- "MCP registry websiteUrl",
110
- "MCP registry description prefix",
111
- ]);
112
- const externalMismatches = parityChecks
113
- .filter((check) => !check.ok && externalMismatchNames.has(check.name));
114
- const hardFailures = parityChecks
115
- .filter((check) => !check.ok && !externalMismatchNames.has(check.name));
116
-
117
- const now = new Date().toISOString();
118
- const lines = [
119
- "# Version Parity Report",
120
- "",
121
- `- Generated: ${now}`,
122
- `- Local target version: ${localVersion}`,
123
- "",
124
- "## Surface Matrix",
125
- "",
126
- "| Surface | Version | Status | Notes |",
127
- "|---|---|---|---|",
128
- row(
129
- "package.json",
130
- localVersion,
131
- "ok"
132
- ),
133
- row(
134
- "server.json (root)",
135
- localServerVersion,
136
- localServerVersion === localVersion ? "ok" : "mismatch"
137
- ),
138
- row(
139
- "server.json (package entry)",
140
- localServerPkgVersion,
141
- localServerPkgVersion === localVersion ? "ok" : "mismatch"
142
- ),
143
- row(
144
- "npm dist-tag latest",
145
- npmVersion,
146
- npmVersion === localVersion ? "ok" : "mismatch",
147
- npmError ? `fetch_error: ${npmError}` : ""
148
- ),
149
- row(
150
- "MCP Registry latest",
151
- mcpRegistryVersion,
152
- mcpRegistryVersion === localVersion ? "ok" : "mismatch",
153
- mcpError ? `fetch_error: ${mcpError}` : ""
154
- ),
155
- row(
156
- "MCP Registry websiteUrl",
157
- mcpRegistryWebsiteUrl,
158
- mcpRegistryWebsiteUrl === expectedWebsiteUrl ? "ok" : "mismatch",
159
- `expected: ${expectedWebsiteUrl}`
160
- ),
161
- row(
162
- "MCP Registry description",
163
- mcpRegistryDescription,
164
- typeof mcpRegistryDescription === "string" && mcpRegistryDescription.startsWith(expectedDescriptionPrefix)
165
- ? "ok"
166
- : "mismatch",
167
- `expected_prefix: ${expectedDescriptionPrefix}`
168
- ),
169
- row(
170
- "Smithery endpoint",
171
- "n/a",
172
- smitheryReachable ? "reachable" : "unreachable",
173
- smitheryError
174
- ? `check_error: ${smitheryError}`
175
- : `status: ${smitheryStatus ?? "unknown"}; version check is manual.`
176
- ),
177
- "",
178
- "## Interpretation",
179
- "",
180
- hardFailures.length === 0
181
- ? "- Local metadata parity: pass."
182
- : `- Local metadata parity: fail (${hardFailures.map((f) => f.name).join(", ")}).`,
183
- externalMismatches.length === 0
184
- ? "- External parity: pass."
185
- : `- External parity mismatch: ${externalMismatches.map((f) => f.name).join(", ")}.`,
186
- "",
187
- "## Actionable Drift Notes",
188
- "",
189
- mcpRegistryWebsiteUrl === expectedWebsiteUrl
190
- ? "- MCP registry `websiteUrl` matches local metadata."
191
- : "- MCP registry `websiteUrl` drift detected. Update registry metadata or re-publish metadata-bearing release to align canonical install landing URL.",
192
- typeof mcpRegistryDescription === "string" && mcpRegistryDescription.startsWith(expectedDescriptionPrefix)
193
- ? "- MCP registry description prefix matches local metadata."
194
- : "- MCP registry description drift detected. Align registry listing description with local `server.json` wording.",
195
- externalMismatches.length === 0
196
- ? "- Propagation mode: not needed (external parity is already aligned)."
197
- : (allowPropagation
198
- ? "- Propagation mode enabled: external mismatch accepted temporarily."
199
- : "- Propagation mode disabled: external mismatch is a release gate failure."),
200
- ];
201
-
202
- const outPath = join(repoRoot, outputArg);
203
- mkdirSync(dirname(outPath), { recursive: true });
204
- writeFileSync(outPath, `${lines.join("\n")}\n`, "utf8");
205
- console.log(`Wrote ${outputArg}`);
206
-
207
- if (hardFailures.length > 0) {
208
- process.exit(1);
209
- }
210
- if (!allowPropagation && externalMismatches.length > 0) {
211
- process.exit(1);
212
- }
213
- }
214
-
215
- main().catch((error) => {
216
- console.error(error);
217
- process.exit(1);
218
- });
@@ -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
- });