agentsclimarketplace

Demo gif

Skill oaklake-studio/oaklake-tools/plugins/demo-gif/skills/demo-gif

Use when the user wants a demo GIF / screen recording of a web UI (for a PR, README, changelog, or showcase) in ANY project. Drives a browser through a short scripted flow you define with the user, records one continuous video (headless by default), and converts it to a calm, followable GIF saved under .demo-gifs/ at the project root (auto-added to .gitignore when the root is a git repo). Browser/UI demos only, not terminal/CLI recordings. Assumes a POSIX shell (macOS/Linux; on Windows use WSL or git-bash).From its SKILL.md

Install
npx -y skills add oaklake-studio/oaklake-tools --skill demo-gif

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

14.8 KB, ~4.0k tokens by cl100k_base, as published. Nobody here has run it

demo-gif

Overview

Records a demo GIF of a running web UI in any project. Pipeline: prepare a reusable Playwright environment → scope the demo with the user → resolve the output folder + gitignore + a run-config file → write a capture script → record the run as one continuous video → verify each beat by screenshot → trim and convert to a palette-optimized GIF under .demo-gifs/.

Two principles carry the quality:

  • Calm and followable. Hold a beat on the full view (~3s), then perform one deliberate interaction at a human pace, then hold again. Don't rapid-fire toggles or cram actions. A slow, clear story reads far better.
  • Artifacts, not repo files. GIFs are large binaries. They live in .demo-gifs/ at the project root, gitignored by default. The user attaches them where needed.

Recording is headless by default (deterministic, works over SSH/CI/no-display). The browser records video fine headless; you don't need a visible window.

When to Use

  • "Make/record a demo gif of <feature>", "capture this for the PR/README", "screen-record the flow"
  • Re-taking a demo (bump the version: -v1-v2)

Do NOT use when:

  • The demo needs a real third-party SSO login on camera (Google/Okta/…). Hands-off automation is blocked by modern browsers, so use hybrid capture: run headed, drive to the provider's screen, then poll only the URL (never screenshot credentials) while the user signs in; delete the throwaway profile after.
  • It's a terminal/CLI demo: use a terminal recorder (asciinema), not this skill.
  • A still screenshot would do, just take one.

⚠️ Shell state does not persist between Bash calls

Each Bash tool call is a fresh shell: variables set in one call are gone in the next, and you MUST stop between capture and conversion to Read the screenshots. So: Step 2 writes a run-config file; every later Bash block starts by sourcing it. Never assume a variable set in an earlier step still exists.

RUNENV="$HOME/.cache/claude/demo-gif/run.env"   # fixed path used by every step

Step 0: Reusable environment (idempotent; once per machine)

POSIX shell required (macOS/Linux; on Windows run this under WSL or git-bash). Keep Playwright in a dedicated persistent venv outside any project, because dependency managers (uv sync, poetry install) prune anything not in their lockfile, which is why an in-project install "disappears."

VENV="$HOME/.cache/claude/demo-gif/venv"; PW="$VENV/bin/python"
mkdir -p "$HOME/.cache/claude/demo-gif"
command -v python3 >/dev/null || { echo "python3 required"; exit 1; }
python3 -c 'import sys; raise SystemExit(0 if sys.version_info>=(3,9) else 1)' || { echo "need python3 >= 3.9"; exit 1; }

# capability-gated (not just "does bin/python exist"): a half-built venv is rebuilt, not reused
if ! "$PW" -c 'import playwright' 2>/dev/null; then
  rm -rf "$VENV"                       # a partial venv must be deleted, never reused
  python3 -m venv "$VENV" || { echo "venv failed (Debian: sudo apt-get install python3-venv)"; exit 1; }
  "$VENV/bin/pip" install -q --upgrade pip playwright || { echo "pip failed: see stderr; behind a proxy set HTTPS_PROXY"; exit 1; }
fi

# ensure a real browser exists: prefer system Chrome, else fetch bundled Chromium once (~150MB)
if ! "$PW" - <<'PY' 2>/dev/null
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
    try: b = p.chromium.launch(headless=True, channel="chrome")
    except Exception: b = p.chromium.launch(headless=True)   # bundled chromium (raises if not installed)
    b.close()
PY
then
  echo "no usable browser: installing bundled Chromium…"
  "$PW" -m playwright install chromium || echo "chromium install failed (network/proxy?). On Linux use: $PW -m playwright install --with-deps chromium"
