Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
cristoslc avatar

Media Summary

  • 21 installs
  • Updated June 30, 2026
  • cristoslc/media-summary

Downloads and summarizes audio/video, X/Twitter threads, and web articles into a structured markdown summary, publishes it as a public GitHub Gist, and opens it.

About

Ingests a media, X/Twitter, or web-article URL, produces a transcript (speech, captions, or OCR), and writes a structured markdown summary published as a GitHub Gist. A developer uses it to summarize podcasts, videos, threads, or articles from a single URL.

  • Handles speech transcripts, on-screen OCR, fxtwitter thread unrolling, and HTML ingestion
  • Saves markdown locally, publishes a public Gist, and opens it in the default app

Media Summary by the numbers

  • 21 all-time installs (skills.sh)
  • Ranked #1,286 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
  • Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cristoslc/media-summary --skill media-summary

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs21
Last updatedJune 30, 2026
Repositorycristoslc/media-summary

What it does

Downloads and summarizes audio/video, X/Twitter threads, and web articles into a structured markdown summary, publishes it as a public GitHub Gist, and opens it.

Files

SKILL.mdMarkdownGitHub ↗

The user has provided a media URL: $ARGUMENTS

Follow these steps exactly:

Step 0 — Bootstrap dependencies

Run the bootstrap script (scripts/bootstrap.sh relative to this skill's directory). It installs missing tools, verifies gh is authenticated, and skips subsequent runs via a marker file.

bash "<SKILL_DIR>/scripts/bootstrap.sh"

If it exits non-zero, stop and tell the user what to fix before continuing.

Step 1 — Classify source and acquire transcript

Inspect the URL and dispatch to the matching leg. Each leg ends with /tmp/media_clean_transcript.txt written. Some legs also pre-set CONTENT_TYPE (consumed in Step 4b).

SourceDetectLegPre-sets CONTENT_TYPE?
X/Twitter thread`(x\twitter\fxtwitter\
Web article / HTML pageAny URL that is not video/audio/thread media1d (HTML ingestion)yes → html-article
Instagraminstagram.com1b (yt-dlp native)no
Facebookfacebook.com/.../videos/1e (yt-dlp native)no
YouTubeyoutube.com, youtu.be1c (yt-dlp)no
Podcast / talk / other(everything else)1c — YouTube search, then audio-only detectionno

Step 1a — X/Twitter thread

Run the thread fetcher. It calls fxtwitter's /2/thread/{id} endpoint and writes both the raw JSON and a stitched transcript:

uv run "<SKILL_DIR>/scripts/fetch_x_thread.py" "<URL>"

The script prints a metadata JSON object to stdout — capture its fields (author_name, author_handle, author_url, published_date, tweet_count, title_guess, source_url, post_urls) for Steps 4c and 5.

Set CONTENT_TYPE=x-thread. Skip Steps 2 and 3 — proceed directly to Step 4.

If the script exits non-zero (empty thread, or a thread-opener that only returned one post because the public fxtwitter deployment lacks an account proxy), fall through to Step 1d (generic HTML ingestion) instead of stopping. The single post's text will be extracted as a web article.

Step 1b — Instagram

Keep the original URL and proceed to Step 2 with --cookies-from-browser (yt-dlp handles Instagram natively — see Step 2 for the flag).

Step 1c — YouTube / podcast / other

If already a YouTube URL (youtube.com or youtu.be), use it directly.

Otherwise (Apple Podcasts, Spotify, podcast pages, conference sites, direct audio URLs, etc.):

1. Extract the episode title — Use fetch_html.py (Tier 1) or the MCP browser snapshot to get the page title. For direct audio URLs (.mp3, .m4a, .wav, .aac, .ogg, .opus), derive a title from the URL slug (strip extension, replace hyphens/underscores with spaces, title-case).

2. Search YouTube — Use mcp__MCP_DOCKER__brave_web_search to search for "<title> <site>" (e.g. "683 atp.fm" or "episode title podcast name"). If a YouTube result matches the episode title, use that YouTube URL and proceed to Step 2.

3. If no YouTube match — Check if the URL is a direct audio file:

  • If the URL ends in .mp3, .m4a, .wav, .aac, .ogg, or .opus (or the Content-Type header from the page indicates audio), set AUDIO_ONLY=true and proceed to Step 2 with the original URL (yt-dlp will download the audio).
  • Otherwise, proceed to Step 2 with the original URL (yt-dlp will attempt to resolve it).

Step 1d — Web article / HTML page (generic HTML ingestion)

For URLs that point to text-based web content (LinkedIn posts, Medium articles, blog posts, Substack, news articles, etc.), ingest the page via a tiered extraction pipeline. This is also the fallback for fxtwitter failures (Step 1a) where only a single post was returned.

Step 1e — Facebook video

Facebook videos are handled by yt-dlp natively. Treat them like YouTube/Instagram:

1. Keep the original URL and proceed directly to Step 2. 2. yt-dlp will extract the en_US auto-caption set (if available) or the full transcript from the description field. 3. No special flags needed beyond what's already in Step 2.

Tier 1 — Specialty domain handler (run fetch_html.py which includes domain-specific logic):

uv run --with "readability-lxml,lxml,beautifulsoup4" "<SKILL_DIR>/scripts/fetch_html.py" "<URL>"

The script prints a JSON metadata object to stdout — capture its fields (title, author, published_date, description, site_name, source_url, content_length, needs_browser). It exits with code:

  • 0 — content extracted successfully, /tmp/media_clean_transcript.txt written. Set CONTENT_TYPE=html-article. Capture the metadata. Skip Steps 2 and 3 — proceed directly to Step 4.
  • 2 — content too thin (<200 chars), likely needs JavaScript rendering. Proceed to Tier 2.
  • 1 — hard error. Proceed to Tier 2.

Tier 2 — MCP browser tools (for JS-heavy pages like LinkedIn):

Use the MCP browser tools to render the page and extract content:

1. Navigate to the URL:

MCP_DOCKER_browser_navigate(url="<URL>")

2. Wait for content to load:

MCP_DOCKER_browser_wait_for(time=3)

3. Take an accessibility snapshot to extract the page text:

MCP_DOCKER_browser_snapshot()

4. Extract the meaningful text content from the snapshot. Look for the main article/post content area — skip navigation, sidebars, and footers.

5. Write the extracted text to /tmp/media_clean_transcript.txt.

6. If the snapshot yields substantive content (>200 chars), set CONTENT_TYPE=html-article. Skip Steps 2 and 3 — proceed directly to Step 4.

If MCP browser tools still yield insufficient content, proceed to Tier 3.

Tier 3 — Puppeteer (full headless Chromium rendering):

Run the Puppeteer renderer:

node "<SKILL_DIR>/scripts/fetch_html_puppeteer.js" "<URL>"

Requires puppeteer npm package — bootstrap.sh installs it on first use. The script:

  • Launches headless Chromium
  • Navigates to the URL, waits for networkidle2
  • Scrolls to trigger lazy-loaded content
  • Tries article-specific selectors (article, main, .post-content, LinkedIn feed selectors, etc.)
  • Falls back to document.body.innerText

Exits with code 0 on success (transcript written), 2 if content is still insufficient, 1 on hard error.

On success, set CONTENT_TYPE=html-article. Skip Steps 2 and 3 — proceed directly to Step 4.

Tier 4 — MCP convert_to_markdown (last resort for any URL):

MCP_DOCKER_convert_to_markdown(uri="<URL>")

This sends the URL to the MCP server which fetches and converts to markdown. Extract the main content from the returned markdown. Write to /tmp/media_clean_transcript.txt. Set CONTENT_TYPE=html-article. Skip Steps 2 and 3 — proceed directly to Step 4.

If all four tiers fail, report the error to the user and stop.

HTML metadata capture: Regardless of which tier succeeds, extract the following from the page for use in Steps 4c and 5: title, author, published_date, description, site_name, source_url. These come from the script stdout (Tier 1/3) or must be extracted manually from the MCP browser snapshot (Tier 2) or markdown output (Tier 4).

Step 2 — Download the transcript with yt-dlp

Run a single yt-dlp call to download subtitles and metadata:

bash "<SKILL_DIR>/scripts/yt-dlp.sh" --write-auto-sub --sub-langs en,en_US --write-info-json --skip-download -o "/tmp/media_transcript" "<URL>"

yt-dlp writes subtitle files in whatever format the platform serves (VTT for YouTube, SRT for Facebook, etc.). The post-processing script in Step 2a discovers the file dynamically — no ffmpeg dependency.

If AUDIO_ONLY=true (set in Step 1c for direct audio URLs) and yt-dlp fails to extract info, pass the original URL directly to Step 2c — transcribe_audio.py accepts URLs and handles audio extraction internally.

Note: For Instagram URLs, add --cookies-from-browser BROWSER, where BROWSER is the user's default browser. Detect it with:

defaults read ~/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers 2>/dev/null | grep -B1 'https' | grep -o '"com\..*"' | head -1

Map the bundle ID: com.google.Chromechrome, com.apple.Safarisafari, org.mozilla.firefoxfirefox, com.brave.Browserbrave. Default to chrome if detection fails.

Run the post-processing script to check VTT/SRT, extract caption fallback, and detect audio-only content:

bash "<SKILL_DIR>/scripts/process_yt_output.sh"

The script prints a status token to stdout:

TokenMeaningNext step
VTT_OKVTT file exists and is non-emptyProceed to Step 3
CAPTION_OKNo VTT, but description (>100 chars) written as fallback to /tmp/media_clean_transcript.txtSkip Step 3 → Step 4
AUDIO_ONLYNo VTT, no caption; media is audio-onlyGo to Step 2c below
VIDEO_NO_SUBSNo VTT, no caption; has video streamsGo to Step 2b
CAPTION_THINDescription exists but ≤100 non-hashtag chars (also outputs AUDIO_ONLY or VIDEO_NO_SUBS)Follow the second token
INFO_MISSINGinfo.json not foundReport error to user

Step 2c — Whisper transcription (audio-only)

This step is reached only when process_yt_output.sh reported AUDIO_ONLY.

Transcribe with Whisper (local, no external API). transcribe_audio.py auto-detects the platform and uses Metal acceleration on Apple Silicon via mlx-whisper, falling back to CPU via faster-whisper on other platforms. It bundles its own ffmpeg via imageio-ffmpeg — no system ffmpeg needed. It accepts either a local file path or a URL:

AUDIO_URL=$(python3 -c "import json; info=json.load(open('/tmp/media_transcript.info.json')); print(info.get('url',''))")
if [ -z "$AUDIO_URL" ]; then
  AUDIO_URL="<URL>"
fi
uv run --with "mlx-whisper,imageio-ffmpeg" python3 "<SKILL_DIR>/scripts/transcribe_audio.py" "$AUDIO_URL" base

On non-Apple-Silicon machines, replace mlx-whisper with faster-whisper in the --with flag. If mlx-whisper fails on Apple Silicon, the script falls back to CPU automatically and warns the user.

This extracts audio and transcribes in one step, writing /tmp/media_clean_transcript.txt (one sentence per line, no timestamps). If transcription fails, skip to graceful degradation below.

Verify /tmp/media_clean_transcript.txt exists and is non-empty. If so, skip Step 3 — go directly to Step 4.

Graceful degradation — If Whisper transcription fails:

  • If process_yt_output.sh had reported CAPTION_THIN (description ≤100 chars), that text was not written. Manually check if a description exists in info.json and use it if >0 chars.
  • Otherwise, report to the user that the podcast could not be transcribed and suggest providing a YouTube link. Do not show the Step 2b OCR prompt — frame extraction is meaningless for audio-only content.

Step 2b — Frame extraction fallback (requires user approval)

Stop and ask the user:

No subtitles or usable caption found for this video. I can extract frames and read the on-screen text to build a transcript. This requires opencv-python-headless (~30MB, installed transiently via uv). Proceed?

If the user declines, stop with a message explaining that the video can't be summarized without a transcript source.

If the user approves:

1. Download the video:

bash "<SKILL_DIR>/scripts/yt-dlp.sh" -o "/tmp/media_video.mp4" "<URL>"

(For Instagram, add --cookies-from-browser BROWSER using the same browser detected above.)

2. Extract frames (scene-change detection):

uv run --with opencv-python-headless python3 "<SKILL_DIR>/scripts/extract_frames.py" /tmp/media_video.mp4

This uses histogram comparison to detect scene changes and saves /tmp/media_frame_000.png, /tmp/media_frame_001.png, etc. The default threshold (0.85) works well for text-overlay videos. Pass a higher value (e.g. 0.92) to capture more frames if results seem sparse.

3. Probe vision capability: Use the Read tool on /tmp/media_frame_000.png. Then attempt to extract any visible text from the image. If you can identify readable text in the frame, vision works — continue with step 4 below. If you cannot read the image or extract meaningful text, fall back to step 5 (local OCR).

4. Vision OCR (preferred): Use the Read tool on each remaining frame. For each frame, extract all visible on-screen text. Collect all extracted text, deduplicate across frames (adjacent frames often repeat), and write the combined text to /tmp/media_clean_transcript.txt. Skip Step 3 — go directly to Step 4.

5. Local OCR fallback: If vision probing failed, inform the user:

Vision not available with this model. Falling back to local OCR via EasyOCR (~400MB first-run download). Proceed?

If approved, run:

uv run --with "easyocr,opencv-python-headless" python3 "<SKILL_DIR>/scripts/ocr_frames.py"

Skip Step 3 — go directly to Step 4.

Step 3 — Parse the subtitle file into clean timestamped lines

Run the subtitle parser script (scripts/parse_subs.py relative to this skill's directory). It discovers the subtitle file (VTT or SRT) written by process_yt_output.sh, deduplicates overlapping caption windows, and preserves timestamps for deep-linking:

uv run "<SKILL_DIR>/scripts/parse_subs.py"

Output format — one line per segment:

[00:00:00] I'm doing something absolutely insane right now.
[00:00:04] Artificial intelligence is a little bit perplexing

Output format — one line per segment:

[00:00:00] I'm doing something absolutely insane right now.
[00:00:04] Artificial intelligence is a little bit perplexing

Step 3.5 — Hard gate: verify transcript and publish to Gist

This step is mandatory for all source types. Do not skip it.

Run the publish_transcript script, which verifies the transcript is present and substantial, then uploads it as a public GitHub Gist. Derive a title from the metadata captured in Step 1 (use media-transcript if no title is available yet):

bash "<SKILL_DIR>/scripts/publish_transcript.sh" "<title>"

The script prints TRANSCRIPT_GIST_URL=<url> on stdout. If it exits non-zero, STOP — do not proceed to Step 4. The exit code tells you why:

  • 1 — transcript missing or too short (<200 chars)
  • 2 — gist upload failed

Step 4 — Read the transcript, then generate the summary

The summary must be generated solely from the transcript content read in this step. Do not rely on prior knowledge of the episode, article, or thread. If a claim cannot be traced to a line in the transcript, omit it.

4a — Check size and read in batches

First check how many lines the transcript has:

wc -l /tmp/media_clean_transcript.txt

Then use the Read tool (not Bash) to read the file in batches of 400 lines using offset and limit. For a 1000-line file, make three Read calls: offset=1/limit=400, offset=401/limit=400, offset=801/limit=400. Read all batches before writing anything.

4b — Classify content type

If CONTENT_TYPE was already set in Step 1 (e.g. x-thread, html-article), skip classification and use that value.

Otherwise, classify the transcript into one of these types:

TypeSignals
recipeCooking instructions, ingredient lists/amounts, food preparation steps, kitchen techniques, dish names, "add the…", "cook until…", "season with…"
generalEverything else — interviews, talks, lectures, panels, commentary, tutorials, reviews

Set CONTENT_TYPE to recipe or general. This determines which template and summary structure to use in the following steps.

4c — Write the summary

Read the appropriate template (see Step 5 for template selection) and follow its section structure. Fill every section with comprehensive, substantive content drawn from the transcript. Use ## section headers, bullet points, and bold text for scannability. Aim for 800–1200 words of substance.

Timestamps (YouTube sources only): If the transcript came from a VTT file (Step 3) and a YouTube URL is available, include YouTube deep-links for each major topic or section. Convert [HH:MM:SS] to total seconds for the ?t= parameter (e.g. [01:05:30] → 3930 seconds). Format as a linked timestamp at the start of the relevant bullet or subheading:

### [[01:05:30]](https://youtu.be/VIDEO_ID?t=3930) Power Concentration

or inline for bullets:

- **[[00:14:00]](https://youtu.be/VIDEO_ID?t=840) Epistemic collapse** — We are entering...

Use the YouTube URL from Step 1 as the base. Include timestamps for every major topic/section — aim for one timestamp per significant topic shift.

No timestamps available: If the transcript came from caption fallback (Step 2a), frame extraction (Step 2b), or Whisper transcription (Step 2c), the content has no timestamps. Omit timestamp links entirely — just use plain section headers and bullets.

Step 5 — Write the markdown file

Derive a slug from the title using only lowercase letters, numbers, and hyphens — strip all other characters (spaces become hyphens, consecutive hyphens collapse to one, leading/trailing hyphens removed). This sanitization is critical: shell metacharacters in the slug (;, $(), backticks, quotes) would be injected into file paths and gh commands below. Example: jenny-wen-design-process. Save the summary to:

~/Downloads/<slug>_summary.md

Choose the template based on CONTENT_TYPE:

Content typeTemplate
generalreferences/media-summary-template.md.j2
recipereferences/recipe-video-template.md.j2
x-threadreferences/x-thread-template.md.j2
html-articlereferences/html-article-template.md.j2

Both paths are relative to this skill's directory. Key points:

  • The metadata fields (Guest, Hosts, Podcast, Published) must be a bullet list, not bare lines — bare consecutive lines collapse into a single paragraph in CommonMark.
  • No horizontal rules (`---`) between sections. Use only one, directly before the italicised source attribution at the bottom.
  • Key Takeaways is the first section, before Guest Background.
  • transcript_url must be set to TRANSCRIPT_GIST_URL captured in Step 3.5. Never leave it blank or as a placeholder.
  • gist_url starts as (to be filled after publishing) and is updated in Step 6.
  • generated_by.model must be set to the runtime model identifier (e.g. claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5). Use the exact model ID from the runtime environment, not a friendly name.
  • The source link at the bottom prefers YouTube or PocketCasts over Apple Podcasts. If you already have a YouTube URL from Step 1, use that. Otherwise check for a PocketCasts link (pca.st or pocketcasts.com). Fall back to the original URL only if neither is available.

X-thread-specific notes (when using x-thread-template.md.j2):

  • Slug derivation: use <handle>-<first-few-words> from the metadata printed by fetch_x_thread.py (e.g. schlickw-us-foreign-policy-anthropic-mythos). Same sanitization rules — lowercase, hyphens only.
  • thread_url and source_url are the root post's URL from the metadata.
  • Summary must be 2–4 sentences, strictly descriptive. State what the thread is about and the shape of its argument — nothing more. Do not infer author background, credentials, or biographical detail from outside the thread. Do not categorize or editorialize the content (e.g. "the list is loosely organized by…"). If you find yourself writing more than 4 sentences, the rest belongs in Context & Annotations.
  • Full Thread: render every post verbatim as a numbered list. Format: N. [[N/total]](post_url) <verbatim text> — the bracketed counter is the hyperlink back to that specific post on X. Preserve the author's wording, line breaks, and hashtags. Strip the leading auto-mention chain (consecutive @handles at the start that X auto-prepends in reply threads), since those are artifacts of the threading mechanism, not the author's words. Hyperlink every `@mention` inline as [@handle](https://x.com/handle) — both in post text and in any external link preview lines. Hyperlink hashtags as [#tag](https://x.com/hashtag/tag).
  • Cite referenced posts inline. When a thread post links to another X/Twitter status, fetch_x_thread.py resolves that tweet and includes it in the cited_posts metadata field (keyed by URL). For each cited post that appears under a thread post, render it as an indented blockquote directly below that thread post, using the format: > **[@handle](https://x.com/handle)** ([date-link](tweet_url)): <verbatim cited text>. Do not just leave the bare URL — the reader should see what's being cited without leaving the summary. If a cited post is missing from cited_posts (deletion, private account, API failure), leave only the bare URL and add a brief > _[cited post unavailable]_ note.
  • Include substantive self-replies in the citation. The self_replies field contains the cited author's follow-up posts to the target tweet (the script auto-walks the self-reply chain). If a self-reply is just a bare URL (it'll already be in external_links), skip it in the blockquote — it's redundant. If a self-reply adds substantive content (continues the thought, extends the argument, adds clarification), append its text to the blockquote as continuation (>\n> <self-reply text>), so the reader sees the full mini-thread the author is citing. Cap at the first 3 substantive self-replies per citation to keep the blockquote readable; link out (> _+N more posts in this thread — see [link](first_self_reply_url)_) if there are more.
  • Resolve external links inside cited posts. Cited tweets often link to longer-form content (X Article, Substack, blog post, paper) — sometimes the tweet body is just a teaser. The cited_posts metadata per citation exposes: article (an X-native long-form post, when present — has title, preview_text, body_excerpt, body_truncated), external_links (URLs from the cited tweet and its author's self-replies — the script fetches the thread chain to catch the common "teaser post + bare-URL self-reply" pattern), self_replies (the full self-reply chain under the target), photos, twitter_card, and author_website. Resolve in this priority (progressive disclosure — stop at the first tier that yields content):

1. `article` is present — the cited tweet is an X long-form Article; the body is already in article.body_excerpt (first ~6000 chars). Render the title as a link to the cited tweet URL and produce a 1–2 sentence synopsis from preview_text + body_excerpt. No WebFetch needed. If body_truncated is true, mention "(article continues on x.com)" so the reader knows there's more. 2. `external_links` non-empty — WebFetch the first substantive longform URL and add a 1–2 sentence synopsis as a sub-blockquote (> _Linked: [title](url)_ — <synopsis>). Self-reply URLs are already included here, so teaser-then-link pairs work out of the box. 3. `twitter_card == "summary_large_image"` and `photos` non-empty — download the first photo (curl -sL <photo_url> -o /tmp/cited_<handle>.jpg) and use the Read tool on it. Authors embed article titles and publication domains directly into preview images when X didn't generate a native link card. If the image reveals a title and domain, construct a likely article URL (e.g. <domain>/p/<slug-of-title>) and WebFetch; add the synopsis as a sub-blockquote. 4. `author_website` + tweet teases an external piece (text mentions "Substack", "blog", "post", "article") — note "Substack/blog index — see [website]" without fetching. 5. Else — skip; the cited tweet is self-contained.

Skip resolution entirely for retweets, social-media-only links, or photos that are clearly not preview cards (selfies, memes, screenshots of other tweets).

  • Context & Annotations (optional but recommended): everything that was inferred, looked up, or editorialized. Include author background pulled from the fxtwitter metadata (name, bio excerpt if useful) — and clearly label it as "from the author's X bio" or similar so the reader knows it's not from the thread. May also include per-post annotations on what's being linked, domain groupings, or observations on the thread's structure. Keep separate from the thread itself.
  • No timestamps — X threads have no internal timeline to deep-link to.

Recipe-specific notes (when using recipe-video-template.md.j2):

  • The metadata fields (Chef, Channel, Cuisine, Published, Servings, Prep/Cook Time) must be a bullet list.
  • If the chef doesn't state exact servings or times, estimate from context and note it with "~" (e.g. "~4 servings").
  • Ingredients should include quantities. If the chef eyeballs amounts, write "to taste" or approximate with "~".
  • Instructions must be numbered steps, not bullets — order matters in a recipe.

html-article-specific notes (when using html-article-template.md.j2):

  • Slug derivation: use <author-or-site>-<first-few-words> from the metadata (e.g. johnpcutler-recently-at-a-conference). Same sanitization rules — lowercase, hyphens only.
  • The metadata fields (Author, Published, Site) must be a bullet list.
  • If author is unavailable from metadata, infer from the URL or content (e.g. LinkedIn vanity URL → author handle). Label inferred metadata as _inferred_.
  • If published_date is unavailable, use the fetch date and note _date not available from source_.
  • Overview should be 2–3 sentences summarizing the article's scope and purpose.
  • Main Arguments & Points is the core of the summary — structure as a numbered or bulleted list of the article's key arguments, claims, or observations, with supporting detail.
  • Notable Details & Examples captures specific examples, data points, anecdotes, or quotes that illustrate the arguments.
  • Context & Significance provides broader context: how this fits into the author's body of work, the field, or current discourse. May include author background if available from the page metadata — label it as "from the author's bio" or similar.
  • No timestamps — HTML articles have no internal timeline.

Step 6 — Publish as a public GitHub Gist

Run the publish_summary script, which creates the gist, backfills the self-referencing gist_url, re-edits the gist, opens the file, and posts a macOS notification:

bash "<SKILL_DIR>/scripts/publish_summary.sh" "<slug>" "<title>"

Both arguments must be double-quoted to prevent word-splitting and globbing. The script prints the Gist URL to stdout on success. If it exits non-zero, report the error to the user.

Final output to user

Tell the user:

  • The local file path
  • The public Gist URL
  • A one-paragraph teaser of what the content is about

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.