Diese Seite erklärt, was PullMD macht, wie der Cache funktioniert und wie du den Service in Claude Code oder anderen KI-Agenten einrichtest.
This page explains what PullMD does, how the cache works, and how to wire it into Claude Code or any other AI agent.
In v3 ist PullMD von einem reinen Web-Reader zu einem allgemeinen Alles-zu-Markdown-Dienst gewachsen. Alles über die normale Web-Extraktion hinaus ist optional und lässt sich einzeln aktivieren:
In v3, PullMD grew from a web-page reader into a general anything-to-Markdown service. Everything beyond plain web extraction is optional and enabled individually:
# Titel + Inhalt; Quell-URL, Datum und Metadaten liegen im YAML-Frontmatter, statt im Text wiederholt zu werden (spart Tokens)..eml, .msg), gespeicherte Seiten (.mhtml), OpenDocument, RTF, Kalender und Kontakte, CSV/JSON/XML, Notebooks, Quelltext, Text und Untertitel per Upload.?pdf=ocr für saubere Tabellen.# Title + content; the source URL, date and metadata live in the YAML frontmatter instead of being repeated in the text (saves tokens)..eml, .msg), saved pages (.mhtml), OpenDocument, RTF, calendars and contacts, CSV/JSON/XML, notebooks, source code, text and subtitles as uploads.?pdf=ocr for clean tables.X-Transcript-Status unterscheidet bei YouTube ein fehlendes Transkript von einer temporären Sperre.?query= liefert nur die zur Frage passenden Abschnitte statt der ganzen Seite, siehe Query-Extraktion.select.content, um den Artikel-Body direkt zu benennen..md-Datei; den Dateinamen schlägt der Server vor (X-Suggested-Filename), auf Wunsch mit Datums-Präfix.GET /api/status meldet, ob die Extraktions-Sidecars antworten, und liefert 503 sobald einer ausfällt. Für externes Monitoring gedacht: ein toter Renderer nimmt den Dienst nicht runter, er verschlechtert nur still das Ergebnis.PULLMD_CACHE_RETENTION_DAYS legt fest, wie lange Cache-Einträge und Share-Links leben; Standard 90 Tage, 0 = unbegrenzt.PULLMD_TRUST_PROXY legt fest, wann Weiterleitungs-Header zählen; Login und Registrierung sind pro Client-Adresse begrenzt, die kostenpflichtigen Medien-Stufen teilen sich ein Stundenbudget, und abgerufene Seiten haben eine Größengrenze. Siehe Authentifizierung.X-Transcript-Status tells a missing YouTube transcript apart from a temporary block.?query= returns only the sections relevant to your question instead of the whole page, see Query extraction.select.content to name the article body outright..md file, named by the server (X-Suggested-Filename), optionally with a date prefix.GET /api/status reports whether the extraction sidecars respond, and answers 503 as soon as one is down. Meant for external monitoring: a dead renderer does not take the service down, it just quietly degrades the result.PULLMD_CACHE_RETENTION_DAYS sets how long cache rows and share links live; default 90 days, 0 keeps them forever.PULLMD_TRUST_PROXY decides when forwarding headers count; login and signup are limited per client address, the paid media tiers share an hourly budget, and fetched pages have a size limit. See Authentication.PullMD ruft eine beliebige URL ab und liefert sie als sauberes Markdown zurück. Je nach Quelle wird der passende Extraktions-Pfad gewählt:
PullMD fetches any URL and returns it as clean Markdown. It picks the right extraction path depending on the source:
Accept: text/markdown nativ unterstützt, wird das direkt durchgereicht (sauberster Output).?render=force bzw. ?render=skip erzwing- oder abschaltbar.Accept: text/markdown natively, that's passed through directly (cleanest output).?render=force / ?render=skip.Drei Korrekturen laufen darüber: Site-Recipes passen die Extraktion pro Host an (siehe Site-Recipes), und der Coverage Guard greift ein, wenn die Extraktion nur einen Bruchteil einer großen Seite behalten hat, und konvertiert stattdessen den Container mit dem eigentlichen Body. Er kann ein Ergebnis nur vergrößern, meldet sich als source: coverage-guard und lässt sich mit PULLMD_COVERAGE_GUARD=off abschalten. Verteilt eine Seite einen Text auf mehrere kurze <article>-Blöcke desselben Autors, fügt Article Series sie zusammen (source: article-series, abschaltbar mit PULLMD_ARTICLE_SERIES=off).
Three corrections sit on top: site recipes tune extraction per host (see Site recipes), and the coverage guard steps in when an extraction kept only a sliver of a large page, converting the container that actually holds the body instead. It can only grow a result, reports itself as source: coverage-guard, and is switched off with PULLMD_COVERAGE_GUARD=off. When a page splits one piece of writing across several short <article> blocks by the same author, article series joins them (source: article-series, switched off with PULLMD_ARTICLE_SERIES=off).
Welcher Pfad genommen wurde, sieht man im Response-Header X-Source und in der History neben jedem Eintrag.
Which path was used shows up in the response header X-Source and next to each entry in the history.
Was PullMD nicht macht: Bot-Schutz-Challenges lösen. Antwortet eine Seite mit einer Cloudflare- oder Bunny-Shield-Interstitial ("Just a moment…", "Establishing a secure connection…"), kommt 502 mit "code":"bot_challenge" und dem Anbieter im Fehlertext - statt eines nackten 403 oder der Challenge-Seite als Markdown. Der Headless-Browser kommt dort ebenfalls nicht durch. Ausweg: Seite im Browser öffnen, als HTML speichern und hochladen.
What PullMD does not do: solve bot-protection challenges. When a site answers with a Cloudflare or Bunny Shield interstitial ("Just a moment…", "Establishing a secure connection…"), you get 502 with "code":"bot_challenge" and the vendor in the error text - instead of a bare 403 or the challenge page as Markdown. The headless browser does not get past these walls either. Way out: open the page in your browser, save it as HTML and upload it.
Jeder Pull wird in einer SQLite-Datenbank gespeichert. Zwei Zeitspannen sind wichtig:
Every pull is stored in a SQLite database. Two timeouts matter:
| Was | Wert | Wann zurücksetzen? | What | Value | When does it reset? |
|---|---|---|---|---|---|
| Re-Fetch von der Quelle | 1 Stunde |
Bei jedem erfolgreichen Pull derselben URL — egal ob über /api?url=… oder über /s/:id. |
Re-fetch from source | 1 hour |
On every successful pull of the same URL — regardless of whether it came through /api?url=… or /s/:id. |
| Share-Link-Lebensdauer | 90 Tage (Standard) |
Bei jedem Re-Fetch (=> Cache schreibt). Auch /s/:id-Aufrufe verlängern, da sie nach 1 h einen Re-Fetch auslösen. Einstellbar über PULLMD_CACHE_RETENTION_DAYS; 0 = unbegrenzt. |
Share link lifetime | 90 days (default) |
On every re-fetch (= cache write). /s/:id requests extend it too, since they trigger a re-fetch after 1h. Configurable via PULLMD_CACHE_RETENTION_DAYS; 0 = unlimited. |
/s/:id/s/:id behavesPull einmal einen Subreddit-Listing-Link, merke dir die Share-ID — und ruf danach nur noch /s/:id auf. Nach jeder Stunde löst der nächste Aufruf einen frischen Fetch aus, die Share-ID bleibt stabil, der Inhalt aktualisiert sich. Praktisch für KI-Agenten, die einen festen Endpoint mit regelmäßig aktuellem Inhalt brauchen.
Pull a subreddit-listing URL once, remember the share ID — and from then on only call /s/:id. After each hour the next request triggers a fresh fetch, the share ID stays stable, and the content updates. Handy for AI agents that need a fixed endpoint with regularly refreshed content.
Funktioniert in jedem Chat-Agent (ChatGPT, Claude.ai, Gemini, Perplexity, …). Kopiere den Block, füge ihn als System- oder Custom-Instruction ein:
Works in any chat-style agent (ChatGPT, Claude.ai, Gemini, Perplexity, …). Copy the block, paste it as a system or custom instruction:
When you need to read a web page, fetch it via PullMD instead of your
built-in fetch/browse tool - not just when that one fails:
GET https://nasser.tail1e8b49.ts.net/api?url=<URL>
Returns clean Markdown (text/markdown). Optional query params:
comments=false skip Reddit / Hacker News comments
comment_depth=N|all comment nesting depth (default 3)
frontmatter=true prepend YAML metadata block
format=text strip Markdown, return plain text
nocache=true bypass the 1h cache and refetch
render=force|skip override the auto headless-Chromium fallback
extractor=readability|trafilatura|playwright force one extractor
pdf=ocr high-quality PDF conversion (tables)
query=<text> got a specific question? pass it and get back only
the matching sections (70-95% fewer tokens)
max_tokens=N budget for query= (default 600, 64-20000)
lang=de|en language for the comments section header
Response headers worth checking:
X-Source reddit | hackernews | cloudflare | readability |
trafilatura | playwright | recipe-content |
coverage-guard | article-series | markitdown | youtube | pdf-ocr | ...
X-Quality 0.0-1.0 extraction confidence
X-Share-Id 32-hex permalink, openable as /s/<id>
X-Transcript-Status youtube only: ok | none | blocked | error
(blocked/error = transient, not cached — retry later)
Reddit URLs are auto-detected (incl. redd.it short links and /s/ shares).
Hacker News URLs are auto-detected too — items, comment permalinks, and the
front/newest/ask/show/jobs listings.
Use this whenever you would otherwise fetch raw HTML — the markdown is
much cleaner and saves significant context window space. When you only
need specific information rather than the whole document, pass your
question as query= and get back just the relevant sections.
Für Claude Code gibt es eine fertige Skill, die WebFetch automatisch durch PullMD ersetzt (mit Fallback). Lade sie als Zip und entpacke nach ~/.claude/skills/:
For Claude Code there's a ready-made skill that automatically routes WebFetch through PullMD (with fallback). Download the zip and unpack into ~/.claude/skills/:
pullmd.zip herunterladenDownload pullmd.zip
curl -O https://nasser.tail1e8b49.ts.net/pullmd.zip mkdir -p ~/.claude/skills unzip pullmd.zip -d ~/.claude/skills/ # Restart Claude Code; the skill activates on web-reading requests.
Upgrade von vor v3: Die Skill hieß früher web-reader. Das neue Zip ersetzt eine bestehende Installation nicht — erst die alte entfernen (rm -rf ~/.claude/skills/web-reader), sonst sind beide Skills parallel aktiv.
Upgrading from pre-v3: the skill used to be called web-reader. The new zip does not replace an existing install — remove the old one first (rm -rf ~/.claude/skills/web-reader), otherwise both skills stay active side by side.
PullMD läuft als remote MCP-Server unter https://nasser.tail1e8b49.ts.net/mcp (Streamable-HTTP-Transport, stateless). Drei Tools: read_url, get_share, list_recent. Server-seitige Updates erreichen automatisch alle Clients — keine lokale Installation nötig. Ist eine Anmeldung aktiv, listet list_recent nur die eigenen Konversionen.
PullMD runs as a remote MCP server at https://nasser.tail1e8b49.ts.net/mcp (Streamable-HTTP transport, stateless). Three tools: read_url, get_share, list_recent. Server-side updates reach every client automatically — no local install needed. With sign-in enabled, list_recent lists only your own conversions.
Claude Code — Prompt einfügen, Claude installiert es selbst:
Claude Code — paste this prompt and Claude will install it for you:
Installiere den PullMD MCP-Server in Claude Code (User-Scope):
- Name: pullmd
- Transport: http
- URL: https://nasser.tail1e8b49.ts.net/mcp
Nutze: claude mcp add --transport http pullmd https://nasser.tail1e8b49.ts.net/mcp
Danach: claude mcp list zur Verifikation.
Install the PullMD MCP server in Claude Code (user scope):
- Name: pullmd
- Transport: http
- URL: https://nasser.tail1e8b49.ts.net/mcp
Run: claude mcp add --transport http pullmd https://nasser.tail1e8b49.ts.net/mcp
Then: claude mcp list to verify.
Claude Code — direkt im Terminal:
Claude Code — directly in the terminal:
claude mcp add --transport http pullmd https://nasser.tail1e8b49.ts.net/mcp
Claude Desktop / Cursor / andere — JSON-Konfig:
Claude Desktop / Cursor / others — JSON config:
{
"mcpServers": {
"pullmd": {
"type": "http",
"url": "https://nasser.tail1e8b49.ts.net/mcp"
}
}
}
Sobald registriert, erscheinen die drei Tools nativ im Agent — keine Prompt-Anweisungen nötig, das LLM erkennt sie über ihre Schema-Beschreibungen.
Once registered, the three tools surface natively in the agent — no prompt instructions needed, the LLM picks them up via their schema descriptions.
read_url kennt dieselben Stellschrauben wie die REST-API: comments, comment_depth, comment_limit, frontmatter, lang, nocache, extractor, yt_timecodes, yt_chunk, pdf_ocr (statt pdf=ocr), query und max_tokens sowie, nur für Reddit, images ("on" / "off"). MCP-Antworten haben keine Response-Header; findet die Query-Extraktion nichts, steht stattdessen ein Kommentar im Markdown.
read_url exposes the same knobs as the REST API: comments, comment_depth, comment_limit, frontmatter, lang, nocache, extractor, yt_timecodes, yt_chunk, pdf_ocr (instead of pdf=ocr), query and max_tokens, plus images ("on" / "off") for Reddit only. MCP responses carry no headers; when query extraction finds no match it prepends an in-band comment to the markdown instead.
Instanz mit Login? Dann brauchen /api und /mcp Zugangsdaten: einen API-Key als Authorization: Bearer pmd_… oder OAuth für Claude Desktop und claude.ai. Details unter Authentifizierung.
Instance with login? Then /api and /mcp need credentials: an API key as Authorization: Bearer pmd_…, or OAuth for Claude Desktop and claude.ai. Details under Authentication.
Alles, was die Instanz nach außen anbietet. GET /api ist der Hauptweg, der Rest ist Beiwerk für PWA, Betrieb und Integration.
Everything the instance exposes. GET /api is the main path; the rest supports the PWA, operations, and integration.
| Endpoint | Zweck | Purpose |
|---|---|---|
GET /api?url=… |
URL zu Markdown. Siehe API-Parameter. | URL to Markdown. See API parameters. |
GET /api/stream?url=… |
Dieselbe Konvertierung als Server-Sent-Events: status-Events pro Extraktionsstufe, dann ein result- oder error-Event. Die PWA zeigt damit den Fortschritt an. |
The same conversion as Server-Sent Events: a status event per extraction stage, then one result or error event. This is what drives the PWA's progress display. |
POST /api/html |
Gespeicherte HTML-Datei konvertieren (max. 10 MB). | Convert a saved HTML file (max 10 MB). |
POST /api/file |
Dokument-Upload konvertieren (max. 25 MB). | Convert an uploaded document (max 25 MB). |
GET /s/:id |
Share-Link, aktualisiert sich selbst. Siehe Share-Links. | Share link, refreshes itself. See Share links. |
GET /api/history |
Letzte Konversionen als JSON (?limit=, Default 20, 1 bis 100). Angemeldete sehen ihre eigenen. |
Recent conversions as JSON (?limit=, default 20, 1 to 100). Signed-in callers see their own. |
GET /api/archive |
Seitenweise durch das ganze Archiv (?limit= Default 50, 1 bis 200, ?offset=). |
Paginated full archive (?limit= default 50, 1 to 200, ?offset=). |
GET /api/storage |
Cache-Größe und Aufbewahrungsdauer. | Cache size and retention. |
GET /api/stats |
Extraktions-Telemetrie: Quellen, Qualität, Laufzeit (?window=-7 days). Die Listen pro Domain gibt es nur für den Admin, oder für alle, wenn Auth aus und die öffentliche History an ist. |
Extraction telemetry: sources, quality, latency (?window=-7 days). The per-domain lists are included only for the admin, or for everyone when auth is off and public history is on. |
GET /api/config |
Was auf dieser Instanz aktiv ist (Auth-Modus, Dokumente, Vision, STT, PDF-OCR, YouTube). Die PWA blendet danach ihre Bedienelemente ein oder aus. | What is enabled on this instance (auth mode, documents, vision, STT, PDF OCR, YouTube). The PWA shows or hides its controls accordingly. |
GET /api/recipes/status |
Welche Site-Recipes geladen bzw. abgelehnt wurden. | Which site recipes loaded or were rejected. |
GET /api/status |
Erreichbarkeit der Extraktions-Sidecars (trafilatura, Playwright, markitdown). 200 wenn alle konfigurierten antworten, 503 sobald einer ausfällt — gedacht für externes Monitoring. Ebenfalls 503 (degraded, policy-missing): ein Playwright-Sidecar, der die Adress-Policy nicht meldet, also ein pullmd-playwright-Image älter als v3.15.2. Es rendert noch, folgt aber Redirects in interne Netze; auf dasselbe Release wie pullmd aktualisieren. |
Health of the extraction sidecars (trafilatura, Playwright, markitdown). 200 when every configured one answers, 503 as soon as one is down — meant for external monitoring. Also 503 (degraded, policy-missing): a Playwright sidecar that does not report the address policy, i.e. a pullmd-playwright image older than v3.15.2. It still renders but follows redirects into internal networks; update it to the same release as pullmd. |
POST /mcp |
MCP-Server (Streamable-HTTP, stateless), drei Tools. | MCP server (Streamable HTTP, stateless), three tools. |
GET /pullmd.zip |
Claude-Code-Skill mit der URL dieser Instanz. /web-reader.zip leitet hierher weiter. |
Claude Code skill with this instance's URL baked in. /web-reader.zip redirects here. |
Zum Schutz vor Server-Side Request Forgery (SSRF) werden Anfragen an private, loopback-, link-local-, CGNAT- und Cloud-Metadata-Adressen (z. B. 169.254.169.254, 100.100.100.200) standardmäßig abgelehnt - auch wenn eine URL erst per Redirect dorthin führt und auch über IPv6-Übergangsadressen. /api und das MCP-Tool read_url prüfen vorab und antworten mit 403, bevor irgendetwas geholt wird; /api/stream meldet die Ablehnung als error-Event. Selbst-Hoster können mit PULLMD_ALLOWED_HOSTS (kommagetrennte CIDRs und/oder Hostnamen) gezielt interne Ziele freigeben. Der Playwright-Sidecar wendet dieselbe Regel auf alles an, was Chromium selbst lädt (Redirects, Meta-Refresh, Skript-Navigation, iframes, Subressourcen).
To guard against Server-Side Request Forgery (SSRF), requests to private, loopback, link-local, CGNAT and cloud-metadata addresses (e.g. 169.254.169.254, 100.100.100.200) are rejected by default - including when a URL only reaches them via redirect, and including IPv6 transition addressing. /api and the MCP read_url tool check up front and answer 403 before fetching anything; /api/stream surfaces the refusal as an error event. Self-hosters can allow specific internal targets via PULLMD_ALLOWED_HOSTS (comma-separated CIDRs and/or hostnames). The Playwright sidecar applies the same rule to everything Chromium loads on its own (redirects, meta refresh, script navigation, iframes, subresources).
| Param | Default | Beschreibung | Default | Description |
|---|---|---|---|---|
url | — | Pflicht. Beliebige öffentliche URL. | Required. Any public URL. | |
comments | true |
Kommentare einschließen (Reddit und Hacker News). false liefert nur den Post. Bei anderen URLs wirkungslos. |
Include comments (Reddit and Hacker News). false returns just the post. No effect on other URLs. |
|
comment_depth | 3 |
Maximale Verschachtelungstiefe (1–10), für Reddit und Hacker News. all zeigt den ganzen Thread: keine Tiefengrenze, und bei Reddit werden eingeklappte Kommentare („weitere Kommentare“, „Thread fortsetzen“) mit zusätzlichen Abrufen nachgeladen. In der Oberfläche ist das der Schalter „Alle“. |
Max nesting depth (1–10), for Reddit and Hacker News. all returns the whole thread: no depth limit, and on Reddit collapsed comments ("load more", "continue this thread") are loaded with extra requests. In the web UI this is the "All" switch. |
|
comment_limit | — | Optionale Obergrenze für Top-Level-Kommentare. Reddit liefert pro Abruf etwa 200 Kommentare; was fehlt, steht als Hinweis unter den Kommentaren. | Optional cap on top-level comments. Reddit returns about 200 comments per request; anything missing is noted below the comments. | |
frontmatter | false |
YAML-Frontmatter mit Metadaten voranstellen. | Prepend YAML frontmatter with metadata. | |
format | md |
text = Markdown-Formatierung entfernen, Plaintext zurückgeben. json = strukturiert mit Metadaten. |
text = strip Markdown, return plain text. json = structured with metadata. |
|
nocache | false |
1-h-Cache umgehen, immer frisch holen. | Bypass the 1-hour cache, always refetch. | |
render | auto | force = immer im Headless-Browser rendern, skip = nie. Umgeht den Cache. |
force = always render in the headless browser, skip = never. Bypasses the cache. |
|
extractor | auto | Erzwingt readability, trafilatura oder playwright und überspringt die Qualitätsauswahl. Notausgang, wenn die Automatik bei einer Seite danebengreift. Umgeht den Cache; bei Reddit wirkungslos. |
Forces readability, trafilatura or playwright and skips the quality pick. An escape hatch for sites where the automatic choice is wrong. Bypasses the cache; ignored for Reddit. |
|
pdf | — | ocr schickt PDFs durch die hochwertige OCR-Stufe (saubere Tabellen). Braucht einen serverseitigen OCR-Key; ohne ihn bleibt es beim normalen Pfad. Umgeht den Cache. |
ocr routes PDFs through the high-quality OCR tier (clean tables). Needs a server-side OCR key; without one the normal path is used. Bypasses the cache. |
|
yt_timecodes | links |
YouTube-Transkript: links = klickbare Zeitstempel, plain = [MM:SS], none = nur Text. Umgeht den Cache. |
YouTube transcript: links = clickable timestamps, plain = [MM:SS], none = text only. Bypasses the cache. |
|
yt_chunk | 30 |
Blockgröße des Transkripts in Sekunden; 0 behält die Original-Schnipsel. Umgeht den Cache. |
Transcript block size in seconds; 0 keeps the original snippets. Bypasses the cache. |
|
lang | de |
Sprache des Kommentar-Headers (de oder en). |
Language for the comments header (de or en). |
|
images | Instanzinstance | Nur Reddit-Posts: on / off (auch true/1, false/0) beschreibt und transkribiert die Bilder des Posts mit dem Vision-Modell der Instanz. Braucht ein Vision-Modell und PULLMD_EMBEDDED_IMAGES ungleich off, siehe Dateien konvertieren. Andere Werte liefern bei Reddit-URLs 400, bei anderen URLs ist der Parameter wirkungslos. Umgeht den Cache nicht. |
Reddit posts only: on / off (also true/1, false/0) describes and transcribes the post's images with the instance's vision model. Needs a vision model and PULLMD_EMBEDDED_IMAGES other than off, see Converting files. Other values return 400 for Reddit URLs; for other URLs the parameter has no effect. Does not bypass the cache. |
|
query | — | Für eine konkrete Frage an eine lange Seite: Frage hier übergeben, zurück kommen nur die passenden Abschnitte statt der ganzen Seite, siehe Query-Extraktion. | For a specific question about a long page: pass the question here and get back only the matching sections instead of the whole page, see Query extraction. | |
max_tokens | 600 |
Token-Budget für query (64–20000). Ohne query wirkungslos; höher setzen, wenn die Antwort über mehrere Abschnitte verteilt liegt. Wird nur geprüft, wenn query gesetzt ist; ein ungültiger Wert liefert 400. |
Token budget for query (64–20000). No effect without query; raise it when the answer likely spans several sections. Only validated when query is set; an invalid value returns 400. |
Dieselben Parameter gelten für GET /api/stream. Alles, was das erwartete Ergebnis verändert (comment_depth, comment_limit, render, extractor, pdf=ocr, yt_*), umgeht den Cache, damit der neue Wert auch wirklich greift.
The same parameters apply to GET /api/stream. Anything that changes the expected result (comment_depth, comment_limit, render, extractor, pdf=ocr, yt_*) bypasses the cache so the new value actually takes effect.
Jede Antwort von /api trägt mit, wie sie zustande kam. Für Agenten ist das der billigste Weg, die Qualität einer Extraktion zu beurteilen, ohne den Text zu analysieren.
Every /api response carries how it was produced. For agents that is the cheapest way to judge an extraction without analyzing the text.
| Header | Inhalt | Contents |
|---|---|---|
X-Source |
reddit · hackernews · cloudflare · readability · readability-fallback · trafilatura · playwright · recipe-content · coverage-guard · article-series · markitdown · youtube · image-caption · audio-transcript · pdf-ocr · email · text · markdown · subtitles · opendocument · rtf · calendar · contact · data · feed · notebook · code |
reddit · hackernews · cloudflare · readability · readability-fallback · trafilatura · playwright · recipe-content · coverage-guard · article-series · markitdown · youtube · image-caption · audio-transcript · pdf-ocr · email · text · markdown · subtitles · opendocument · rtf · calendar · contact · data · feed · notebook · code |
X-Quality |
0.0–1.0, Konfidenz der Extraktion. Niedrige Werte heißen dünner oder verrauschter Text. |
0.0–1.0 extraction confidence. Low values mean thin or noisy output. |
X-Share-Id |
32-stellige Hex-ID, aufrufbar als /s/<id>. Fehlt bei /api/html und /api/file, weil lokale Dateien nicht gecacht werden. |
32-hex id, openable as /s/<id>. Absent on /api/html and /api/file, since local files are never cached. |
X-Suggested-Filename |
Vorgeschlagener Dateiname für den Download, z. B. YT-vortrag-titel-dQw4w9WgXcQ.md. YouTube bekommt Titel plus Video-ID, Bilder, Audio und Dokumente den ursprünglichen Dateinamen, alles andere den Titel. Self-Hoster können mit PULLMD_FILENAME_DATE_PREFIX ein Datum voranstellen (Vorlage mit den Platzhaltern YYYY MM DD HH mm ss, z. B. YYYY-MM-DD-). Der Download-Button in der Web-Oberfläche speichert das Ergebnis unter genau diesem Namen. Bei /api/stream gibt es keine Header, dort steckt derselbe Wert als suggestedFilename im result-Event. |
Suggested download filename, e.g. YT-talk-title-dQw4w9WgXcQ.md. YouTube gets title plus video id, images, audio and documents keep their original file name, everything else uses the title. Self-hosters can prepend a date with PULLMD_FILENAME_DATE_PREFIX (a template with the tokens YYYY MM DD HH mm ss, e.g. YYYY-MM-DD-). The download button in the web UI saves the result under exactly this name. /api/stream has no headers, so there the same value travels as suggestedFilename in the result event. |
X-Transcript-Status |
Nur YouTube: ok · none · blocked · error. blocked (Rate-Limit, HTTP 429) und error sind vorübergehend und werden nicht gecacht - später erneut versuchen. none heißt, das Video hat wirklich kein Transkript. |
YouTube only: ok · none · blocked · error. blocked (rate limit, HTTP 429) and error are transient and not cached - retry later. none means the video genuinely has no transcript. |
X-Extracted |
true/false. Nur vorhanden, wenn query aktiv ist. |
true/false. Present only when query is active. |
X-Extract-Confidence |
high · medium · low. Fehlt, wenn die Seite ohnehin klein genug war und die Extraktion übersprungen wurde. |
high · medium · low. Absent when the page was small enough that extraction was skipped. |
X-Extract-Sections |
Anzahl der zurückgelieferten Abschnitte. | Number of returned sections. |
X-Extract-Original-TokensX-Extract-Returned-Tokens |
Geschätzte Tokenzahl (Zeichen / 4) der ganzen Seite und der Antwort. Die Differenz ist die Ersparnis. | Estimated token count (chars / 4) of the full page and of the response. The difference is what you saved. |
MCP-Antworten haben keine Header. Dort landen dieselben Angaben im Frontmatter bzw. bei format=json im extract-Objekt.
MCP responses have no headers. There the same data lands in the frontmatter, or in the extract object with format=json.
Wer eine konkrete Frage an eine lange Seite hat, braucht selten die ganze Seite. ?query= nimmt die Frage in natürlicher Sprache entgegen und liefert nur die passenden Abschnitte - auf langen Seiten typischerweise 70-95 % weniger Tokens. Die ganze Seite lohnt sich dann noch, wenn man sie wirklich komplett braucht: zusammenfassen, übersetzen, archivieren.
When you have a specific question about a long page, you rarely need the whole page. ?query= takes that question in natural language and returns only the matching sections - typically 70-95% fewer tokens on long pages. Fetching the full page still makes sense when you genuinely need all of it: summarizing, translating, archiving.
curl -s "https://nasser.tail1e8b49.ts.net/api?url=https://example.com/long-article&query=how+does+caching+work&max_tokens=800"
max_tokens begrenzt die Ausgabe (Default 600, 64–20000, geschätzt als Zeichen / 4).<!-- … -->.confidence: low).query (oder mit leerem Wert) ist die Antwort byte-identisch zu vorher - keine zusätzlichen Header, keine zusätzlichen Felder.max_tokens caps the output (default 600, 64–20000, estimated as chars / 4).<!-- … -->.confidence: low in the first case).query (or with an empty value) the response is byte-identical to before - no extra headers, no extra fields.Mit ?frontmatter=true kommen extracted, extract_confidence, sections_selected, original_tokens und returned_tokens dazu; format=json liefert dieselben Werte im extract-Objekt.
With ?frontmatter=true you also get extracted, extract_confidence, sections_selected, original_tokens and returned_tokens; format=json returns the same values in the extract object.
Nicht alles steht als abrufbare URL im Netz. Zwei Upload-Wege decken den Rest ab. Beide landen aus Datenschutzgründen nicht im Cache - kein History-Eintrag, kein Share-Link, keine X-Share-Id.
Not everything lives at a fetchable URL. Two upload paths cover the rest. For privacy, neither is ever cached - no history entry, no share link, no X-Share-Id.
Bereits gespeicherte Seiten ("Seite speichern unter", SingleFile-Exports) lassen sich direkt konvertieren: die .html-Datei auf die PullMD-Oberfläche ziehen (Desktop) oder den gestrichelten Hinweis unter dem URL-Feld antippen, um eine Datei zu wählen (Desktop + Mobile) - oder per API:
Already-saved pages ("Save Page As", SingleFile exports) can be converted directly: drag-and-drop the .html file onto the PullMD UI (desktop) or tap the dashed hint below the URL field to pick a file (desktop and mobile) - or via the API:
curl -s -X POST --data-binary @page.html \ -H 'Content-Type: text/html' \ -H 'X-Filename: page.html' \ "https://nasser.tail1e8b49.ts.net/api/html"
Optionale Parameter: url=… (Original-URL - aktiviert Site-Recipes und den verlinkten Header), format=json|text, frontmatter=true, extractor=readability|trafilatura. Statt des Headers X-Filename (URI-encodiert) geht auch ?filename=; der Header hält den Dateinamen aus Access-Logs heraus. Maximal 10 MB. Findet die Extraktion so gut wie keinen Inhalt - typisch für eine gespeicherte JavaScript-App-Hülle - kommt 422 mit der Bitte, stattdessen die Original-URL zu schicken.
Optional parameters: url=… (original URL - enables site recipes and the linked header), format=json|text, frontmatter=true, extractor=readability|trafilatura. Instead of the X-Filename header (URI-encoded) you can use ?filename=; the header keeps the file name out of access logs. Max 10 MB. If extraction finds almost no content - typical for a saved JavaScript app shell - you get a 422 asking for the original URL instead.
Diese Formate liest PullMD selbst, ein Sidecar ist nicht nötig. Hochgeladen werden sie wie Dokumente - per Drag-and-drop, Dateiauswahl oder POST /api/file:
PullMD reads these formats itself, no sidecar needed. They are uploaded like documents - drag-and-drop, file picker or POST /api/file:
.eml, z.B. aus Thunderbird, Apple Mail oder Gmails "Original herunterladen", und Outlooks .msg) - Betreff, Von, An, Cc und Datum als Kopf, darunter der Mailtext (der HTML-Teil, sonst der Klartext). Anhänge werden nur mit Namen aufgelistet, nicht umgewandelt. Im Frontmatter stehen Betreff, Absender und Datum (source: email); lang=en schaltet die Kopfzeilen auf Englisch..mhtml / .mht, "Seite speichern unter > Webseite, eine Datei" in Chrome und Edge) - läuft wie eine hochgeladene HTML-Datei, nur dass die Original-URL aus der Datei mitkommt: Sie steht im Frontmatter, und Site-Recipes greifen wie beim normalen Abruf..txt, .md) - werden durchgereicht, mit einer Titelzeile (erste Überschrift, sonst der Dateiname) und Frontmatter. Ein eigenes YAML-Frontmatter einer Notiz bleibt als yaml-Codeblock erhalten. Dateien im alten Windows-Zeichensatz werden erkannt..srt, .vtt) - Nummern, Zeitstempel und Formatierungen fallen weg, wiederholte Zeilen automatischer Untertitel werden zusammengefasst, längere Sprechpausen ergeben einen neuen Absatz..odt, .ods, .odp aus LibreOffice und OpenOffice) - Texte mit Überschriften, fett und kursiv, Links, Listen, Tabellen und Fußnoten; Tabellendokumente mit einer Tabelle je Blatt; Präsentationen mit einem Abschnitt je Folie samt Sprechernotizen. Titel, Autor und Datum kommen aus den Dokumenteigenschaften. Bilder, Diagramme und eingebettete Objekte fallen weg, passwortgeschützte Dateien ergeben 422..rtf) - Absätze, Überschriften, fett und kursiv, Links, Listen und Tabellen. Kopf- und Fußzeilen sowie Fußnoten fallen weg. RTF mit eingebettetem HTML (so speichert Outlook HTML-Mails) läuft über die HTML-Extraktion; das hilft auch bei .msg-Dateien, deren HTML nur im RTF-Teil steckt..ics, .vcf) - Termine mit Beginn, Ende, Ort, Organisator, Teilnehmern, Wiederholung und Beschreibung, Einladungen und Absagen werden als solche markiert; Kontakte mit Organisation, Telefon, E-Mail, Adresse und Geburtstag. Mehrere Einträge in einer Datei ergeben je einen Abschnitt..csv, .tsv, .json, .xml) - CSV und TSV als Tabelle (Trennzeichen wird erkannt, auch das Semikolon aus deutschem Excel), JSON und XML als Codeblock. RSS- und Atom-Feeds werden zu einer Liste der Einträge mit Link, Datum und Text..ipynb) - Textzellen als Text, Codezellen als Codeblock, Ausgaben als Text; Bildausgaben erscheinen nur als Platzhalter..py, .js, .yaml, .sql, .log, Dockerfile und viele mehr) - die Datei als Codeblock mit passender Sprache, darüber der Dateiname. .env-Dateien werden bewusst nicht angenommen, weil sie meist Zugangsdaten enthalten. Textformate wie .rst, .adoc und .org werden wie .txt durchgereicht..eml, e.g. from Thunderbird, Apple Mail or Gmail's "Download message", and Outlook's .msg) - subject, from, to, cc and date as a header block, followed by the message text (the HTML part, otherwise the plain text). Attachments are only listed by name, not converted. The frontmatter carries subject, sender and date (source: email); lang=en switches the header labels to English..mhtml / .mht, "Save page as > Webpage, single file" in Chrome and Edge) - handled like an uploaded HTML file, except that the original URL comes along from the file: it lands in the frontmatter, and site recipes apply as for a live fetch..txt, .md) - passed through with a title line (the first heading, otherwise the file name) and frontmatter. A note's own YAML frontmatter is kept as a yaml code block. Files in the legacy Windows character set are detected..srt, .vtt) - numbers, timestamps and styling are dropped, repeated lines from auto-generated captions are collapsed, and longer pauses start a new paragraph..odt, .ods, .odp from LibreOffice and OpenOffice) - text documents with headings, bold and italic, links, lists, tables and footnotes; spreadsheets with one table per sheet; presentations with one section per slide, including speaker notes. Title, author and date come from the document properties. Images, charts and embedded objects are dropped; password-protected files return 422..rtf) - paragraphs, headings, bold and italic, links, lists and tables. Headers, footers and footnotes are dropped. RTF with embedded HTML (the way Outlook stores HTML mails) goes through the HTML extraction; this also helps .msg files whose HTML exists only inside the RTF body..ics, .vcf) - events with start, end, location, organizer, attendees, recurrence and description, with invitations and cancellations marked as such; contacts with organization, phone, e-mail, address and birthday. Several entries in one file give one section each..csv, .tsv, .json, .xml) - CSV and TSV as a table (the delimiter is detected, including the semicolon from German Excel), JSON and XML as a code block. RSS and Atom feeds become a list of their entries with link, date and text..ipynb) - text cells as text, code cells as code blocks, outputs as text; image outputs only appear as a placeholder..py, .js, .yaml, .sql, .log, Dockerfile and many more) - the file as a code block with the matching language, the file name above it. .env files are deliberately not accepted, since they usually hold credentials. Text formats such as .rst, .adoc and .org are passed through like .txt.Eine Datei, die nicht zum Format passt (keine Mail-Header, keine Untertitel-Zeitstempel, kein Kalendereintrag, Binärdaten als .txt, ein kaputtes oder verschlüsseltes Dokument) oder bei der eine gespeicherte Seite kaum Inhalt enthält, ergibt 422.
A file that does not match its format (no mail headers, no subtitle timestamps, no calendar entry, binary data as .txt, a broken or encrypted document), or a saved page with almost no content, returns 422.
PDF, Word, PowerPoint, Excel, EPUB, ZIP (Inhalt wird aufgelistet), CSV, JSON und XML werden zu Markdown konvertiert. Liegt das Dokument im Netz, reicht die normale API - der Dokumenttyp wird am Content-Type erkannt:
PDF, Word, PowerPoint, Excel, EPUB, ZIP (contents listed), CSV, JSON and XML are converted to Markdown. If the document is online, the regular API is enough - the type is detected from the content type:
curl -s "https://nasser.tail1e8b49.ts.net/api?url=https://example.com/report.pdf"
Lokale Dateien gehen per Upload - rohe Bytes im Body, Content-Type der Datei, maximal 25 MB. In der Web-Oberfläche funktioniert dafür derselbe Drag-and-drop bzw. die Dateiauswahl:
Local files go through the upload path - raw bytes in the body, the file's content type, max 25 MB. In the web UI the same drag-and-drop and file picker handle it:
curl -s -X POST --data-binary @report.pdf \ -H 'Content-Type: application/pdf' \ -H 'X-Filename: report.pdf' \ "https://nasser.tail1e8b49.ts.net/api/file"
Optionale Parameter: format=json|text, frontmatter=true, pdf=ocr. Die Dokument-Konvertierung braucht den markitdown-Sidecar; ist er nicht konfiguriert, antworten Dokument-URLs und /api/file mit 502. Ob diese Instanz ihn hat, verrät /api/config.
Optional parameters: format=json|text, frontmatter=true, pdf=ocr. Document conversion needs the markitdown sidecar; without it, document URLs and /api/file return 502. Whether this instance has it is reported by /api/config.
Bilder und Audio funktionieren über denselben Weg (URL oder Upload). Standardmäßig kommen nur Metadaten zurück - EXIF beim Bild, Track-Infos beim Audio. Hat die Instanz ein Vision- bzw. Speech-to-Text-Modell hinterlegt, kommt eine Bildbeschreibung bzw. ein Transkript dazu; die verwendeten Modelle und der Tokenverbrauch stehen dann im Frontmatter. YouTube-URLs liefern Titel, Beschreibung und Transkript, wenn die Instanz das aktiviert hat. Bildbeschreibung, Transkription und PDF-OCR teilen sich ein Stundenbudget pro Benutzer bzw. Client-Adresse (PULLMD_LLM_RATE_LIMIT, Standard 30); darüber hinaus kommt das Ergebnis ohne Modellaufruf zurück und wird nicht gecacht.
Images and audio work the same way (URL or upload). By default you only get metadata - EXIF for images, track info for audio. If the instance has a vision or speech-to-text model configured, you also get a caption or a transcript; the models used and the token spend then show up in the frontmatter. YouTube URLs return title, description and transcript when the instance enables it. Captioning, transcription and PDF OCR share an hourly budget per user or client address (PULLMD_LLM_RATE_LIMIT, default 30); beyond it you get the result without a model call, and it is not cached.
Bilder in Reddit-Posts: Bei einem Bild-Post ist das Bild oft der eigentliche Inhalt. Hat die Instanz ein Vision-Modell, beschreibt PullMD jedes Bild eines Reddit-Posts (einzelnes Bild, Galerie oder im Text eingebettetes Bild; Bilder in Kommentaren nicht) kurz (ein bis zwei Sätze) und transkribiert den lesbaren Text. Beides steht als Zitatblock direkt unter dem Bild, beschriftet mit Bildbeschreibung und Bildtranskript (mit lang=en: Image description und Image transcript). Das ist standardmäßig aus: PULLMD_EMBEDDED_IMAGES steht auf on-request, Bilder werden also nur mit images=on (MCP: images: "on") verarbeitet; always macht es zum Standard, off schaltet es hart ab. Ohne Anforderung steht unter jedem Bild ohne Transkript die Zeile Bild nicht transkribiert, damit man merkt, dass Kontext fehlt; ohne Vision-Modell oder mit off bleibt der Text unverändert (Ausnahme: früher gespeicherte Transkripte erscheinen weiterhin, sofern die Instanz nicht auf off steht). Im Frontmatter zählt images_transcribed (z. B. 1/2). Grenzen: nur Reddit, nur Bilder von i.redd.it und preview.redd.it (einzelne Bild-Posts als jpg, png oder webp, Galerie- und Textbilder nur, wenn Reddit sie als nicht animiertes Bild ausweist; höchstens 20 MB), höchstens PULLMD_EMBEDDED_IMAGES_MAX Bilder pro Post (Standard 4), 90 Sekunden Gesamtzeit je Post. Jedes neu transkribierte Bild verbraucht eine Einheit des Stundenbudgets. Transkripte werden pro Bild gespeichert: Dasselbe Bild wird nur einmal transkribiert, egal in welcher URL-Form der Post abgerufen wird (old.reddit.com, redd.it-Kurzlink, /s/-Share-Link), solange die Cache-Aufbewahrung es behält; jede spätere Extraktion nutzt es ohne neuen Modellaufruf. Lehnt der Anbieter ein Bild mit 400, 413, 415 oder 422 ab, gilt es nur für dieses gecachte Ergebnis als gescheitert: Solange der Eintrag frisch ist, wird es nicht erneut versucht, nach Ablauf des Eintrags oder über eine andere URL-Form des Posts dagegen schon. Scheitert ein Bild nur vorübergehend (Budget, Zeitlimit, Serverfehler), kommt das Ergebnis trotzdem, wird aber nicht gecacht; die übrigen Bilder des Posts bleiben gespeichert. Der transkribierte Text stammt aus dem Bild und ist nicht vertrauenswürdig (Prompt Injection möglich); die Beschriftung kennzeichnet ihn.
Images in Reddit posts: On an image post the image is often the actual content. If the instance has a vision model, PullMD describes each image of a Reddit post (a single image, a gallery or an image embedded in the text; not images in comments) briefly (one or two sentences) and transcribes the legible text. Both appear as a blockquote directly under the image, labelled Bildbeschreibung and Bildtranskript (with lang=en: Image description and Image transcript). This is off by default: PULLMD_EMBEDDED_IMAGES is on-request, so images are only processed with images=on (MCP: images: "on"); always makes it the default, off switches it off for good. When not requested, every image without a transcript gets an Image not transcribed line so you can tell context is missing; without a vision model, or with off, the text is unchanged (exception: transcripts stored earlier are still shown unless the instance is set to off). In the frontmatter, images_transcribed counts them (e.g. 1/2). Limits: Reddit only, only images from i.redd.it and preview.redd.it (single-image posts as jpg, png or webp, gallery and inline images only when Reddit marks them as non-animated images; at most 20 MB), at most PULLMD_EMBEDDED_IMAGES_MAX images per post (default 4), 90 seconds overall per post. Each newly transcribed image uses one unit of the hourly budget. Transcripts are stored per image: the same image is transcribed once, whichever URL form of the post is requested (old.reddit.com, a redd.it short link, a /s/ share link), for as long as cache retention keeps it; every later extraction reuses it without a new model call. If the provider rejects an image with 400, 413, 415 or 422, it counts as failed for that cached result only: it is not retried while the entry is fresh, but it is retried after the entry expires or with another URL form of the post. If an image only misses temporarily (budget, deadline, server error) the result is still returned but not cached; the post's other images stay stored. The transcribed text comes from the image and is not trustworthy (prompt injection is possible); the label marks it.
Mit ?frontmatter=true wird vor dem Inhalt ein YAML-Block mit Metadaten eingefügt. Felder mit leerem Wert werden weggelassen:
With ?frontmatter=true a YAML metadata block is prepended to the content. Empty fields are omitted:
--- title: "Why I migrated my side-project from Postgres to SQLite" url: https://news.ycombinator.com/item?id=42424242 source: readability fetched: 2026-04-25T13:53:00Z quality: 0.85 author: kentonv published: 2026-04-24T18:42:00Z description: "After two years on managed Postgres..." language: en share_id: a3f9c2b7 ---
Basis-Felder: title, url, source, fetched, quality, author, published, modified, description, language, image, site, extractor_reason, share_id. Je nach Quelle zusätzlich: subreddit, upvotes, images_transcribed (Reddit) · duration, views (YouTube) · image_size, audio_seconds, llm_model, llm_tokens, llm_prompt_tokens, llm_completion_tokens (Media) · pdf_pages (PDF-OCR). MCP-Antworten ergänzen share_url, cached, refreshed, age_ms. Mit PULLMD_FRONTMATTER_FIELDS lässt sich die Auswahl serverseitig einschränken.
Base fields: title, url, source, fetched, quality, author, published, modified, description, language, image, site, extractor_reason, share_id. Depending on the source, additionally: subreddit, upvotes, images_transcribed (Reddit) · duration, views (YouTube) · image_size, audio_seconds, llm_model, llm_tokens, llm_prompt_tokens, llm_completion_tokens (media) · pdf_pages (PDF OCR). MCP responses add share_url, cached, refreshed, age_ms. PULLMD_FRONTMATTER_FIELDS can trim the selection server-side.
Bei aktiver Query-Extraktion kommen extracted, extract_confidence, sections_selected, original_tokens und returned_tokens dazu. Site-Recipes können weitere Felder beisteuern, etwa aus dem JSON-LD einer Seite.
With query extraction active, extracted, extract_confidence, sections_selected, original_tokens and returned_tokens are added. Site recipes can contribute further fields, e.g. from a page's JSON-LD.
In der Web-App schaltet der Frontmatter-Regler die Anzeige sofort um - der YAML-Block erscheint oder verschwindet, ohne dass man erneut auf Pull drücken muss.
In the web app the Frontmatter toggle switches the view instantly - the YAML block appears or disappears with no second Pull.
Manche Seiten brauchen Nachhilfe: eine Paywall-Hülle, ein Cookie-Banner mitten im Text, ein Layout, in dem die Automatik den falschen Block für den Artikel hält. Site-Recipes sind kleine JSON-Einträge pro Host, die genau das geradebiegen.
Some sites need a nudge: a paywall wrapper, a cookie banner in the middle of the text, a layout where the automatic pick grabs the wrong block. Site recipes are small per-host JSON entries that straighten exactly that out.
select.remove) - seitenspezifische Banner, Teaser, Skelette.select.content) - statt die Automatik raten zu lassen, sagt das Recipe direkt, welche Container den Artikel bilden. Ergebnis trägt source: recipe-content.grid-to-table) - Vergleichstabellen, die nur aus CSS-Grid-divs bestehen, werden als echte Tabelle ausgegeben; Icons lassen sich vorher in Text wie "ja"/"nein" umwandeln.select.remove) - site-specific banners, teasers, skeletons.select.content) - instead of letting the automatic pick guess, the recipe says which containers make up the article. Output carries source: recipe-content.grid-to-table) - comparison tables built only from CSS-grid divs come out as a real table; icons can be turned into text such as "yes"/"no" first.Ein paar Recipes werden mitgeliefert. Selbst-Hoster legen eigene unter data/site-recipes.json ab oder setzen PULLMD_SITE_RECIPES auf einen Pfad; nach Änderungen muss die Instanz neu starten. Was geladen wurde und was abgelehnt, zeigt GET /api/recipes/status. Wie man ein Recipe schreibt (und beisteuert), steht in SITE-RECIPES.md.
A few recipes ship with PullMD. Self-hosters add their own in data/site-recipes.json or point PULLMD_SITE_RECIPES at a path; the instance needs a restart after changes. GET /api/recipes/status reports what loaded and what was rejected. How to write (and contribute) one is documented in SITE-RECIPES.md.
Standardmäßig ist eine PullMD-Instanz offen. Self-Hoster können das umstellen; welcher Modus hier läuft, steht in /api/config unter authMode.
By default a PullMD instance is open. Self-hosters can change that; the mode in use here is reported by /api/config as authMode.
| Modus | Bedeutung | Meaning |
|---|---|---|
disabled |
Default. Kein Login, alles offen. | Default. No login, everything open. |
single-admin |
Ein Benutzer aus den Server-Einstellungen, keine Selbstregistrierung. Für den Heimserver. | One user from the server settings, no self-signup. For a homelab. |
multi-user |
Registrierung unter /signup, Login unter /login, getrennte Daten pro Benutzer. Die Registrierung lässt sich mit PULLMD_ALLOW_SIGNUP=false schließen. |
Sign up at /signup, log in at /login, per-user data isolation. Self-signup can be closed with PULLMD_ALLOW_SIGNUP=false. |
Ist ein Modus aktiv, brauchen /api, /api/stream, die Upload-Endpunkte, /mcp sowie History und Archiv Zugangsdaten. Öffentlich bleiben die Startseite, diese Hilfeseite, das Skill-Zip und vor allem die Share-Links unter /s/:id - geteilte Inhalte bleiben also teilbar.
With a mode active, /api, /api/stream, the upload endpoints, /mcp, history and archive all need credentials. The start page, this help page, the skill zip and above all the share links at /s/:id stay public - shared content stays shareable.
/settings erzeugen und als Authorization: Bearer pmd_… mitschicken. Der Schlüssel wird nur einmal angezeigt, danach liegt serverseitig nur noch sein Hash. Das ist der Weg für curl, Skripte und die Claude-Code-CLI.https://nasser.tail1e8b49.ts.net/mcp und landet beim ersten Klick auf der Login- und Zustimmungsseite dieser Instanz. Die so ausgestellten Tokens gelten nur für /mcp; Skripte gegen /api brauchen einen API-Key./settings and send it as Authorization: Bearer pmd_…. The key is shown once; only its hash is kept server-side. This is the path for curl, scripts and the Claude Code CLI.https://nasser.tail1e8b49.ts.net/mcp and the first click takes you to this instance's login and consent screen. Tokens issued this way are valid on /mcp only; scripts calling /api need an API key.curl -s -H "Authorization: Bearer pmd_YOURKEY" \ "https://nasser.tail1e8b49.ts.net/api?url=https://example.com" claude mcp add --transport http pullmd https://nasser.tail1e8b49.ts.net/mcp \ --header "Authorization: Bearer pmd_YOURKEY"
Fehlen die Zugangsdaten, antwortet die API mit 401; im Browser wird stattdessen zur Login-Seite weitergeleitet und danach zurück zur ursprünglichen Seite.
Without credentials the API answers 401; in the browser you are redirected to the login page and back afterwards.
Login und Registrierung sind pro Client-Adresse begrenzt: 10 Login-Versuche pro Minute, 5 Registrierungen pro Stunde, danach kommt das Formular mit 429 und Retry-After zurück. Formulare (Login, Registrierung, Logout, OAuth-Zustimmung) werden nur angenommen, wenn Origin bzw. Referer diesen Host oder den Host von PUBLIC_URL nennt.
Login and signup are limited per client address: 10 login attempts per minute, 5 signups per hour, after that the form comes back with 429 and Retry-After. Form posts (login, signup, logout, OAuth consent) are accepted only when Origin or Referer names this host or the host of PUBLIC_URL.
Selbst-Hoster hinter einem Reverse-Proxy: PULLMD_TRUST_PROXY setzen (meist 1, oder die Adresse des Proxys, wenn der Port auch direkt erreichbar ist) und PUBLIC_URL auf die öffentliche Adresse. Ohne die Einstellung teilen sich alle Clients ein Rate-Limit, weil jede Anfrage von der Adresse des Proxys zu kommen scheint. Der Proxy darf keine Referrer-Policy: no-referrer senden, sonst schicken Browser Formulare mit Origin: null ab, und der Login wird abgelehnt. Details in MIGRATION.md.
Self-hosters behind a reverse proxy: set PULLMD_TRUST_PROXY (usually 1, or the proxy's address when the port is also reachable directly) and PUBLIC_URL to the public address. Without the setting all clients share one rate limit, because every request appears to come from the proxy's address. The proxy must not send Referrer-Policy: no-referrer, or browsers post forms with Origin: null and login is refused. Details in MIGRATION.md.
Konten verwaltet der Betreiber auf der Kommandozeile der Instanz; eine Oberfläche dafür gibt es nicht. In single-admin stammt der eine Benutzer aus den Server-Einstellungen, in multi-user registrieren sich Benutzer selbst - es sei denn, PULLMD_ALLOW_SIGNUP steht auf false. Dann ist create-user der Weg, trotzdem Konten anzulegen.
Accounts are managed by the operator on the instance's command line; there is no UI for it. In single-admin the single user comes from the server settings, in multi-user users sign up themselves - unless PULLMD_ALLOW_SIGNUP is false. In that case create-user is how accounts still get created.
docker compose exec pullmd node scripts/admin.js list-users docker compose exec pullmd node scripts/admin.js create-user someone@example.com docker compose exec pullmd node scripts/admin.js make-admin someone@example.com docker compose exec pullmd node scripts/admin.js reset-password someone@example.com
list-users - alle Konten mit ID, E-Mail, Admin-Flag und Anlagedatum.create-user <email> - legt ein Konto an und fragt das Passwort interaktiv ab (mindestens 8 Zeichen). Das Konto bekommt keine Admin-Rechte.make-admin <email> - befördert ein bestehendes Konto zum Admin. Bewusst ein eigener Schritt.reset-password <email> - setzt das Passwort neu und wirft dabei alle offenen Sitzungen des Benutzers weg.list-users - every account with id, email, admin flag and creation date.create-user <email> - creates an account and prompts for the password interactively (8 characters minimum). The account gets no admin rights.make-admin <email> - promotes an existing account to admin. Deliberately a separate step.reset-password <email> - sets a new password and drops all of that user's open sessions.Ohne Docker läuft derselbe Befehl als node scripts/admin.js … im Verzeichnis der Instanz. Wichtig: create-user und reset-password lesen das Passwort von der Standardeingabe, es braucht also docker compose exec oder docker exec -it. Ein docker exec ohne -i hängt die Eingabe nicht an; der Befehl bricht dann mit einer Meldung und Code 2 ab und nennt die funktionierenden Aufrufe. Alternativ lässt sich das Passwort hineinpipen: echo "…" | docker exec -i <container> node scripts/admin.js ….
Without Docker the same command runs as node scripts/admin.js … in the instance's directory. Important: create-user and reset-password read the password from standard input, so use docker compose exec or docker exec -it. A plain docker exec without -i does not attach input; the command then aborts with a message and exit code 2 that names the invocations which do work. You can also pipe the password in: echo "…" | docker exec -i <container> node scripts/admin.js ….
Jeder Eintrag in der History kriegt ein Client-Badge — daran sieht man, woher der Pull kam:
Every history entry gets a client badge so you can see where the pull originated: