Skip to content

Latest commit

 

History

53 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mediumlm

Research a topic on Medium using your own logged-in Medium session — without needing an interactive browser at request time. Extracts your Medium session cookies from Chrome once, then uses a headless browser to search Medium and fetch full article text (including member-only content your account can access).

Built as the engine behind a Claude Code skill (~/.claude/skills/mediumlm/SKILL.md, installed separately, outside this repo), but the mediumlm CLI works standalone from any terminal.

Install

uv tool install .          # installs the `mediumlm` CLI into ~/.local/bin
python3 -m playwright install chromium

After changing the source, refresh with uv tool install --reinstall .. For a dev/editable install for hacking on the code, use pip3 install -e ".[dev]".

Requires Python 3.9+ and Chrome, logged into Medium, for the first cookie extraction.

Usage

# Extract your Medium session cookies from Chrome (run once, and
# again whenever the session goes stale)
mediumlm cookies extract

# Confirm the stored session still authenticates
mediumlm cookies check

# Search Medium for a topic (Medium currently blocks this for
# headless clients; the command fails with a clear error pointing
# at the fallback — see "How it works" below)
mediumlm search "claude code mcp" --limit 8

# Fetch a specific article's full text using your session
mediumlm fetch "https://medium.com/@author/article-slug-abc123abc123"

# Fetch several articles in one call: the browser launch is shared
# (each article still gets a fresh context — Medium challenges reused
# ones), results come back as a JSON array in input order, and a
# per-URL failure is recorded as {"access": "error", "error": "..."}
# instead of aborting the batch
mediumlm fetch "https://medium.com/@a/first-abc123abc123" \
               "https://medium.com/@a/second-def456def456"

# Repeat fetches are served from ~/.mediumlm/cache (marked
# "cached": true in the JSON); only full-access articles are cached,
# so previews and errors always retry. --no-cache forces a refetch.
mediumlm cache list                 # what's cached: [{url, title, fetched_at}]
mediumlm cache clear --url <url>    # drop one entry (omit --url for all)

# If the stored session has expired, fetch automatically re-extracts
# cookies from Chrome once and retries just the expired URLs
# (disable with --no-refresh; a note is printed to stderr when this
# happens — stdout stays pure JSON)

Every command prints a single JSON object/array to stdout on success. On failure, stdout is empty, an error: <message> line goes to stderr, and the exit code is 1.

fetch's output includes an explicit access: "full" | "preview" field with an access_reason (blocked, cookies_expired, or not_member) whenever the article wasn't fully readable — this is never silently collapsed into a plain success. Batch mode prints a JSON array (single-URL mode a single object), and the exit code is 1 only when every URL in a batch failed — single-URL failures keep the error:-on-stderr, empty-stdout contract.

How it works

  • Cookies are extracted from Chrome's local cookie store (browser_cookie3) and stored at ~/.mediumlm/cookies.json with 0600 permissions — this file is a bearer-token-equivalent secret for your Medium session; never commit it or share it.
  • Fetching an article uses a headless Playwright browser with your cookies injected. A plain HTTP client doesn't work here — Medium's Cloudflare bot detection blocks it; a real (headless) browser context does not trip the same defenses.
  • Searching is currently unavailable. A 2026-07-15 finding showed unauthenticated search working while authenticated search was blocked; that was superseded on 2026-07-19 — Medium's search-results GraphQL API now returns 403 to headless-browser XHRs regardless of whether cookies are sent, and the page renders an error state instead of results. mediumlm search detects this and raises a clear "search unavailable" error (exit 1) rather than returning junk; it still returns a genuine empty list for real zero-result queries if search ever works again. The practical discovery path in the meantime is an external web search restricted to site:medium.com, feeding the found URLs to mediumlm fetch (which still works fully with your session). The search code path stays in place so search resumes working automatically if Medium unblocks it.

See docs/superpowers/specs/2026-07-14-mediumlm-design.md for the full design rationale, including the live-verification findings above.

Scope

Built for personal research against your own Medium account, at normal single-topic, on-demand volume. Automated access likely falls outside Medium's Terms of Service; this is not a bulk-scraping tool.

Security

  • The cookie file is the only secret. ~/.mediumlm/cookies.json is written with 0600 permissions, and the tool refuses to write it anywhere inside a git-tracked directory. .gitignore also blocks any *cookies*.json pattern as a second line of defense. Cookie values never appear on stdout/stderr or in error messages — only counts and status make it into output. Automatic refresh (see fetch above) reuses this same guarded write path (0600, git-tracked-path refusal) and runs at most once per invocation, only when the session itself has expired — never in response to bot-challenge pages.
  • Dependencies are version-bounded in pyproject.toml. browser_cookie3 gets the tightest pin of the group, since its entire job is decrypting your local browser's cookie store (Keychain access on macOS) — that's not something to let bump automatically; upgrades there should be deliberate.
  • Fetched article content is untrusted input. It comes from a third party and is parsed with BeautifulSoup, which does not execute JavaScript at parse time (the page's own JS only runs inside Chromium's sandbox during fetch). Anything downstream that consumes the resulting markdown — an AI assistant, your notes — should treat it as data, never as instructions; the companion Claude skill enforces that boundary explicitly.
  • The article cache is private, member-only content. ~/.mediumlm/cache/, created 0700, stores the full text of member-only articles you've fetched. cache clear wipes it and refuses to delete anything outside its own directory.
  • Accepted, by design: any process running as your user can read the cookie file, same as any other local secret under the standard single-user trust model. cookies extract needs access to Chrome's Safe Storage key to work at all. And automated access sits in a gray area of Medium's ToS — see "Scope" above.

Development

python3 -m pytest tests/ -v

The suite is fixture/mock-driven except one test that drives a real local HTTP server to verify cookie injection actually reaches the browser layer.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages