Extract links (Codex variant)
Annotate every URL in a notes file with its content. By default (light) writes a one-line gist inline next to each URL — fast triage, nothing written to disk. With --full, pulls full content (YouTube subtitles, Telegram post text, HTML articles) into a shared sibling extracted/<note-basename>/ directory, replacing each URL with a local pointer (multiple notes in one directory consolidate under a single extracted/ parent).
This is the Codex CLI variant. Behaviourally identical to the Claude Code variant — only the user-interaction idiom differs (numbered list TUI prompt instead of AskUserQuestion radio UI). All shared resources (roles/, scripts/) come from the Claude variant tree via install-time symlinks.
Letter = spirit. If a rule blocks you from reaching the goal it was written for, the rule is wrong, not the goal. Don't look for a wording loophole — ask what the rule is protecting, and protect that.
Usage
/extract-links <note.md> [--force] [--full | --light]
--force re-processes URLs even if they're already annotated (default: skip already-annotated).
Light is the default: a one-line summary of each link's gist written inline next to the URL, no extracted/ tree — fast triage for URL-heavy notes. Pass --full to extract full content into extracted/ with local pointers (use it when /cleanup / /blueprint need the actual text offline). --light is accepted explicitly too.
Weaknesses and when NOT to use
- Does not work with private/auth resources. Private Telegram channels, paywalled articles, logged-in-only pages — out of scope. Those URLs return an error in the final report.
- Depends on external tools (yt-dlp, pandoc). If they're not installed, the skill prompts the user (numbered-list TUI prompt) to install. Never auto-installs without explicit OK. If the user declines — the matching URLs get an error. (Telegram embed-scrape needs only
curl, which is almost always present.) - Overkill for 1-2 URLs. Copy-paste is faster than the pipeline. Use it only with 3+ URLs.
- Long YouTube videos (>2h, ~30k words). Extract will succeed, but downstream work (cleanup) may choke on the volume. Pre-trim manually if needed.
- JS-heavy SPA sites.
extract-html.shuses curl — JS is not executed. You'll get the page skeleton without content. Use it for blog posts, articles, docs, NOT for interactive web apps. - Heuristic reference-detection is imperfect. Notes often contain URLs that aren't content to extract — API doc landing pages, GitHub repo roots, citation-style references, tool homepages. The skill flags them via heuristics in step 1 and surfaces a triage prompt in step 2 (default = skip). What looks like a "tool homepage" might be content the user wants (e.g. a project's blog) — the call is always handed to the user, never silently dropped.
- Light mode is best-effort metadata, not content.
--lightwrites a one-line gist (from title / description / Telegram post-preview) next to each URL — fast, noextracted/tree. It does NOT bring content offline; for JS-SPA / paywalled / private URLs the metadata is thin or missing (the summary must say so, never fabricate). Use--full(default) when downstream/cleanup//blueprintneed the actual content. - Non-interactive mode (
codex exec). This skill needs user input for the dependency-install prompt (step 3) and reference triage (step 2). If invoked fromcodex execwithout TTY, fail with an explicit error rather than silently auto-installing or auto-skipping. User must run fromcodexTUI.
How to do it wrong vs right
Dependency management
❌ Wrong: yt-dlp missing → automatically pip install --user yt-dlp without asking.
- User might not want Python user-site packages.
- User might be on a shared system where pip is restricted.
- The skill turned itself into an installer.
✅ Right: Detect missing → print numbered options in TUI and wait for reply:
yt-dlp not found. How would you like to proceed?
1. Install yt-dlp now (recommended)
2. Skip YouTube URLs (they will be reported as errors)
3. Abort
> _
Accept 1, 2, 3, or text alias. Re-prompt on ambiguous reply. User-explicit only.
URL annotation
❌ Wrong: Replace the URL in the note entirely: [YouTube video](./extracted/youtube-abc/subtitles.en.txt) — original URL is lost.
- Auditability: no way to tell where the content came from.
- Re-extract is impossible without the original.
✅ Right: URL preserved, local pointer appended next to it:
See https://youtube.com/watch?v=abc → [./extracted/note/youtube-abc/subtitles.en.txt](./extracted/note/youtube-abc/subtitles.en.txt)
URL noise (citations, API refs, repo roots)
❌ Wrong: Note has 12 URLs — extract all 12, including 4 that are bare GitHub repo roots, an API docs landing page, and citation [1] [2] [3] references at the end.
- 4 useless extracts pollute
extracted/, each with their own slug subfolder. - Downstream
/cleanuphas to navigate around noise files that contain only navigation HTML. - The user's actual content (the other 8) gets diluted; signal-to-noise drops.
✅ Right: Classify URLs in step 1. Detected reference URLs (bare hosts, docs.*, GitHub repo roots, package-registry landings, anchor-only fragments) are surfaced together in a single triage prompt (step 2): "These N URLs look like references, not content — skip all? Extract all anyway? Pick which?" Default = skip all. Skipped URLs appear in the final report so the user can audit the decision.
Multi-URL note
❌ Wrong: Note has 10 URLs — processed 7, 3 failed — report "extract done" without mentioning the errors.
- The user doesn't know about the 3 broken URLs.
- Downstream works from a holey map.
✅ Right: Final report enumerates: 7 extracted, 3 errors (with reason per URL). The note only annotates successful ones. Errored URLs are left un-annotated — the user decides whether to retry or accept the gap.
Roles
roles/interactive-prompt.md — prompt template for other-type URLs (not YouTube, not Telegram). Substitution: {url_short} (first 60 chars of the URL). Originally written for AskUserQuestion semantics; in Codex you render it as a numbered list in the TUI and accept a numeric or text reply.
Scripts in scripts/ are building blocks; the skill calls them via Bash (no Codex-specific changes — same shell scripts as Claude variant):
| Script | Purpose | Args |
|---|---|---|
install-deps.sh |
Probe which tools are installed | (none) |
extract-youtube.sh |
yt-dlp wrapper, subtitle cleanup | <url> <output-dir> |
extract-telegram.sh |
public Telegram embed-page scrape | <url> <output-dir> |
extract-html.sh |
pandoc / curl fallback | <url> <output-dir> |
summarize-url.sh |
light-mode metadata fetch, no download (prints TITLE/DESC/TEXT… or ERROR:) |
<url> |
Mode routing
Default is Light (the ## Light mode section below) — fast inline gists, nothing written to disk. Pass --full for the full extraction pipeline (steps 1-7 here). Both modes start from the same step-1 URL detection.
Full mode (--full) — step by step
Read the note, find URLs. Regex
https?://[^\s)]+(with trailing-punctuation strip). Classify each URL:youtube—youtube.com/watch?v=*oryoutu.be/*(specific video).telegram—t.me/<channel>/<post-id>(specific public post).reference— likely not content, by any of:- bare host (no path, or only
/), docs.*subdomain, or path starts with/docs/,/reference/,/api/,/library/,- GitHub repo root (
github.com/<owner>/<repo>with no further segments —/blob/,/issues/N,/pull/N,/releases/...are content, not reference), - package registry landings (
npmjs.com/package/*,pypi.org/project/*,crates.io/crates/*,rubygems.org/gems/*), - anchor-only or fragment-only URLs (
#section-id).
- bare host (no path, or only
other— everything else (article, blog post, generic HTML).
Triage references with the user (if any detected). Show a numbered list in the TUI:
N URLs look like references, not content: a) https://github.com/owner/repo (GitHub repo root) b) https://docs.python.org/ (docs landing) ... How to handle? 1. Skip all references (recommended — go into final report as skipped(reference)) 2. Extract all anyway (re-classify each to `other` and run interactive prompt) 3. Pick which to extract (1-at-a-time prompts per URL) > _Default =
1if user just hits enter. Accept1/2/3or text alias. For option3, iterate per-URL prompts.If there are no
referenceURLs — skip this step silently.Probe dependencies.
bash scripts/install-deps.sh. If a tool required for the URL types is missing — TUI numbered prompt (install / skip / abort). NEVER auto-install without explicit OK.For each URL — extract. Output root is
<note-dir>/extracted/<note-basename>/— one sharedextracted/per directory, per-note subfolder inside. For multi-note runs in the same directory, the parent is consolidated automatically.- YouTube →
bash scripts/extract-youtube.sh <url> <note-dir>/extracted/<note-basename>/<slug>/ - Telegram →
bash scripts/extract-telegram.sh <url> <note-dir>/extracted/<note-basename>/<slug>/ - Other → render
roles/interactive-prompt.mdas numbered TUI prompt; accept reply. By choice: readable HTML / skip / custom command. - Slug:
<type>-<short-id>(e.g.youtube-dQw4w9WgXcQ,telegram-channel-123,html-blog-example-com). Max 50 chars. - Errors (404, private, fetch fail) — log to
<note-dir>/extracted/<note-basename>/.errors.log, do not annotate the URL, keep going.
- YouTube →
Annotate the note. For each SUCCESSFULLY extracted URL — append
→ [<local-path>](<local-path>)right after the URL in the note.<local-path>is relative to the note:./extracted/<note-basename>/<slug>/.... Original URL preserved. Use the Edit tool, not Write. If a URL is already annotated (→ [./...]directly after) — skip unless--force. Triage-skipped URLs are NOT annotated.Update .gitignore. Add
extracted/to.gitignoreat git root (if git is initialized). Idempotent — only append if the line isn't already there:grep -qxF 'extracted/' .gitignore 2>/dev/null || echo 'extracted/' >> .gitignoreFinal report + commit. One line per URL with state
extracted/error/skipped(reference)/skipped(user)plus aggregate metrics. Auto-commitextract-links: <N> URLs from <note>(only processed files, not the whole branch).
Light mode (default)
The default (or explicit --light). Same URL detection as Full step 1, then for every URL (reference-triage is relaxed — a one-liner on a repo root or docs page is cheap and useful, so summarise all, don't skip references):
- Probe deps (light). Only
curl(almost always present) is required;yt-dlpis needed only if YouTube URLs exist (pandocis NOT — light mode never converts full HTML). Same TUI install gate as Full step 3, but skip it when there are no YouTube URLs. - Fetch metadata.
bash scripts/summarize-url.sh <url>— prints labelled metadata (TITLE/UPLOADER/DURATION/TEXT/DESC) or anERROR: <reason>line. No full-content download, nothing written to disk. - Condense to one line. From that metadata, write a single plain-language sentence — what the link is and why it's likely here — aim for ≤ ~140 chars. If the script returned
ERROR:(private / JS-SPA / fetch failed) or only thin metadata, say so honestly (e.g._(Telegram post — preview unavailable)_). Never invent content you didn't fetch. - Annotate inline. Append
→ _<summary>_directly after the URL (italic, no link). Original URL preserved. Idempotent: skip URLs already annotated with→ _…_unless--force. - Report + commit. One line per URL:
summarised/error (reason)+ aggregate. Commitextract-links: summarise <N> URLs from <note>. Noextracted/tree, no.gitignorechange.
Light mode is for orientation/triage of URL-heavy notes (e.g. a backlog). It does NOT bring content offline — switch to Full mode when /cleanup / /blueprint need the actual text.
Light mode — wrong vs right
❌ Wrong: summarize-url.sh returns ERROR: telegram post not accessible; you write a plausible-sounding summary from the channel name anyway.
- That's fabricating content you never fetched — the whole point of a summary is that it's grounded.
✅ Right: Write → _(Telegram post — preview unavailable: private or deleted)_ and count it as error in the report. The user decides whether to open it manually.
Outputs
Per processed note (anchored at <note-dir>/extracted/<note-basename>/):
<slug>/— extracted content (per URL):- YouTube:
subtitles.en.txt,subtitles.ru.txt(if available),metadata.json - Telegram:
post.md,media/urls.txt+ optional media files - HTML:
content.md,metadata.json
- YouTube:
.errors.log— per-URL errors (if any)<note>— modified: a local pointer link is appended next to each URL
Multi-note layout in one directory (single shared parent):
<dir>/note-1.md
<dir>/note-2.md
<dir>/extracted/
note-1/
youtube-abc/...
telegram-xyz/...
note-2/
html-blog-example/...
Git:
.gitignore—extracted/added- Commit:
extract-links: <N> URLs from <note>(encompasses note edit + .gitignore; the contents ofextracted/are gitignored)
Light mode (--light): no extracted/ tree and no .gitignore change — only the note is modified, with → _<summary>_ appended next to each URL. Commit: extract-links: summarise <N> URLs from <note>.
Connections to other skills
- Input: any markdown file with URLs. Usually a note or plan before
/cleanupor manual review. - Output: annotated note with local pointers. Suitable for
/cleanup, manual analysis, plain reading. - Does not call other skills automatically. After step 7:
Extracted N URLs from <note>. Run /cleanup next if needed.— a soft hint for the typical pipeline, not a forced chain.
Rules
Commonality
The note is a shared artifact. After extract, it'll be read by the user, downstream skills, and future sessions. If you skipped URLs or lost error info, everything downstream works from a holey map. Not "processed most of them" — that's "missed some", and it has to be reported explicitly.
Prior commitment
In step 1 you committed to classifying every URL (not just youtube/telegram/other — also reference). In step 2 you committed to surfacing every reference URL to the user before deciding its fate. In step 4 you committed to processing EVERY remaining URL — either extracted, or explicitly errored with reason. In step 5 you committed to preserving the original URL and appending the pointer (not replacing). In step 6 — idempotent gitignore_add. Skipping any step withdraws the basis for trusting the final report.
Authority
The skill exists precisely because extracting a dozen URLs by hand is slow and error-prone. If you silently skip "obviously irrelevant" URLs without prompting the user, you are judging content you haven't seen — that's the user's role, not yours. The reference heuristic in step 1 is a PROPOSAL, not a verdict; the triage prompt in step 2 is where the actual skip decision lands.
Self-check before delivering the result
Would this result pass review by a senior engineer? Concretely:
- Were all URLs from the note processed — extracted, errored (with reason), or skipped via explicit user decision (triage)?
- Were
reference-classed URLs surfaced via numbered TUI prompt in step 2 — not silently dropped? - Is the original note annotated correctly — original URL preserved, pointer appended only for extracted ones, triage-skipped URLs left bare?
- Is the
extracted/<note-basename>/layout consistent (slug naming, metadata.json per extract; one sharedextracted/parent per directory)? - Is
.gitignoreupdated idempotently — no duplicate lines? - Does the commit message reflect actual work —
extract-links: N URLs from <note>? - Does the final report enumerate every URL with state (
extracted/error/skipped(reference)/skipped(user))? - No leaked secrets in
extracted/(auth tokens from API responses, personal data)? - (Light mode) Every URL got either a one-line summary or an explicit
error; thin/failed fetches are flagged honestly (not fabricated); noextracted/tree or.gitignorechange was made?
If "no" on any item — redo, don't ship.