@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,163 +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
- initialHold: 500,
31
- // Title and closing card durations (ms)
32
- introDuration: 4000, // Title card (3s visible + fade)
33
- outroDuration: 5500, // Closing card (4s visible + fades)
34
- // Approximate duration per scenario at 0.5x speed (in ms)
35
- scenarioDurations: {
36
- search: 26000, // +1s for transition fade
37
- thread: 26000,
38
- list: 21000,
39
- send: 19000,
40
- multi: 36000
41
- }
42
- };
43
-
44
- const canonicalOutput = argValue('--out') || join(projectRoot, 'docs', 'videos', 'demo-claude.webm');
45
- const archiveOutput = hasArg('--archive');
46
-
47
- async function recordDemo() {
48
- console.log('╔════════════════════════════════════════════════════════════╗');
49
- console.log('║ Slack MCP Server - Demo Video Recording ║');
50
- console.log('╚════════════════════════════════════════════════════════════╝');
51
- console.log();
52
-
53
- // Ensure videos directory exists
54
- const videosDir = join(projectRoot, 'docs', 'videos');
55
- if (!existsSync(videosDir)) {
56
- mkdirSync(videosDir, { recursive: true });
57
- console.log(`📁 Created directory: ${videosDir}`);
58
- }
59
-
60
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
61
- const timestampedOutput = join(videosDir, `demo-claude-${timestamp}.webm`);
62
-
63
- console.log('🚀 Launching browser...');
64
- const browser = await chromium.launch({
65
- headless: true,
66
- });
67
-
68
- const context = await browser.newContext({
69
- viewport: CONFIG.viewport,
70
- recordVideo: {
71
- dir: videosDir,
72
- size: CONFIG.viewport
73
- },
74
- colorScheme: 'dark'
75
- });
76
-
77
- const page = await context.newPage();
78
-
79
- // Load the demo
80
- const demoPath = join(projectRoot, 'public', 'demo-claude.html');
81
- console.log(`📄 Loading: ${demoPath}`);
82
- await page.goto(`file://${demoPath}`);
83
- await page.waitForTimeout(1000);
84
-
85
- // Keep first frame brief so autoplay reaches full-screen flow quickly.
86
- console.log(`⏸️ Holding initial frame (${CONFIG.initialHold}ms)...`);
87
- await page.waitForTimeout(CONFIG.initialHold);
88
-
89
- // Set slow speed for video recording
90
- console.log(`⏱️ Setting speed to ${CONFIG.speed}x...`);
91
- await page.selectOption('#speedSelect', CONFIG.speed);
92
- await page.waitForTimeout(300);
93
-
94
- // Start auto-play BEFORE entering fullscreen (button hidden in fullscreen)
95
- console.log('▶️ Starting auto-play...');
96
- await page.click('#autoPlayBtn');
97
- await page.waitForTimeout(500);
98
-
99
- // Now enter fullscreen mode
100
- console.log('🖥️ Entering fullscreen mode...');
101
- console.log();
102
- await page.keyboard.press('f');
103
- await page.waitForTimeout(500);
104
-
105
- // Wait for title card
106
- console.log('📺 Showing title card...');
107
- await page.waitForTimeout(CONFIG.introDuration);
108
-
109
- // Wait for each scenario
110
- const scenarios = ['search', 'thread', 'list', 'send', 'multi'];
111
- let totalWait = 0;
112
-
113
- for (let i = 0; i < scenarios.length; i++) {
114
- const scenario = scenarios[i];
115
- const duration = CONFIG.scenarioDurations[scenario];
116
- totalWait += duration;
117
-
118
- console.log(` 📍 Scenario ${i + 1}/${scenarios.length}: ${scenario} (${Math.round(duration/1000)}s)`);
119
- await page.waitForTimeout(duration);
120
- }
121
-
122
- // Wait for closing card
123
- console.log();
124
- console.log('🎬 Showing closing screen...');
125
- await page.waitForTimeout(CONFIG.outroDuration);
126
-
127
- // Exit fullscreen
128
- console.log('🔚 Exiting fullscreen...');
129
- await page.keyboard.press('Escape');
130
- await page.waitForTimeout(500);
131
-
132
- // Close context to flush video
133
- console.log('💾 Saving video...');
134
- const video = await page.video();
135
- await context.close();
136
- await browser.close();
137
-
138
- // Get the actual video path
139
- const videoPath = await video.path();
140
-
141
- console.log();
142
- console.log('╔════════════════════════════════════════════════════════════╗');
143
- console.log('║ ✅ Recording Complete! ║');
144
- console.log('╚════════════════════════════════════════════════════════════╝');
145
- console.log();
146
- copyFileSync(videoPath, canonicalOutput);
147
- console.log(`📹 Canonical video: ${canonicalOutput}`);
148
- if (archiveOutput) {
149
- copyFileSync(videoPath, timestampedOutput);
150
- console.log(`🗂️ Archived copy: ${timestampedOutput}`);
151
- }
152
- console.log();
153
- console.log('Next steps:');
154
- console.log(' 1. Review the canonical video in a media player');
155
- console.log(' 2. Convert to GIF with FFmpeg:');
156
- 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`);
157
- console.log(' 3. Re-run with --archive to keep timestamped historical outputs');
158
- }
159
-
160
- recordDemo().catch(err => {
161
- console.error('❌ Recording failed:', err.message);
162
- process.exit(1);
163
- });
@@ -1,247 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawnSync } from "child_process";
4
- import { mkdirSync, writeFileSync } from "fs";
5
- import { dirname, resolve } from "path";
6
- import { fileURLToPath } from "url";
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const ROOT = resolve(__dirname, "..");
10
- const REPORT_PATH = process.env.PREPUBLISH_REPORT_PATH
11
- ? resolve(ROOT, process.env.PREPUBLISH_REPORT_PATH)
12
- : resolve(ROOT, "output", "release-health", "prepublish-dry-run.md");
13
-
14
- const EXPECTED_NAME = process.env.EXPECTED_GIT_NAME || "jtalk22";
15
- const EXPECTED_EMAIL = process.env.EXPECTED_GIT_EMAIL || "james@revasser.nyc";
16
- const OWNER_RANGE = process.env.OWNER_CHECK_RANGE || "origin/main..HEAD";
17
-
18
- function run(command, args = [], options = {}) {
19
- return spawnSync(command, args, {
20
- cwd: ROOT,
21
- encoding: "utf8",
22
- env: process.env,
23
- maxBuffer: 20 * 1024 * 1024,
24
- ...options
25
- });
26
- }
27
-
28
- function trimOutput(text = "", maxChars = 1200) {
29
- const normalized = String(text || "").trim();
30
- if (!normalized) return "";
31
- if (normalized.length <= maxChars) return normalized;
32
- return `${normalized.slice(0, maxChars)}... [truncated]`;
33
- }
34
-
35
- function stepResult(name, command, ok, details = "", commandOutput = "") {
36
- return { name, command, ok, details, commandOutput };
37
- }
38
-
39
- function gitIdentityStep() {
40
- const nameResult = run("git", ["config", "--get", "user.name"]);
41
- const emailResult = run("git", ["config", "--get", "user.email"]);
42
-
43
- const actualName = nameResult.stdout.trim();
44
- const actualEmail = emailResult.stdout.trim();
45
- const ok =
46
- nameResult.status === 0 &&
47
- emailResult.status === 0 &&
48
- actualName === EXPECTED_NAME &&
49
- actualEmail === EXPECTED_EMAIL;
50
-
51
- const details = ok
52
- ? `Configured as ${actualName} <${actualEmail}>`
53
- : `Expected ${EXPECTED_NAME} <${EXPECTED_EMAIL}>, found ${actualName || "(missing)"} <${actualEmail || "(missing)"}>`;
54
-
55
- return stepResult(
56
- "Git identity",
57
- "git config --get user.name && git config --get user.email",
58
- ok,
59
- details,
60
- `${nameResult.stdout}${nameResult.stderr}${emailResult.stdout}${emailResult.stderr}`
61
- );
62
- }
63
-
64
- function ownerAttributionStep() {
65
- const result = run("bash", ["scripts/check-owner-attribution.sh", OWNER_RANGE]);
66
- return stepResult(
67
- "Owner attribution",
68
- `bash scripts/check-owner-attribution.sh ${OWNER_RANGE}`,
69
- result.status === 0,
70
- result.status === 0 ? "All commits in range are owner-attributed." : "Owner attribution check failed.",
71
- `${result.stdout}${result.stderr}`
72
- );
73
- }
74
-
75
- function publicLanguageStep() {
76
- const result = run("bash", ["scripts/check-public-language.sh"]);
77
- return stepResult(
78
- "Public language",
79
- "bash scripts/check-public-language.sh",
80
- result.status === 0,
81
- result.status === 0 ? "Public wording guardrail passed." : "Disallowed wording found.",
82
- `${result.stdout}${result.stderr}`
83
- );
84
- }
85
-
86
- function markerScanStep() {
87
- const pattern = "Co-authored-by|co-authored-by|Generated with|generated with";
88
- const scanPaths = [
89
- "README.md",
90
- "docs",
91
- "public",
92
- ".github/RELEASE_NOTES_TEMPLATE.md",
93
- ".github/ISSUE_REPLY_TEMPLATE.md"
94
- ];
95
- const result = run("rg", [
96
- "-n",
97
- pattern,
98
- "--glob",
99
- "!docs/release-health/**",
100
- "--glob",
101
- "!output/release-health/**",
102
- ...scanPaths
103
- ]);
104
-
105
- if (result.status === 1) {
106
- return stepResult(
107
- "Public attribution markers",
108
- "marker-scan",
109
- true,
110
- "No non-owner attribution markers found in public surfaces."
111
- );
112
- }
113
-
114
- return stepResult(
115
- "Public attribution markers",
116
- "marker-scan",
117
- false,
118
- result.status === 0
119
- ? "Found disallowed markers on public surfaces."
120
- : "Marker scan failed.",
121
- `${result.stdout}${result.stderr}`
122
- );
123
- }
124
-
125
- function runNodeStep(name, scriptPath, extraArgs = []) {
126
- const result = run("node", [scriptPath, ...extraArgs]);
127
- return stepResult(
128
- name,
129
- `node ${scriptPath}${extraArgs.length ? ` ${extraArgs.join(" ")}` : ""}`,
130
- result.status === 0,
131
- result.status === 0 ? "Passed." : "Failed.",
132
- `${result.stdout}${result.stderr}`
133
- );
134
- }
135
-
136
- function npmPackSnapshot() {
137
- const result = run("npm", ["pack", "--dry-run", "--json"]);
138
- if (result.status !== 0) {
139
- return {
140
- ok: false,
141
- details: "Unable to generate npm pack snapshot.",
142
- output: `${result.stdout}${result.stderr}`
143
- };
144
- }
145
-
146
- try {
147
- const parsed = JSON.parse(result.stdout);
148
- const entry = Array.isArray(parsed) ? parsed[0] : parsed;
149
- const fileCount = Array.isArray(entry.files) ? entry.files.length : 0;
150
- const details = `package size ${entry.size} bytes, unpacked ${entry.unpackedSize} bytes, files ${fileCount}`;
151
- return { ok: true, details, output: result.stdout };
152
- } catch (error) {
153
- return {
154
- ok: false,
155
- details: "npm pack output was not valid JSON.",
156
- output: `${result.stdout}\n${String(error)}`
157
- };
158
- }
159
- }
160
-
161
- function buildReport(results, packSnapshot) {
162
- const generated = new Date().toISOString();
163
- const failed = results.filter((step) => !step.ok);
164
- const lines = [];
165
- lines.push("# Prepublish Dry Run");
166
- lines.push("");
167
- lines.push(`- Generated: ${generated}`);
168
- lines.push(`- Expected owner: \`${EXPECTED_NAME} <${EXPECTED_EMAIL}>\``);
169
- lines.push(`- Owner range: \`${OWNER_RANGE}\``);
170
- lines.push("");
171
- lines.push("## Step Matrix");
172
- lines.push("");
173
- lines.push("| Step | Status | Command | Details |");
174
- lines.push("|---|---|---|---|");
175
- for (const step of results) {
176
- lines.push(`| ${step.name} | ${step.ok ? "pass" : "fail"} | \`${step.command}\` | ${step.details} |`);
177
- }
178
- lines.push("");
179
- lines.push("## npm Pack Snapshot");
180
- lines.push("");
181
- lines.push(`- Status: ${packSnapshot.ok ? "pass" : "fail"}`);
182
- lines.push(`- Details: ${packSnapshot.details}`);
183
- lines.push("");
184
-
185
- if (failed.length === 0 && packSnapshot.ok) {
186
- lines.push("## Result");
187
- lines.push("");
188
- lines.push("Prepublish dry run passed.");
189
- } else {
190
- lines.push("## Result");
191
- lines.push("");
192
- lines.push("Prepublish dry run failed.");
193
- lines.push("");
194
- lines.push("### Failing checks");
195
- for (const step of failed) {
196
- lines.push(`- ${step.name}`);
197
- }
198
- if (!packSnapshot.ok) lines.push("- npm pack snapshot");
199
- }
200
-
201
- lines.push("");
202
- lines.push("## Command Output (Truncated)");
203
- lines.push("");
204
- for (const step of results) {
205
- lines.push(`### ${step.name}`);
206
- lines.push("");
207
- lines.push("```text");
208
- lines.push(trimOutput(step.commandOutput) || "(no output)");
209
- lines.push("```");
210
- lines.push("");
211
- }
212
- lines.push("### npm Pack Snapshot");
213
- lines.push("");
214
- lines.push("```text");
215
- lines.push(trimOutput(packSnapshot.output, 4000) || "(no output)");
216
- lines.push("```");
217
- lines.push("");
218
-
219
- return lines.join("\n");
220
- }
221
-
222
- function main() {
223
- const steps = [
224
- gitIdentityStep(),
225
- ownerAttributionStep(),
226
- publicLanguageStep(),
227
- markerScanStep(),
228
- runNodeStep("Core verification", "scripts/verify-core.js"),
229
- runNodeStep("Web verification", "scripts/verify-web.js"),
230
- runNodeStep("Install-flow verification", "scripts/verify-install-flow.js"),
231
- runNodeStep("Version parity", "scripts/check-version-parity.js", ["--allow-propagation"])
232
- ];
233
-
234
- const packSnapshot = npmPackSnapshot();
235
- const report = buildReport(steps, packSnapshot);
236
-
237
- mkdirSync(dirname(REPORT_PATH), { recursive: true });
238
- writeFileSync(REPORT_PATH, `${report}\n`);
239
-
240
- console.log(`Wrote ${REPORT_PATH}`);
241
- const failed = steps.some((step) => !step.ok) || !packSnapshot.ok;
242
- if (failed) {
243
- process.exitCode = 1;
244
- }
245
- }
246
-
247
- main();
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- repo_root="$(git rev-parse --show-toplevel)"
5
- cd "$repo_root"
6
-
7
- [[ -d .githooks ]] || {
8
- echo "Missing .githooks directory." >&2
9
- exit 1
10
- }
11
-
12
- git config core.hooksPath .githooks
13
- find .githooks -maxdepth 1 -type f -exec chmod +x {} +
14
-
15
- echo "Configured git hooks path: .githooks"
@@ -1,208 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { chromium } from "playwright";
4
- import { existsSync, mkdirSync } from "node:fs";
5
- import { dirname, resolve } from "node:path";
6
-
7
- const argv = process.argv.slice(2);
8
- const hasFlag = (flag) => argv.includes(flag);
9
- const argValue = (flag) => {
10
- const idx = argv.indexOf(flag);
11
- return idx >= 0 && idx + 1 < argv.length ? argv[idx + 1] : null;
12
- };
13
-
14
- const repo = argValue("--repo") || "jtalk22/slack-mcp-server";
15
- const imagePath = resolve(
16
- argValue("--image") || "docs/images/social-preview-v3.png"
17
- );
18
- const profileDir = resolve(
19
- argValue("--profile-dir") || ".cache/playwright/github-social-preview"
20
- );
21
- const evidencePath = resolve(
22
- argValue("--evidence") || "output/release-health/social-preview-settings.png"
23
- );
24
- const headed = hasFlag("--headed");
25
- const loginTimeoutMs = Number(argValue("--login-timeout-ms") || 10 * 60 * 1000);
26
- const settingsUrl = `https://github.com/${repo}/settings`;
27
-
28
- if (!existsSync(imagePath)) {
29
- console.error(`Missing image: ${imagePath}`);
30
- process.exit(1);
31
- }
32
-
33
- console.log(`Repo: ${repo}`);
34
- console.log(`Image: ${imagePath}`);
35
- console.log(`Profile: ${profileDir}`);
36
- console.log(`Opening: ${settingsUrl}`);
37
-
38
- mkdirSync(profileDir, { recursive: true });
39
- mkdirSync(dirname(evidencePath), { recursive: true });
40
-
41
- const context = await chromium.launchPersistentContext(profileDir, {
42
- channel: "chrome",
43
- headless: !headed,
44
- viewport: { width: 1440, height: 1100 },
45
- });
46
- const page = await context.newPage();
47
-
48
- const getSocialPreviewImageSrc = async () => {
49
- return page.evaluate(() => {
50
- const extractRepoImageUrl = (value) => {
51
- if (!value || value === "none") return null;
52
- const match = value.match(/https:\/\/repository-images\.githubusercontent\.com\/[^")]+/);
53
- return match ? match[0] : null;
54
- };
55
-
56
- const headings = Array.from(document.querySelectorAll("h1, h2, h3, h4, strong"));
57
- const socialHeading = headings.find((el) => el.textContent?.trim() === "Social preview");
58
- if (!socialHeading) return null;
59
-
60
- let node = socialHeading.parentElement;
61
- for (let depth = 0; depth < 6 && node; depth += 1) {
62
- const img = node.querySelector("img");
63
- if (img?.src) return img.src;
64
-
65
- const ownBg = extractRepoImageUrl(getComputedStyle(node).backgroundImage);
66
- if (ownBg) return ownBg;
67
-
68
- const descendants = Array.from(node.querySelectorAll("*"));
69
- for (const child of descendants) {
70
- const bg = extractRepoImageUrl(getComputedStyle(child).backgroundImage);
71
- if (bg) return bg;
72
- }
73
-
74
- node = node.parentElement;
75
- }
76
-
77
- const fallback = Array.from(document.querySelectorAll("img")).find((img) =>
78
- img.src.includes("repository-images.githubusercontent.com")
79
- );
80
- if (fallback?.src) return fallback.src;
81
-
82
- const bgFallback = Array.from(document.querySelectorAll("*"))
83
- .map((el) => extractRepoImageUrl(getComputedStyle(el).backgroundImage))
84
- .find(Boolean);
85
- return bgFallback || null;
86
- });
87
- };
88
-
89
- const waitForAuthenticatedSettings = async () => {
90
- const started = Date.now();
91
- while (Date.now() - started < loginTimeoutMs) {
92
- const url = page.url();
93
- const title = await page.title().catch(() => "");
94
- const onSettings = url.includes(`/${repo}/settings`) && !title.includes("Page not found");
95
- if (onSettings) return;
96
- await page.waitForTimeout(1200);
97
- }
98
- throw new Error(
99
- `Timed out waiting for authenticated settings page after ${loginTimeoutMs}ms`
100
- );
101
- };
102
-
103
- try {
104
- await page.goto(settingsUrl, { waitUntil: "domcontentloaded", timeout: 120000 });
105
-
106
- const initialTitle = await page.title().catch(() => "");
107
- if (initialTitle.includes("Page not found") || page.url().includes("/login")) {
108
- console.log("GitHub settings not authenticated in this browser context.");
109
- console.log("Please sign in in the opened browser window, then keep this process running.");
110
- await waitForAuthenticatedSettings();
111
- }
112
-
113
- // GitHub settings pages often keep long-lived background requests, so
114
- // networkidle can hang even when UI is interactive.
115
- await page.waitForLoadState("domcontentloaded", { timeout: 120000 });
116
-
117
- const socialHeading = page.getByText("Social preview", { exact: true }).first();
118
- await socialHeading.waitFor({ timeout: 120000 });
119
- await socialHeading.scrollIntoViewIfNeeded();
120
- const beforeImageSrc = await getSocialPreviewImageSrc();
121
-
122
- // In current GitHub settings, upload is often behind an Edit button.
123
- const editButtons = page.getByRole("button", { name: /^Edit$/i });
124
- const editCount = await editButtons.count();
125
- if (editCount > 0) {
126
- for (let i = 0; i < editCount; i += 1) {
127
- const btn = editButtons.nth(i);
128
- if (await btn.isVisible()) {
129
- try {
130
- await btn.click({ timeout: 3000 });
131
- break;
132
- } catch {
133
- // Try next visible Edit button.
134
- }
135
- }
136
- }
137
- }
138
-
139
- const fileInput = page.locator('#repo-image-file-input, input[type="file"]').first();
140
- await fileInput.waitFor({ state: "attached", timeout: 90000 });
141
- await fileInput.setInputFiles(imagePath);
142
- await page.waitForTimeout(1500);
143
- const afterUploadImageSrc = await getSocialPreviewImageSrc();
144
- const previewUpdated =
145
- Boolean(afterUploadImageSrc) && afterUploadImageSrc !== beforeImageSrc;
146
- const previewAlreadyPresent =
147
- Boolean(beforeImageSrc) &&
148
- Boolean(afterUploadImageSrc) &&
149
- beforeImageSrc === afterUploadImageSrc;
150
-
151
- // GitHub UI labels can vary; try likely save/update actions.
152
- const saveCandidates = [
153
- /update social preview/i,
154
- /update social image/i,
155
- /save changes/i,
156
- /^save$/i,
157
- /upload/i,
158
- /^apply$/i,
159
- /^done$/i,
160
- ];
161
-
162
- let clicked = false;
163
- for (const rx of saveCandidates) {
164
- const button = page.getByRole("button", { name: rx }).first();
165
- const count = await button.count();
166
- if (count > 0) {
167
- try {
168
- await button.scrollIntoViewIfNeeded();
169
- await button.click({ timeout: 5000 });
170
- clicked = true;
171
- break;
172
- } catch {
173
- // Keep trying other candidates.
174
- }
175
- }
176
- }
177
-
178
- if (!clicked && !previewUpdated) {
179
- const visibleButtons = await page
180
- .locator("button:visible")
181
- .evaluateAll((els) => els.map((el) => el.textContent?.trim() || "").filter(Boolean))
182
- .catch(() => []);
183
- console.log("Visible buttons during upload:", visibleButtons.join(" | "));
184
- }
185
-
186
- if (previewUpdated) {
187
- console.log("Social preview image updated in settings preview.");
188
- } else if (previewAlreadyPresent) {
189
- console.log("Social preview image is already present; no save action required.");
190
- } else if (!clicked) {
191
- console.log(
192
- "Uploaded image file to input; could not confidently click a save button automatically."
193
- );
194
- console.log("Complete the final click in the open browser if needed.");
195
- } else {
196
- await page.waitForTimeout(2500);
197
- console.log("Social preview update action submitted.");
198
- }
199
-
200
- await page.screenshot({ path: evidencePath, fullPage: true });
201
- console.log(`Saved evidence screenshot: ${evidencePath}`);
202
- console.log("Done.");
203
- } catch (error) {
204
- console.error(error instanceof Error ? error.message : String(error));
205
- process.exitCode = 1;
206
- } finally {
207
- await context.close();
208
- }