
Twitter Reader
Clip full X/Twitter articles with images into local Markdown for competitor scans and research vaults.
Overview
twitter-reader is an agent skill for the Idea phase that fetches X/Twitter articles with images and saves them as local Markdown clippings for solo-builder research.
Install
npx skills add https://github.com/daymade/claude-code-skills --skill twitter-readerWhat is this skill?
- Fetches structured article data via `uv run --with twitter-cli twitter article`
- Downloads all inline images into an attachments folder beside the note
- Optional Jina reader fallback via `JINA_API_KEY` and curl when you need cleaned text
- Emits Markdown with embedded image paths and metadata for Obsidian or repo docs
- CLI args: article URL plus optional output directory (e.g. ./Clippings)
- Optional Jina API path via JINA_API_KEY
- CLI: article URL plus optional output_dir
Adoption & trust: 1.7k installs on skills.sh; 1.2k GitHub stars; 2/3 security scanners passed (skills.sh audits).
What problem does it solve?
You found a long X post or article you want in your notes, but copy-paste drops images and structure.
Who is it for?
Solo builders who monitor X for product ideas, launches, and competitor narratives and want offline Markdown archives.
Skip if: Builders who only need a one-line quote, cannot run Python/uv in their environment, or must not use third-party reader APIs.
When should I use this skill?
User shares an x.com or twitter.com article URL and wants Markdown plus downloaded images in a folder.
What do I get? / Deliverables
You get a Markdown file plus an attachments folder you can commit, search, and summarize in your agent workflow.
- Markdown article file
- attachments directory with images
Recommended Skills
Journey fit
Spans multiple journey phases - primary shelf plus alternate fits below.
Canonical shelf is Idea → research because builders most often pull threads and long posts while validating markets and tracking founders. Research subphase covers saving external signal; this skill turns share URLs into structured clippings instead of manual copy-paste.
Where it fits
Archive a competitor’s X article before writing your positioning doc.
Save a founder’s pricing thread as Markdown while scoping your MVP.
Clip a viral thread with images to draft your own newsletter outline.
How it compares
Use instead of manual screenshots or browser-only save; it is a local clip script, not a Twitter API dashboard or MCP server.
Common Questions / FAQ
Who is twitter-reader for?
Indie developers and content-oriented builders who research on X and want durable Markdown clippings with images in their project or vault.
When should I use twitter-reader?
During Idea research when archiving competitor threads; during Validate when saving founder proof posts; during Grow when clipping distribution examples to repurpose—any time an article URL on X should become a local note.
Is twitter-reader safe to install?
Review the Security Audits panel on this Prism page for scan results; the skill runs subprocesses (uv, curl) and needs network access to X and optional Jina endpoints—treat API keys as secrets.
SKILL.md
READMESKILL.md - Twitter Reader
Security scan passed Scanned at: 2026-04-06T16:24:46.159857 Tool: gitleaks + pattern-based validation Content hash: bfcc70ffa4bfda5e6308942605ad8dc3cf62e8aeade5b00c6d9b774bebb190fb #!/usr/bin/env python3 """ Fetch Twitter/X Article with images using twitter-cli. Usage: python fetch_article.py <article_url> [output_dir] Example: python fetch_article.py https://x.com/HiTw93/status/2040047268221608281 ./Clippings Features: - Fetches structured data via twitter-cli - Downloads all images to attachments folder - Generates Markdown with embedded image references """ import sys import os import re import subprocess import argparse from pathlib import Path from datetime import datetime def run_twitter_cli(url: str) -> dict: """Fetch article data using twitter-cli via uv run.""" cmd = ["uv", "run", "--with", "twitter-cli", "twitter", "article", url] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(f"Error fetching article: {result.stderr}", file=sys.stderr) sys.exit(1) return parse_yaml_output(result.stdout) def run_jina_api(url: str) -> str: """Fetch article text with images using Jina API.""" api_key = os.getenv("JINA_API_KEY", "") jina_url = f"https://r.jina.ai/{url}" cmd = ["curl", "-s", jina_url] if api_key: cmd.extend(["-H", f"Authorization: Bearer {api_key}"]) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(f"Warning: Jina API failed: {result.stderr}", file=sys.stderr) return "" return result.stdout def parse_yaml_output(output: str) -> dict: """Parse twitter-cli YAML output into dict.""" try: import yaml data = yaml.safe_load(output) if data.get("ok") and "data" in data: return data["data"] return data except ImportError: print("Error: PyYAML required. Install with: uv pip install pyyaml", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Error parsing YAML: {e}", file=sys.stderr) sys.exit(1) def extract_image_urls(text: str) -> list: """Extract image URLs from markdown text.""" # Extract all pbs.twimg.com URLs (note: twimg not twitter) pattern = r'https://pbs\.twimg\.com/media/[^\s\)"\']+' matches = re.findall(pattern, text) # Deduplicate and normalize to large size seen = set() urls = [] for url in matches: base_url = url.split('?')[0] if base_url not in seen: seen.add(base_url) urls.append(f"{base_url}?format=jpg&name=large") return urls def download_images(image_urls: list, attachments_dir: Path) -> list: """Download images and return list of local paths.""" attachments_dir.mkdir(parents=True, exist_ok=True) local_paths = [] for i, url in enumerate(image_urls, 1): filename = f"{i:02d}-image.jpg" filepath = attachments_dir / filename cmd = ["curl", "-sL", url, "-o", str(filepath)] result = subprocess.run(cmd, capture_output=True) if result.returncode == 0 and filepath.exists() and filepath.stat().st_size > 0: local_paths.append(f"attachments/{attachments_dir.name}/{filename}") print(f" ✓ {filename}") else: print(f" ✗ Failed: {filename}") return local_paths def replace_image_urls(text: str, image_urls: list, local_paths: list) -> str: """Replace remote image URLs with local paths in markdown text.""" for remote_url, local_path in zip(image_urls, local_paths): # Extract base URL pattern base_url = remote_url.split('?')[0].replace('?format=jpg&name=large', '') # Replace all variations of this URL pattern = re.escape(base_url) + r'(\?[^\)]*)?' text = re.sub(pattern, local_path, text) return text def sanitize_filename(name: str) -> str: """Sanitize string for use i