Web article capture (bob ref create <URL> [<URL>…])¶
bob ref create <URL> turns a web article into a readable, provenance-stamped
PDF, reference note, and reading task. By default it files all three together;
if intake is kept or cannot safely run, the PDF remains in xlib/blogs/ for
bob ref scan -i or the host's local intake timer. Two to twenty URLs create
one collection PDF and one reference; see
Several URLs for naming, ordering, layout,
and collection rules. bob ref clip is a permanent hidden alias for create.
bob ref create [OPTIONS] <URL>...
Examples¶
# Capture and file the reference note and reading task in one command
bob ref create https://example.com/posts/some-article/ -P sase
# Keep a capture queued for a later local drain
bob ref create https://example.com/posts/some-article/ -k -P sase
# Preview everything without writing
bob ref create https://example.com/posts/some-article/ -d -P sase
# Override extracted metadata
bob ref create https://example.com/posts/some-article/ \
-T "The Real Title" -A "Jane Doe" -p 2026-04-27 -P sase
# Replay a page saved from a real browser (see below)
bob ref create https://example.com/posts/some-article/ -H saved.html -P sase
# Narrate the article and bind the episode as companion audio
bob ref create https://example.com/posts/some-article/ -L -P sase
# Capture a two-part series as one collection
bob ref create https://example.com/posts/series-part-1 \
https://example.com/posts/series-part-2 -N series -L -P sase
-P/--parent is required on every invocation: the area or project note
that owns the reading task.
-L, --listen narrates the article with highlights.listen_command
(BOB_HIGHLIGHTS_LISTEN_COMMAND overrides; see
highlights-create.md for the contract) and binds
the episode beside the PDF as <stem>.mp3. The command's output streams
unchanged, and nothing is written when it fails. When the URL is already
captured, --listen attaches the episode to the existing PDF and ref note
inline when intake can run — unless the URL is recorded
only by legacy notes without a Highlights PDF, which capture a fresh copy
plus the episode instead.
The pipeline¶
- Each URL is validated (public
http(s)only) and cleaned: the fragment and tracking parameters (utm_*,fbclid,gclid,mc_cid,mc_eid,ref_src) are removed. The cleaned URL is recorded as the markersource_url. - Each URL is deduped against ref-note
source_url(and legacyurl:) values and queued intake PDFs. A PDF-backed ref-note hit refuses even with--force; that flag can overwrite the same intake target when the planned path matches. A URL recorded only by notes without a Highlights PDF warns (already in the library as <path>, a note without a Highlights PDF; capturing a fresh copy) and captures a fresh copy. Import deduplication reads every URL from scalar or list-valuedsource_url/urlmetadata in ref notes and queued intake markers. - For one URL, the pinned adapter fetches the page, extracts the article in reader mode, and renders a Bob-owned print template to PDF. The template loads the same static Source Serif 4 Text, Inter, and JetBrains Mono faces note snapshots embed, so Chromium writes CID fonts rather than Type 3.
- For a collection, each page is captured into private scratch without
printing; the adapter composes the ordered article data into one document
and prints it once. The collection marker records
source_urlas an ordered list. - The PDF is stamped with a page-1 marker (
status,parent,title,id,source_url, plusauthor,published, andcapturedwhen known) and staged underxlib/<ref-type>/<stem>.pdffor intake. - Unless intake is kept or cannot safely run,
createmoves the PDF intolib/, writes theref/note and reading task, and commits those paths together. A queued PDF is drained locally bybob ref scan -ior the host's intake timer.
"Reliable" means "never silently wrong": bot challenges it cannot clear,
login walls, thin extractions, missing browsers, and private-network URLs
all fail closed with a hint: line and exit code 1, writing nothing.
Hosts and browsers¶
The adapter starts headless and retries headed automatically when it meets
a bot challenge. That retry needs a display nobody sees: a private Xvfb
display on Linux (as on athena) or an off-screen window on macOS. Hosts
with no usable browser fail closed with an install hint; Playwright's
bundled Chromium counts when one is present. Discovery chooses the newest
installed revision numerically and launches that exact executable, even
when the pinned Playwright's default revision is missing. Both current
macOS Chrome-for-Testing and legacy Chromium app layouts are supported,
along with Linux's chrome-linux* layouts. PLAYWRIGHT_BROWSERS_PATH
continues to select a custom browser cache.
bob ref doctor reports the web clip uv, web clip adapter, web clip
browser, and web clip headed fallback rows. Its mermaid row also
renders a tiny flowchart in a temporary directory, with a 60-second adapter
timeout. A working browser reports test render ok; a failed render warns
that diagrams print as source. BOB_HIGHLIGHTS_DIAGRAMS=off skips this
probe. Doctor writes nothing to the vault.
The --html escape hatch¶
When a site will not yield to automation, save the page from a real browser (Save Page As, or SingleFile) and replay it:
bob ref create https://example.com/walled/ -H ~/Downloads/walled.html -P sase
cat ~/Downloads/walled.html | bob ref create https://example.com/walled/ -H - -P sase
Subresources still load live on a best-effort basis; the challenge retry is skipped.
Environment variables¶
| Variable | Meaning |
|---|---|
BOB_WEB_CLIP_ADAPTER |
Replaces the adapter invocation (test seam) |
BOB_CHROME |
Browser executable the adapter launches instead of discovery. Discovery itself tries Google Chrome at the usual Linux and macOS paths, then Playwright's bundled Chromium, newest installed revision first, launched by its discovered path even if Playwright's default revision is missing |
BOB_WEB_CLIP_TIMEOUT_SECS |
Overall adapter timeout in seconds (default 300) |
BOB_WEB_CLIP_KEEP_WORKDIR |
1 keeps the scratch directory for debugging and prints its path |
Marker fields and dedupe¶
Every marker carries status (default ready), parent (required:
the resolved -P route), title, id (always the file stem), and
source_url.
It also carries author, published (YYYY-MM-DD), and captured
(YYYY-MM-DD, local date) when known; a field is omitted rather than
written wrong.
The dedupe key lowercases the scheme and host, drops a leading www. and
the default port, strips the trailing slash, and sorts the remaining
query parameters, so the same article reached through two URL spellings
dedupes. Default targets walk <stem>, <stem>_2, …: a name taken by a
different reference is suffixed (with a renamed: line before pdf:),
and only the same reference refuses. --force overwrites the same intake
target only; library PDFs are never overwritten, and an already-captured
ref note is never remade.
A ref-note hit refuses only when the note is PDF-backed (it carries a
source_pdf). A URL recorded only by legacy notes without a Highlights
PDF warns (already in the library as <path>, a note without a
Highlights PDF; capturing a fresh copy) and captures anyway; bob ref
find and bob ref list treat the older notes as superseded once create
(or the queued intake drain) writes the new one. A dry run adds
legacy: <path> (superseded by this capture) to its report.
Failure kinds¶
| Kind | Meaning and fix |
|---|---|
blocked |
Challenge or login wall; rerun headed, or pass --html FILE |
thin |
Extraction too small; retry with --html, or not an article |
browser |
No usable browser. The hint is install Google Chrome or set BOB_CHROME=/path/to/chrome. A system Chromium package needs BOB_CHROME |
network |
DNS, TLS, HTTP, or private-network refusal |
timeout |
Capture took too long; raise BOB_WEB_CLIP_TIMEOUT_SECS |
unsupported_content |
Non-HTML, non-PDF content |
render |
Printing failed |
invalid_request |
Malformed adapter request (a bug; report it) |
Adapter protocol v1 (summary)¶
The Rust side writes one compact JSON capture request to the adapter's
stdin and reads one JSON response from stdout; ping checks versions,
browser, and headed fallback for doctor. The adapter exits 0 whenever it
wrote a response. Non-JSON stdout or a nonzero exit is a protocol error,
reported with the stderr tail. The full request/response shapes live in
src/native/highlights_ref/clip_adapter.rs.
For a collection, Rust sends a capture request per URL in argument order.
render: false stores the extracted article.json and assets in that part's
work directory and returns its absolute path with pdf_bytes: null, without
printing. Requests that omit render keep the single-article behavior and
still send out_pdf; only the optional render field is omitted. The additive
article_json response field appears only when the request sets render: false.
Rust then sends one compose request with the ordered article paths,
collection-level title/author/published/captured metadata, a work directory,
and output PDF path. compose returns the combined PDF byte count, image
report, and warnings. Both operations use protocol version 1.
The composed PDF has an opening page with linked contents, then a fresh page for each article, its own masthead, and one top-level bookmark per part. Page numbering runs across the whole document. Article ids, fragments, and asset names are prefixed per part; relative article links keep their original base URL. Rust stamps and installs the result only after all captures and composition succeed.
Verified (2026-10-01)¶
Live gate for https://openai.com/index/open-source-codex-orchestration-symphony/,
built from this tree (cargo build --release) and copied to athena's
/tmp/bob-clip-verify/bob (never installed over athena's ~/.cargo/bin/bob):
highlights doctoron athena: uv, adapter (playwright 1.62.0, defuddle 0.19.4), browser (chrome 154.0.8037.92), and headed fallback (xvfb) all OK.- Dry run: target
~/bob/xlib/blogs/open_source_codex_orchestration_symphony.pdf, title, author, published 2026-04-27 (visible-date), capture headed (Xvfb) after a headless challenge, fidelity ok. - Scratch-vault capture (
-b /tmp/bob-clip-verify/vault): 38 pages, images 2/2, code blocks 1/1, 8912 words, 483.5 KB. Rendered pages 1-6 and 24 at 80 dpi and viewed: clean masthead, both SVG diagrams are the light variants and clearly visible, the spec code block wraps, footers readn / N(first page without title), no site navigation or junk.mutool draw -F txtfinds no U+00AD, U+2060, U+200B-D, U+FEFF, or ligature code points (U+FB00-FB06). - Scratch scan (
--no-hooks, scratch vault only):ref/blogs/<stem>.mdcarriestype: "[[ref]]",ref_type: blogs,title,id,source_url,author,published,captured, and the^reftask line; a second scan is a no-op (1 unchanged, writes: none). - Real capture on athena: the PDF, ref note, and reading task were committed
and pushed by one create; a re-run refuses the occupied target (exit 1,
--forcehint). A queued capture is drained by athena's localbob-ref-intake.timer. - Static-site check (
https://lilianweng.github.io/posts/2023-06-23-agent/, scratch vault): headless, no headed fallback, 28 pages, images 13/13, fidelity ok; figures and code blocks render correctly. - apollo originally captured the OpenAI URL headed with bundled Chromium
151 and an SSH-forwarded
DISPLAY(38 pages, 2/2 images). - On 2026-10-20, a newer Playwright installation replaced revision 1234
with Chromium revision 1248 (156.0.8078.4). Discovery found 1248, but
the adapter's pinned Playwright 1.62.0 still tried to launch its missing
default headless revision 1234. Two library PDFs consequently printed
Mermaid source instead of figures. Launching the discovered executable
fixes this version skew without reinstalling a particular revision.
Verification on 2026-10-21 used a temporary copy of the adapter on apollo:
its browser-backed self-test passed, and a one-item diagrams request
produced a valid PDF through
chromium-1248/chrome-linux64/chrome, with no browser installation. The headed check was skipped because Xvfb was unavailable; the existing sandbox retry remained necessary. Truly browser-less hosts still fail closed via thebrowser/blockederrors covered by the CLI and adapter fixture tests.