npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

in-amber

v0.8.1

Published

Save any web page as a clean, self-contained offline folder — deterministic capture, judgement by Claude.

Readme

Amber turns a URL into a folder you own: a single index.html with every image, stylesheet, font, and video pulled local, and the clutter — ads, cookie banners, trackers, newsletter popups, analytics scripts — stripped away. Open it in ten years with the network unplugged and it still just works.

The mechanical half (render, download, rewrite, package) is deterministic TypeScript. The judgement half — what's junk? what's the real content? which embed is a video worth keeping? — is handled by Claude and written out as a plan.json you can read, edit, and replay.

Features

  • 📦 One self-contained folder per pageindex.html + a local assets/ tree, every reference rewritten to a relative path.
  • 🧹 De-junked — ads, cookie/consent banners, share bars, newsletter popups, and tracking scripts removed; the page's own structure (header, nav, footer, bylines) kept.
  • 🔌 Genuinely offline — scripts and connection/preload hints (preconnect, modulepreload, prefetch, …) are stripped, so an opened archive makes no background requests. (Keep-js archives patch the network APIs instead: recorded data replays offline, unrecorded requests fail synthetically.)
  • 🎥 Real media, not dead embeds — a YouTube/Vimeo embed becomes a downloaded local <video> (via yt-dlp); self-hosted clips are localised too.
  • 🧠 Claude does the judgement — and writes it to an auditable plan.json you can edit and re-apply.
  • 🏷️ Auto-tagged — Claude reads the page and adds topical tags to the manifest for later browsing and search.
  • 🌐 Handles JS-rendered pages — headless Chromium (Playwright) capture, used only when a page actually needs it.
  • 🫀 Preserves living pages — when a page's presentation is its JavaScript (WebGL scenes, scroll choreography), Claude escalates to keep-js mode: the page's own code is kept and bundled, the browsing session's data is recorded and replayed, and the archive collapses into one self-contained index.html that plays offline. See Living pages.
  • 🕵️ An escalation path for stubborn pagesamber agent has Claude clean the page interactively, tool call by tool call, when the one-shot plan isn't enough.

Quick start

npm install -g in-amber
export ANTHROPIC_API_KEY=...   # optional — falls back to heuristics without it

amber https://example.com/some-post

(Or without installing: npx in-amber <url>.)

A run narrates every decision it makes — nothing is interactive, everything is overridable next time:

[1/4] Fetching https://example.com/some-post (static probe)
      2841 chars of visible text — static capture is enough
[2/4] Asking Claude for a cleanup plan
[3/4] Removing junk, then downloading assets and rewriting references
      removed 12 elements; 31 assets, 0 errors
[4/4] Downloading embedded media
      1 media item(s)

Archive written to: ~/Documents/Archives/example.com-some-post/
  open ~/Documents/Archives/example.com-some-post/index.html

Archives land in ~/Documents/Archives/<slug>/ by default (override with -o or AMBER_ARCHIVE_DIR). Run amber doctor to see what your setup can do — every optional piece degrades gracefully:

  • ANTHROPIC_API_KEY — without it, cleanup uses heuristics instead of Claude's judgement. With it, you pay per archive on your own key: typically a few cents per page (one planning call; the HTML sent is capped at 400k characters, so even a huge page tops out around $0.30).
  • Playwright — only needed for JS-rendered pages; without it, amber captures what a plain fetch can see. Opt in with npm install -g playwright && playwright install chromium.
  • yt-dlp — needed to download embedded media (brew install yt-dlp or pipx install yt-dlp); ffmpeg is optional, for muxing separate video+audio streams.

Usage

The bare command is the sensible default: it fetches statically, escalates to a headless render only if the page looks client-rendered, and uses Claude when a key is set. The flags below only force a choice.

amber <url>                     # auto capture + Claude if a key is set
amber --no-llm <url>            # heuristics only, never call the model
amber --static <url>            # force a plain HTTP fetch (never boot Chromium)
amber --playwright <url>        # force a headless-Chromium render
amber --plan plan.json <url>    # replay a saved plan
amber --overwrite <url>         # replace the latest snapshot, keep no history
amber -o ~/somewhere <url>      # choose the output directory
amber --keep-js <url>           # force keep-js mode (see Living pages below)
amber --no-keep-js <url>        # never keep JS, even if the plan recommends it
amber --at 2009-06 <url>        # the page as it WAS, via the Wayback Machine (see below)
amber --at latest <url>         # the most recent Wayback capture — for dead sites
amber https://web.archive.org/web/20090615/http://example.com/   # same thing, from a Wayback URL
amber agent <url>               # Claude cleans interactively — for pages the pipeline gets wrong
amber serve <slug-or-path>      # view an archive over localhost (folder-layout keep-js archives)
amber doctor                    # check the environment: key, Playwright, esbuild, yt-dlp, ffmpeg

