labali-x-download-post-assets
MANDATORY — load references/plan.md before any browser or extraction action begins.
⚠️ NEVER WRITE YOUR OWN SCRIPT
The download logic is fully implemented. Always invoke the existing script — do NOT write a new one.
cd ~/.claude-stella/skills/labali-x-download-post-assets
npx tsx scripts/run.ts \
--post-url "<url>" \
--output-dir "$HOME/Downloads/x"
The sections below (image extraction, anti-detection, MHTML generation) are implementation documentation for the script itself, not instructions for you to re-implement. If the script doesn't exist or can't run, report the error — never substitute with hand-written Playwright code.
URL Expansion (t.co Shortlinks)
Tweet links render as t.co shortlinks (e.g., https://t.co/yr4YXZ6SgU) — both in href attributes and textContent. post.md must output the full resolved URL (e.g., https://github.com/anthropics/skills/tree/main/skills/pptx).
Because Twitter's Content Security Policy (CSP) blocks cross-origin fetch() inside page.evaluate(), URL expansion must be done in Node.js:
- Collect all
<a> element href attributes inside page.evaluate()
- Call
fetch(href, { redirect: "follow" }) on https://t.co/* links from Node.js to get the final URL
- Replace each anchor's
textContent with the resolved URL, then extract full text
This step is integrated into extractTweetSnapshot() and must not be skipped.
Required Constraints
- Use browser automation only.
- Do not use X private APIs.
- Reuse manual-login session via unified Chrome CDP startup:
open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-labali" --no-proxy-server.
- Prefer semantic extraction from visible page state and loaded resources.
- Download only target post assets: images plus optional post video.
- Generate
post.md for extracted text metadata.
- Do not generate
manifest.json.
- Preserve all query parameters for page navigation.
Anti-Detection Principles
X applies behavioral analysis to detect automation. Violations of these principles may result in account restrictions.
Core test — apply before every browser action:
"Would a real user do this, from this state, at this moment?"
If no → skip it or slow it down. Re-navigating an already-open post, batch-extracting DOM nodes, issuing fresh HTTP requests for images the browser just loaded, using fixed delays — all fail this test.
Navigation:
- If the tab is already on the target post URL, skip
page.goto() entirely.
- All fixed
waitForTimeout values must be randomized (e.g., base + Math.random() * range).
- After navigating to a post, scroll down briefly to simulate reading, then scroll back up before interacting with images.
Image acquisition:
- Never issue new HTTP requests for images — the browser has already downloaded them.
- Register
page.on("response") BEFORE calling page.goto() — images load during navigation.
- Primary image source: DOM extraction from
[data-testid="tweetPhoto"] — handles both <img> elements (regular tweets) and CSS background-image divs (X Notes).
- Fetch each URL via
fetch(url, {cache: 'force-cache'}) in page.evaluate().
- After download: deduplicate by SHA-256 content hash; remove files < 40% of median size.
MHTML Generation (CDP Page.captureSnapshot):
- Calls Chrome's built-in
Page.captureSnapshot command via Playwright CDPSession to generate standard MHTML
- Before archiving, automatically removes UI elements unrelated to post content (login buttons, nav bars, recommended content, etc.)
cleanupPageForArchive removes: LoginForm, signup links, banner nav, sidebarColumn, app-bar, etc.
- Text-only tweets (no images/video) skip MHTML and only generate
post.md
- X Notes and posts with images/video generate
article.mhtml + post.md
Video:
- Before downloading, simulate user engagement: bring the tab to front, click the video element, wait 3–5 seconds (randomized) for buffering.
- Video download uses
page.request.get().
General:
- Always operate within the user's authenticated Chrome session (CDP reuse) — never launch a headless or separate browser.
- Never manipulate the DOM beyond what a user's own browser JS would do.
NEVER
- Never launch a new Chrome if CDP is already responding on port 9223.
- Never hijack a non-X browser tab — reuse X tab or open a new X tab.
- Never strip query params from post URL before navigating.
- Never use fixed (non-randomized) delays.
- Never retry after CAPTCHA or rate-limit signal.
- Never report success without verifying output files exist.
Success Criteria
A run is successful only when all conditions hold:
- An output folder is created:
<output_dir>/<YYYYMMDD>-<tweet_id>/
post.md is generated in the folder with author, text, timestamp, URL.
- Post image files are saved as
image_01.jpg, image_02.jpg, etc.
- Post video file is saved as
video.mp4 when the post contains video.
article.mhtml is generated for X Notes and posts with images/video; text-only tweets omit it.
- Files are deduplicated by content hash.
- URL output and logs use canonical
x.com form.
Operational Mode
- Default: guided browser flow + semantic extraction + authenticated media download.
- Startup:
- Check if CDP responds:
curl -s http://localhost:9223/json/version
- If CDP NOT responding → auto-launch Chrome immediately:
open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-labali" --no-proxy-server; wait 3s; verify CDP responds
- If Chrome with remote debugging is already running, reuse it
- Find existing x.com tab → reuse it; if none → open new tab
- Check login state; if wall detected → guide user to complete login manually; continue in same session
- Input:
- After startup, if
post_url is missing → prompt user interactively
- If
output_dir is missing → prompt with default ~/Downloads/x
Resources
| When |
Must load |
Do NOT load |
| Always — at skill invocation start |
references/plan.md |
references/architecture.md |
| Extraction returns wrong count or fails |
references/architecture.md |
— |
| Video download unclear |
references/architecture.md |
— |
See Resources table above for conditional loads.
1---2name: labali-x-download-post-assets3description: Download X (Twitter) post assets — text, images, video — to a local folder using browser automation with manual-login session reuse. Use when downloading an X post, saving post images, exporting post content, or archiving a tweet. Trigger phrases: "download x post", "save tweet", "x post images", "x video download", "twitter post assets".4license: MIT5---67# labali-x-download-post-assets89> **MANDATORY — load `references/plan.md` before any browser or extraction action begins.**1011## ⚠️ NEVER WRITE YOUR OWN SCRIPT1213**The download logic is fully implemented. Always invoke the existing script — do NOT write a new one.**1415```bash16cd ~/.claude-stella/skills/labali-x-download-post-assets17npx tsx scripts/run.ts \18 --post-url "<url>" \19 --output-dir "$HOME/Downloads/x"20```2122The sections below (image extraction, anti-detection, MHTML generation) are **implementation documentation for the script itself**, not instructions for you to re-implement. If the script doesn't exist or can't run, report the error — never substitute with hand-written Playwright code.2324### URL Expansion (t.co Shortlinks)2526Tweet links render as t.co shortlinks (e.g., `https://t.co/yr4YXZ6SgU`) — both in `href` attributes and `textContent`. **post.md must output the full resolved URL** (e.g., `https://github.com/anthropics/skills/tree/main/skills/pptx`).2728Because Twitter's Content Security Policy (CSP) blocks cross-origin `fetch()` inside `page.evaluate()`, URL expansion must be done in Node.js:29301. Collect all `<a>` element `href` attributes inside `page.evaluate()`312. Call `fetch(href, { redirect: "follow" })` on `https://t.co/*` links from Node.js to get the final URL323. Replace each anchor's `textContent` with the resolved URL, then extract full text3334This step is integrated into `extractTweetSnapshot()` and **must not be skipped**.3536## Required Constraints3738- Use browser automation only.39- Do not use X private APIs.40- Reuse manual-login session via unified Chrome CDP startup:41 `open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-labali" --no-proxy-server`.42- Prefer semantic extraction from visible page state and loaded resources.43- Download only target post assets: images plus optional post video.44- Generate `post.md` for extracted text metadata.45- Do not generate `manifest.json`.46- Preserve all query parameters for page navigation.4748## Anti-Detection Principles4950X applies behavioral analysis to detect automation. Violations of these principles may result in account restrictions.5152**Core test — apply before every browser action:**53> "Would a real user do this, from this state, at this moment?"54> If no → skip it or slow it down. Re-navigating an already-open post, batch-extracting DOM nodes, issuing fresh HTTP requests for images the browser just loaded, using fixed delays — all fail this test.5556**Navigation:**57- If the tab is already on the target post URL, skip `page.goto()` entirely.58- All fixed `waitForTimeout` values must be randomized (e.g., `base + Math.random() * range`).59- After navigating to a post, scroll down briefly to simulate reading, then scroll back up before interacting with images.6061**Image acquisition:**62- Never issue new HTTP requests for images — the browser has already downloaded them.63- **Register `page.on("response")` BEFORE calling `page.goto()`** — images load during navigation.64- **Primary image source: DOM extraction from `[data-testid="tweetPhoto"]`** — handles both `<img>` elements (regular tweets) and CSS `background-image` divs (X Notes).65- Fetch each URL via `fetch(url, {cache: 'force-cache'})` in `page.evaluate()`.66- After download: deduplicate by SHA-256 content hash; remove files < 40% of median size.6768**MHTML Generation (CDP Page.captureSnapshot):**69- Calls Chrome's built-in `Page.captureSnapshot` command via Playwright CDPSession to generate standard MHTML70- Before archiving, automatically removes UI elements unrelated to post content (login buttons, nav bars, recommended content, etc.)71- `cleanupPageForArchive` removes: LoginForm, signup links, banner nav, sidebarColumn, app-bar, etc.72- Text-only tweets (no images/video) skip MHTML and only generate `post.md`73- X Notes and posts with images/video generate `article.mhtml` + `post.md`7475**Video:**76- Before downloading, simulate user engagement: bring the tab to front, click the video element, wait 3–5 seconds (randomized) for buffering.77- Video download uses `page.request.get()`.7879**General:**80- Always operate within the user's authenticated Chrome session (CDP reuse) — never launch a headless or separate browser.81- Never manipulate the DOM beyond what a user's own browser JS would do.8283## NEVER8485- Never launch a new Chrome if CDP is already responding on port 9223.86- Never hijack a non-X browser tab — reuse X tab or open a new X tab.87- Never strip query params from post URL before navigating.88- Never use fixed (non-randomized) delays.89- Never retry after CAPTCHA or rate-limit signal.90- Never report success without verifying output files exist.9192## Success Criteria9394A run is successful only when all conditions hold:95961. An output folder is created: `<output_dir>/<YYYYMMDD>-<tweet_id>/`972. `post.md` is generated in the folder with author, text, timestamp, URL.983. Post image files are saved as `image_01.jpg`, `image_02.jpg`, etc.994. Post video file is saved as `video.mp4` when the post contains video.1005. `article.mhtml` is generated for X Notes and posts with images/video; text-only tweets omit it.1016. Files are deduplicated by content hash.1027. URL output and logs use canonical `x.com` form.103104## Operational Mode105106- Default: guided browser flow + semantic extraction + authenticated media download.107- Startup:108 - Check if CDP responds: `curl -s http://localhost:9223/json/version`109 - If CDP NOT responding → auto-launch Chrome immediately: `open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-labali" --no-proxy-server`; wait 3s; verify CDP responds110 - If Chrome with remote debugging is already running, reuse it111 - Find existing x.com tab → reuse it; if none → open new tab112 - Check login state; if wall detected → guide user to complete login manually; continue in same session113- Input:114 - After startup, if `post_url` is missing → prompt user interactively115 - If `output_dir` is missing → prompt with default `~/Downloads/x`116117## Resources118119| When | Must load | Do NOT load |120|------|-----------|-------------|121| Always — at skill invocation start | `references/plan.md` | `references/architecture.md` |122| Extraction returns wrong count or fails | `references/architecture.md` | — |123| Video download unclear | `references/architecture.md` | — |124125See Resources table above for conditional loads.