fi

command -v ffmpeg >/dev/null || echo "WARN: ffmpeg missing, install it (brew install ffmpeg | apt-get install ffmpeg | dnf install ffmpeg | choco install ffmpeg)"
echo "ready: $PW"

If anything above fails, surface the stderr and stop. Don't proceed on a broken environment. channel="chrome" skips the browser download entirely when Chrome is present; on headless Linux you generally want the bundled Chromium with --with-deps (pulls libnss3/libatk/libgbm).

Step 1: Scope the demo

Ask the user only what you can't infer:

  1. What we're demoing: a one-line story + a short slug for the filename (e.g. checkout-flow).
  2. Base URL + whether the app is already running (if not, how to start it, or have them start it).
  3. Login: none / credentials / a described flow. Never guess credentials; for real SSO see "Do NOT use."
  4. Theme + data: light or dark (color_scheme)? And confirm the target has meaningful, stable demo data (not an empty/half-seeded DB or randomized rows) so the GIF shows something real.
  5. The beats: 3–4 max, each one deliberate action.

Discover selectors from the live page with a quick throwaway script using the Step-0 venv (navigate + page.get_by_role(...) / page.content() / a snapshot screenshot) rather than guessing. (If the Playwright MCP is available in your session, its browser_snapshot also works.)

Step 2: Output dir + gitignore + run-config

Resolve the project root (git top-level if in a repo, else cwd), make .demo-gifs/, gitignore it (only when not already effectively ignored), pick the next version, and persist everything to $RUNENV:

RUNENV="$HOME/.cache/claude/demo-gif/run.env"
VENV="$HOME/.cache/claude/demo-gif/venv"
if ROOT=$(git rev-parse --show-toplevel 2>/dev/null); then IS_GIT=1; else ROOT=$(pwd); IS_GIT=0; fi
mkdir -p "$ROOT/.demo-gifs"
if [ "$IS_GIT" = 1 ] && ! git -C "$ROOT" check-ignore -q .demo-gifs/ 2>/dev/null; then
  printf '\n# demo/screencast artifacts (claude demo-gif skill)\n.demo-gifs/\n' >> "$ROOT/.gitignore"
fi

SLUG="<slug>"; SCHEME="light"        # set SLUG; SCHEME=dark if the app is dark-first
N=1; while [ -e "$ROOT/.demo-gifs/$SLUG-v$N.gif" ]; do N=$((N+1)); done
WORK=$(mktemp -d "${TMPDIR:-/tmp}/demo-gif-$SLUG.XXXXXX")

cat > "$RUNENV" <<EOF
PW="$VENV/bin/python"
ROOT="$ROOT"
WORK="$WORK"
SLUG="$SLUG"
N="$N"
OUT="$ROOT/.demo-gifs/$SLUG-v$N.gif"
BASE="<base-url>"
SCHEME="$SCHEME"
EOF
cat "$RUNENV"

(If a project deliberately wants demos committed, the user just removes the .demo-gifs/ line; mention only if relevant.)

Step 3: Write the capture script

Write the template below to $WORK/capture.py (source $RUNENV first to know $WORK). Set/delete the login block and replace # ===== BEATS =====. For each beat: perform one action → wait on the resulting element (get_by_*().wait_for()), never networkidlehold()shot(). Scroll to below-the-fold content with a short page.mouse.wheel loop so the motion reads.

Step 4: Run + verify