When an archive comes out wrong

The cleanup step uses Claude's judgement by defult, and sometimes Claude gets things wrong. Amber has a few options for handling this, each trading more time and cost for more care:

  1. Read the judgement. plan.json in the archive folder is the complete plan that was applied: what was removed, what was kept, which embeds were treated as real media.
  2. Edit it and replay. Delete the selector that ate your sidebar — or add one for the popup that survived — then re-run with amber --plan plan.json <url>. Deterministic and free; no model involved.
  3. Escalate to the agent. amber agent <url> swaps the single planning call for Claude working the page interactively: outline the DOM, inspect anything ambiguous, remove junk step by step, download media, finalize. Slower and costlier (many model calls instead of one), but it handles pages the one-shot plan mangles. Needs ANTHROPIC_API_KEY (no heuristic fallback) and Playwright; best on a machine you own, where it has direct network egress and your browser's cookies for yt-dlp. Details in agent/README.md.

Living pages (keep-js)

Most pages read perfectly as static HTML, and stripping their scripts is what makes an archive private and permanent. But for some pages the experience is the JavaScript — a WebGL hero, scroll-driven film choreography, a generative art piece.

For these, amber has keep-js mode. By default Claude decides per page: the plan carries a preserveRuntime judgement, and when it's true the pipeline re-captures in keep-js mode automatically. --keep-js forces it on; --no-keep-js forbids it.

What it does:

  • Keeps the page's own code, drops the surveillance. Analytics/consent/ad scripts are still removed; the app bundle survives.
  • Flattens module scripts into one classic script (esbuild), because browsers refuse to load ES modules from a double-clicked file:// page.
  • Records the browsing session during the render and replays it offline through a small shim that patches fetch/XMLHttpRequest and remaps runtime-constructed asset URLs. Randomness is seeded identically at capture and replay, so pages that randomise at boot make the same choices. Frame sequences (f_001.webp, f_002.webp, …) are completed by fetching the gaps the render didn't hit.
  • Collapses to a single index.html with every asset inlined as a data: URI. This is what makes canvas and WebGL work from a double-clicked file — data: resources are same-origin, so nothing hits the browser's file:// taint rules. Archives whose assets exceed ~200MB keep the folder layout instead and get a double-clickable View archive.command that serves them on localhost (or use amber serve).

The contract: the recorded session works offline; behaviour beyond it is best-effort. Data behind interactions the capture never performed isn't in the archive. Keep-js needs ANTHROPIC_API_KEY (for the judgement — or force it with --keep-js), Playwright, and esbuild (npm i -g esbuild).

Historical versions (Wayback Machine)

Amber can archive a page as it was, from the Internet Archive's Wayback Machine, and file it as if you'd captured it then:

amber --at 2009-06 http://example.com/          # nearest capture to June 2009
amber --at latest http://gone.example.com/      # a dead site's last capture
amber https://web.archive.org/web/20090615/http://example.com/

Any web.archive.org URL works as the target — paste one from your browser, or hit the extension's button while looking at a Wayback page — and --at takes a year, month, day, or a 14-digit Wayback stamp. Wayback snaps to the nearest capture it has (which can be months away for a quiet URL; the run says which capture it actually served).

What makes this clean is Wayback's raw mode: amber fetches the capture's original bytes — no toolbar, no injected scripts, no rewritten URLs — and routes every asset download through the archive at the same timestamp. The result is an ordinary amber archive of the 2009 page: filed under the original URL's slug, cleaned by the same plan, tagged, thumbnailed.

The timeline is the point. A Wayback capture's effective date is the snapshot's date (snapshotAt in the manifest; capturedAt stays the real run time), so archiving today's page and then --at 2009 of the same URL gives one slug whose root is the newest version and whose versions/ holds 2009. The library index has one row per slug, dated by its newest version and counting the rest; a page whose newest capture came from Wayback (a dead site) shows that snapshot's date with a "wayback" mark. Old pages also tend to be Latin-1, Shift_JIS or EUC-JP; amber decodes by the declared charset (or a sensible default) and writes the archive as UTF-8.