source "$HOME/.cache/claude/demo-gif/run.env"
rm -f "$WORK"/video/*.webm 2>/dev/null; mkdir -p "$WORK/video"   # clean slate so re-runs don't reuse a stale take
DEMO_URL="$BASE" DEMO_WORK="$WORK" DEMO_SCHEME="$SCHEME" "$PW" "$WORK/capture.py"

Then Read the beat screenshots in $WORK: confirm framing and that each beat landed on real content (not a loader/empty state/overlay). Fix the script and re-run before converting. To pick trim points, build a duration-spanning labeled montage and Read it:

source "$HOME/.cache/claude/demo-gif/run.env"
WEBM=$(ls -t "$WORK"/video/*.webm | head -1)
DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$WEBM")
STEP=$(awk "BEGIN{s=$DUR/20; print (s<0.5?0.5:s)}")   # 20 tiles always span the whole clip
ffmpeg -y -i "$WEBM" -vf "fps=1/$STEP,scale=320:-1,drawtext=text='%{pts\:hms}':x=6:y=6:fontsize=20:fontcolor=yellow:box=1:[email protected],tile=5x4" -frames:v 1 "$WORK/montage.png" 2>/dev/null \
  || ffmpeg -y -i "$WEBM" -vf "fps=1/$STEP,scale=320:-1,tile=5x4" -frames:v 1 "$WORK/montage.png"   # fallback if drawtext/font unavailable
echo "montage tiles are ${STEP}s apart (tile k ≈ k*${STEP}s), clip is ${DUR}s"

Step 5: Convert to GIF

source "$HOME/.cache/claude/demo-gif/run.env"
WEBM=$(ls -t "$WORK"/video/*.webm | head -1)
DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$WEBM")
A=0; B=$DUR; FPS=14; SCALE=800        # set A/B from the montage (drop login head + dead tail)
awk "BEGIN{exit !($A<$DUR && $B>$A)}" || { echo "bad trim window: A=$A B=$B DUR=$DUR"; exit 1; }
PAL="$WORK/pal.png"
ffmpeg -y -i "$WEBM" -vf "trim=$A:$B,setpts=PTS-STARTPTS,fps=$FPS,scale=$SCALE:-1:lanczos,palettegen=stats_mode=diff:max_colors=200" "$PAL"
[ -s "$PAL" ] || { echo "palette empty: trim window likely off"; exit 1; }
ffmpeg -y -i "$WEBM" -i "$PAL" -lavfi "[0:v]trim=$A:$B,setpts=PTS-STARTPTS,fps=$FPS,scale=$SCALE:-1:lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=4" -loop 0 "$OUT"
ffmpeg -y -sseof -1 -i "$OUT" -frames:v 1 "$WORK/check.png"   # Read this to confirm legibility at scale
ls -lh "$OUT"

Step 6: Report + clean up

State the path (.demo-gifs/<slug>-v<N>.gif), size, dimensions, duration, and the beats; note whether .demo-gifs/ was added to .gitignore. Then rm -rf "$WORK". Offer a re-take (-v<N+1>) for different framing/pacing.

Capture script template

"""<slug> demo: <story>. Records continuous video into DEMO_WORK; ffmpeg trims after."""
import os
from pathlib import Path
from playwright.sync_api import sync_playwright

BASE   = os.environ.get("DEMO_URL", "http://localhost:3000")
WORK   = Path(os.environ["DEMO_WORK"]); (WORK / "video").mkdir(parents=True, exist_ok=True)
SCHEME = os.environ.get("DEMO_SCHEME", "light")
HEADED = os.environ.get("DEMO_HEADED") == "1"   # default headless, works with no display
W, H = 1360, 900

def make_browser(p):
    try:    return p.chromium.launch(headless=not HEADED, channel="chrome")  # system Chrome
    except Exception:
        return p.chromium.launch(headless=not HEADED)                        # bundled chromium

with sync_playwright() as p:
    browser = make_browser(p)
    ctx = browser.new_context(
        viewport={"width": W, "height": H},
        record_video_dir=str(WORK / "video"), record_video_size={"width": W, "height": H},
        reduced_motion="reduce", color_scheme=SCHEME,   # deterministic frames; correct theme
    )
    page = ctx.new_page()
    hold = lambda ms=2500: page.wait_for_timeout(ms)
    def shot(n): page.screenshot(path=str(WORK / n), animations="disabled"); print("shot", n, flush=True)
    def hide_overlays():  # dev indicators / common banners that pollute frames (extend per app)
        page.add_style_tag(content="nextjs-portal,[data-nextjs-toast],#__next-build-watcher{display:none!important}")

    try:
        # ===== BEATS (customize) =====
        page.goto(BASE, wait_until="domcontentloaded")
        page.get_by_role("heading").first.wait_for(timeout=30000)   # wait on a REAL element, never networkidle
        hide_overlays(); hold(3000); shot("01.png")
        # BEAT 2, one deliberate action, wait on its result, then hold:
        #   page.get_by_role("button", name="<label>").click()
        #   page.get_by_text("<expected>").first.wait_for(timeout=15000); hold(); shot("02.png")
        # BEAT 3, below the fold? Smooth-scroll first:
        #   for _ in range(8): page.mouse.wheel(0, 90); page.wait_for_timeout(55)
        #   ...; hold(); shot("03.png")
        # ===== END BEATS =====
    finally:
        ctx.close(); browser.close()   # ALWAYS close: Playwright only finalizes the .webm here

vids = sorted((WORK / "video").glob("*.webm"), key=lambda f: f.stat().st_mtime)
print("VIDEO:", vids[-1] if vids else "NONE (capture failed before any frame)", flush=True)

A thrown beat still leaves a usable partial video (the finally finalizes it). Inspect it to see which beat failed.

Design notes (calm & followable)

  • Open holding on the full view so the viewer reads the starting state.
  • One deliberate interaction per beat; waiting on the resulting element keeps timing honest across machines.
  • A slow tooltip/hover sweep across a chart or list reads better than toggling things on/off (step page.mouse.move(...) across an element's bounding box with small waits).
  • Pick a viewport that fits the important content; scroll to anything below the fold rather than shrinking everything.
  • Continuous animation both breaks deterministic screenshot verification and defeats palettegen's diff mode (bloating the GIF), hence reduced_motion + animations="disabled".

Sizing knobs (in priority order)

Detail-heavy UIs and full-page scrolls inflate GIF size. Turn these down if it's bigger than wanted:

  1. Duration: tighter trim / shorter holds (biggest lever).
  2. fps: 16 → 14 → 12 → 10.
  3. scale: 900 → 800 → 720.
  4. paletteuse=dither=bayer:bayer_scale=4|5; palettegen ...:max_colors=200|128.

Rule of thumb: a ~15s UI walkthrough at fps=14 / scale=800 lands ~2–3 MB. GitHub accepts GIFs ≤10 MB, so don't over-optimize unless asked. gifsicle -O3 --lossy=80 squeezes further if it's installed.

Gotchas

  • Never use networkidle. It never settles on apps with a websocket/SSE/polling (Vite/Next HMR, live dashboards) and times out. Wait on a concrete element instead.
  • Headless by default. Set DEMO_HEADED=1 only when you need a visible window (e.g. SSO hybrid); on headless Linux, headed needs xvfb-run.
  • Never install Playwright into a project .venv. Lockfile managers prune it. Use the ~/.cache/claude/demo-gif/venv from Step 0; if import playwright fails, rm -rf that venv and re-run Step 0.
  • iframes: page.get_by_* won't reach inside them. Use page.frame_locator("iframe#…").get_by_role(...) (Storybook canvas, embedded widgets).
  • Mobile demo: build the context from a device preset (**p.devices["iPhone 13"]) and match record_video_size, instead of the 1360×900 desktop default.
  • Stale dev server: if the UI shows old/wrong data, the running server may predate the code under test; restart it before filming.
  • SSO logins aren't automatable (see "Do NOT use"): hybrid capture, user signs in, delete the throwaway profile after.
  • Keep GIFs out of git: .demo-gifs/ is gitignored by default; don't commit large binaries into history unless the user explicitly asks.
  • Proxy/offline: if pip or the browser download fails, surface stderr and stop; levers are HTTPS_PROXY/HTTP_PROXY, PLAYWRIGHT_DOWNLOAD_HOST, or channel="chrome" to skip the download.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most readme changelog skills give in ~4.0k tokens

Counted across 472 of the 478 authors here whose files we hold, read 2026-08-07

  • Follow the keep a changelog formatin 31 of 472, across 26 files
  • Run from the git repository rootin 20 of 472, across 7 files
  • Save output directly to CHANGELOG.mdin 20 of 472, across 7 files
  • Group commits into logical categoriesin 19 of 472, across 6 files
  • Review the generated changelog before publishingin 19 of 472, across 6 files
  • Omit empty sectionsin 19 of 472
  • Apply changelog guidelines and brand voicein 18 of 472, across 5 files
  • Exclude internal commits like refactoring and testsin 18 of 472, across 5 files
  • Specify date ranges for focused changelogsin 17 of 472, across 4 files
  • Convert technical commits into customer languagein 17 of 472, across 4 files
  • Provide a migration guide for breaking changesin 15 of 472, across 10 files
  • Group commits by conventional commit typein 13 of 472

Said here and by no other author read

  • install playwright in a dedicated persistent venv
  • scope demo beats and base URL with the user
  • persist run configuration to a fixed file
  • source the run configuration in every shell call
  • record the capture script as one continuous video
  • wait for specific elements after each interaction

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 326,452. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.