Wayback captures are static by default. Add --keep-js (or let Claude's plan decide) and amber instead renders the historical page in Chromium at its original URL with every request the browser makes answered from the archive — so the era's JavaScript runs against the era's assets, and a 2010 carousel spins in the archive the way it did in 2010. Re-running a historical capture with --overwrite replaces that version in place (never the root). archive.org is fetched politely throughout — sequentially, spaced out, with backoff — so a keep-js render of an asset-heavy old page takes a few minutes.

What you get

~/Documents/Archives/example.com-some-post/
├── index.html              # cleaned, faithful to the original — always the newest capture
├── assets/
│   ├── images/             # every image, favicon, srcset entry
│   ├── static/             # css, fonts
│   └── media/              # videos/audio — self-hosted files + yt-dlp downloads
├── thumbnail.jpg           # viewport screenshot, for the library index
├── plan.json               # the cleanup judgement that was applied (auditable, replayable)
├── manifest.json           # source URL, capture time, topical tags, asset list, errors, what was removed
└── versions/               # older snapshots (after you re-archive, or backfill via Wayback), each a full archive
    └── 20260102T090000Z/   # … named by when the snapshot is FROM, with its own index.html + assets + manifest

History over time

Re-archiving a URL keeps the old copy. The newest capture stays at <slug>/, and the previous one rotates into <slug>/versions/<timestamp>/ — each version is a complete, self-contained archive you can open on its own. The folders are the history; there's no index to maintain.

An identical re-capture is detected (by a content hash that ignores timestamps) and skipped, so a page that hasn't changed doesn't pile up duplicate snapshots. Pass --overwrite to replace the latest in place and keep no history. Wayback captures slot into the same timeline by their snapshot date: one older than the current latest is filed into versions/ and the root is left alone.

How it works

  1. Capture — render the page: static fetch, or headless Chromium when it's client-rendered.
  2. Planclaude-sonnet-4-6 reads the raw page and returns a structured plan: main-content selector, junk selectors, embedded media to download, topical tags, and a preserveRuntime judgement (does the presentation need its JS? → keep-js mode). A heuristic fallback runs with --no-llm or if the API call fails.
  3. Clean & localise — remove junk by selector and unconditionally strip anything a static copy must never keep (scripts, preload/prefetch/connection hints) before downloading, so bytes referenced only by junk are never fetched. Then walk the surviving DOM and rewrite every loaded reference — <link>, <img src/srcset>, <source>, <video>/<audio>, inline styles, and url(...) inside <style> blocks and CSS files (recursively, so web-fonts and background images come too) — to a local path, and finally swap embedded media for locally downloaded files.
  4. Package — write index.html, plan.json, and manifest.json.

Development

git clone https://github.com/chasemccoy/amber && cd amber
pnpm install
pnpm exec playwright install chromium   # one-time, for the headless-render backend

pnpm archive <url>   # run the CLI from source (tsx, no build step)
pnpm test            # deterministic unit tests — no key, no network, no browser
pnpm typecheck       # tsc --noEmit
pnpm evals           # judgement suite (see evals/README.md)
pnpm build           # tsup → dist/ (what npm installs; bin/amber.js wraps dist/cli.js)

Limitations

  • Media downloads need a real network. yt-dlp on a datacenter IP hits YouTube's anti-bot wall, and TLS-intercepting proxies break cert verification. Opt into a trusted proxy with --insecure-tls / AMBER_INSECURE_TLS=1.
  • No ffmpeg → no stream muxing. Without it, set AMBER_MEDIA_FORMAT to a progressive single-file format.
  • Interactive <iframe> embeds can't be made offline — CAD viewers, live web apps, and the like stay pointed at the original.
  • The plan is advisory, not infallible — which is exactly why it's written to disk: read it, edit it, re-run with --plan.
  • Some SPA routers refuse file://. An app that matches location.pathname against its routes sees a filesystem path from a double-clicked file, and may hard-navigate away. Those archives work over a localhost origin instead — View archive.command or amber serve. (Worker-based WebGL engines — Draco/Basis decoding, WASM physics — do work: the shim translates URL spaces and replays recorded responses to them.)
  • Keep-js preserves the recorded session, not the live service. Content behind interactions the capture never performed is absent; apps that render from per-user server data will replay the captured session's data. Bundles using top-level await or truly dynamic import(expr) can't be flattened and fall back to a static archive (noted in the manifest).
  • Single-file archives trade open-time for portability — a ~190MB index.html takes a few seconds to parse on open. The launcher fallback needs Node on the machine (any static server works too: python3 -m http.server).

Credits

Inspired by Alex Chan's A personal archive of the web, which describes doing this by hand. Amber automates the mechanical parts and hands the judgement to Claude.