Busabase
Busabase is an open-source (MIT) database and workspace for AI agents — data, docs, skills, and apps in one place. Run it locally, self-host it inside your network, or use Busabase Cloud, and turn age
翻译准备中
TypeScript API client and CLI for the official lebensmittelwarnung.de product-warning feeds — food, cosmetics and consumer-product recalls (Rückrufe), filterable by federal state and product type
翻译准备中
数据TypeScript · JavaScriptGitHub @maschinenlesbar-org在线运营者未确认
试用过了吗? 点一下就好,无需登录。
正在加载完整 README。 以下为已保存的摘录。
Website: English · Deutsch — command reference, guides and API docs Check Germany's official product-warning portal (lebensmittelwarnung.de) from your terminal. lebensmittel is a command-line tool over the portal's official RSS feeds — the current recalls (Rückrufe) for food, cosmetics, and consumer products, narrowable by federal state and product type, as clean JSON you can pipe straight into jq.查看完整 README ↗
Website: English · Deutsch — command reference, guides and API docs
Check Germany's official product-warning portal
(lebensmittelwarnung.de) from your terminal.
lebensmittel is a command-line tool over the portal's official RSS feeds — the
current recalls (Rückrufe) for food, cosmetics, and consumer products, narrowable
by federal state and product type, as clean JSON you can pipe straight into
jq.
--state bayern, --type lebensmittel, combined
or alone; both are applied server-side by the feed.--since <date>, --search <term>, --limit <n>.--compact for scripting, -o <file>
to write to disk.Want to use this as a TypeScript library, or curious how it parses the RSS feeds with zero dependencies? See DEVELOPING.md.
npm i -g @maschinenlesbar.org/lebensmittelwarnung-cli
This installs the lebensmittel command. Requires Node.js 22.12+. No API key.
Check it works:
lebensmittel warnings --limit 1 | jq '.[0].title'
# The 5 most recent recalls, product name + reason
lebensmittel warnings --limit 5 | jq -r '.[] | "\(.title) — \(.reason // "?")"'
# Only recalls affecting Bavaria
lebensmittel warnings --state bayern | jq length
# Only food recalls since the start of the month
lebensmittel warnings --type lebensmittel --since 2026-07-01 \
| jq -r '.[] | "\(.pubDate | split(" ")[1:4] | join(" "))\t\(.title)"'
# Search current recalls by product name
lebensmittel warnings --search schokolade | jq -r '.[].title'
| Command | What it shows |
|---|---|
warnings | Current product warnings (recalls), filterable by --state / --type / --since / --search / --limit |
states | The valid --state Bundesland slugs (offline) |
types | The valid --type product-type slugs (offline) |
New to terms like Rückruf, Grund der Meldung or the Bundesland slugs? The Glossary decodes every one.
warnings options| Option | Meaning |
|---|---|
--state <slug> | Only warnings for one Bundesland — server-side filter. One of the 16 slugs from lebensmittel states (e.g. bayern, nordrheinwestfalen). An unknown slug is a usage error. |
--type <slug> | Only warnings for one product type — server-side filter. One of lebensmittel, kosmetischemittel, bedarfsgegenstaende, mittelzumtaetowieren, babyundkinderprodukte. |
--since <YYYY-MM-DD> | Only warnings whose pubDate is on or after this date, counted in German time (Europe/Berlin), client-side. A warning whose pubDate can't be read as a date is left out, and a note on stderr says how many. pubDate is often the time of the last update, not of first publication: an old notice that was updated comes back as new (see Usage.md). |
--search <term> | Only warnings whose product name (title or product) contains this text (client-side). Case, umlaut spelling (käse, kaese and kase all find "Käse"; Erdnuss finds "Erdnüsse"), ß/ss, accents and Unicode normalisation don't matter. It is still a substring match: Tahini does not find "Tahina", so search a stem (Tahin) before concluding there is no recall. |
--limit <n> | Return at most n warnings, in feed order (most recent first). |
--state and --type are validated against the fixed slug lists, so a typo fails
at parse time (exit 2) rather than silently returning the full, unfiltered feed; a slug is
trimmed ( bayern is bayern), and one with an invisible or control character (a zero-width
space pasted along) is refused, showing it as \u200b. A --search term that is blank, or
holds only invisible characters or combining marks (it would match every warning), or one with
a control, zero-width or bidi character in it, is refused too (exit 2).
Each option takes one value: the feed has no "several states" query, so a repeated
option (--state bayern --state hessen) is a usage error (exit 2) too.
Each item carries typed accessors plus a generic fields map and the raw HTML:
{
"title": "ja! Beerenmischung, tiefgefroren, 750 Gramm Beutel",
"product": "ja! Beerenmischung, tiefgefroren, 750 Gramm Beutel",
"link": "https://www.lebensmittelwarnung.de/.../Meldung.html",
"pubDate": "Wed, 8 Jul 2026 16:00:00 +0200",
"published": "2026-07-08T14:00:00.000Z",
"reason": "Krankheitserreger",
"manufacturer": "Eurogroup España Frutas y Verduras S.A.U., …",
"affectedStates": ["Nordrhein-Westfalen", "Bayern", "…"],
"lotNumbers": "L-26085",
"bestBefore": "Mindesthaltbarkeitsdatum: 15.03.2028",
"packaging": "750 Gramm",
"imageUrls": ["https://www.lebensmittelwarnung.de/.../Bild.jpg?__blob=normal&v=1"],
"images": [{ "url": "https://www.lebensmittelwarnung.de/.../Bild.jpg?__blob=normal&v=1", "credit": "© Firma …" }],
"fields": { "Bildquelle": "© Firma …", "Grund der Meldung": "Krankheitserreger", "…": "…" },
"rawDescription": "<img …/><br/><b>Grund der Meldung:</b> …"
}
title is the product name. In September 2026 the portal served every item's <title>
as an unrendered template ($esc.escapeXml(…); real names were back by 2026-10-05).
When it serves one, the CLI fills title from the notice's Produktbezeichnung /
-beschreibung, which is also product.
The typed fields are extracted from the feed's HTML <description>; fields is the
complete label→value map (a superset, so a label this CLI does not model first-class
is still there), and rawDescription keeps the original markup.
Every command prints JSON to stdout; diagnostics go to stderr, so piping into
jq stays clean. When a label the typed fields depend on (product, reason,
manufacturer, affectedStates) is missing from every warning in the feed, a
WARN record on stderr says so (a portal-side rename; the value is still in fields).
Text in the feed is never changed silently: bytes that are not valid in its charset and half
characters (a reference to a lone surrogate) are replaced with U+FFFD, so the output stays
readable for jq, and a WARN record of lebensmittel.api says how many.
Each line on stderr is a log record: a timestamp (UTC), a level (ERROR, WARN,
INFO) and a topic, the program and the area it comes from (lebensmittel.cli for usage
errors, lebensmittel.api for the feed's answers and the notes on them, a malformed answer
(the HTML shell, an empty body, no RSS) included, lebensmittel.http
for the connection, lebensmittel.output for -o and a failed write to stdout: stdout redirected to a file and cut
short, by a full disk or a file-size limit, exits 1, never 0). By default it is written log4j style;
--log-format jsonl writes one JSON object per line instead. A record is always one line:
a line break, a control character or a bidi control in a message (a server's text, a value
you typed) is written as an escape (\n, \u001b, \u202e), so it can neither split a
record nor forge another one, nor steer the terminal (a refused value you typed shows an
invisible character, such as a zero-width space, as \u200b); a message longer than 4000
characters is cut and ends in … (N more characters):
2026-10-09T14:03:12.481Z WARN [lebensmittel.http] requests to mirror.test are sent unencrypted (http:, not https:)
2026-10-09T14:03:12.700Z WARN [lebensmittel.http] HTTP 503 from www.lebensmittelwarnung.de: retry 1 of 2 in 200 ms
2026-10-09T14:03:12.902Z INFO [lebensmittel.output] Wrote 3808 bytes to recalls.json
lebensmittel --log-format jsonl -o recalls.json warnings 2>log.jsonl # {"ts":"…","level":"INFO","topic":"lebensmittel.output","msg":"Wrote … bytes to recalls.json"}
# Reasons, grouped and counted (a warning can carry several, joined with ", ")
lebensmittel warnings | jq -r '[.[] | (.reason // "?") | split(", ")[]] | group_by(.)[] | "\(.[0]): \(length)"'
# Recalls affecting a given Land (the same set --state hamburg returns)
lebensmittel warnings | jq -r '.[] | select(.affectedStates | index("Hamburg")) | .title'
# All image URLs in the current recalls, each with its own credit (Bildquelle)
lebensmittel warnings | jq -r '.[].images[]? | "\(.url)\t\(.credit // "")"'
Use --compact for single-line JSON and -o <file> to write to a file — both are
global options that work before or after the command.
Exit codes make the CLI easy to use in scripts:
| Code | Meaning |
|---|---|
0 | Success (also --help / --version) |
2 | Bad usage / invalid argument (nothing was sent) |
4 | Not found (404 from the server) |
6 | Network / transport failure (DNS, connection, timeout, size cap) |
1 | Any other error — including a non-RSS response (HTML shell), an empty body, an unfiltered feed with no items, or <item>s outside the channel |
command not found: lebensmittel — the global npm bin directory isn't on your
PATH. It is $(npm prefix -g)/bin; add that to your PATH.1 / "received an HTML page" — the feed URL returned the website's HTML
shell instead of RSS (it may have moved). Check --base-url.1 / "GET : empty response … legacy JSON API" — the old
megov.bayern.de JSON API (bundesAPI spec) is defunct and returns an empty
body; this CLI uses the RSS feeds instead and does not touch it. If you see this,
a proxy or a wrong --base-url returned an empty body.1 / "GET : the unfiltered feed … with no elements" or "Found N
elements that are not a direct child of the first " — the all-Germany feed
lists every active recall and is never empty, and items in a wrapper would be dropped
unread; rather than answer "no recalls", the CLI fails. Try again later; if it persists,
the portal changed its feed.--state/--type are validated, so
an unknown value exits 2. --search/--since that match nothing return [].Given before or after the command, e.g. lebensmittel --compact warnings. Each option
that takes a value takes it once: a repeat (--base-url A --base-url B) is a usage error
(exit 2), not "the last one wins".
| Option | Description |
|---|---|
-V, --version | Print the version number |
-h, --help | Show help for the program or a command |
--compact | Print JSON on a single line instead of pretty-printed |
--log-format <format> | How errors, warnings and notes are written to stderr: text (default; log4j style, 2026-10-09T14:03:12.481Z WARN [lebensmittel.http] …) or jsonl (one JSON object per line: ts, level, topic, msg). stdout is not affected |
-o, --output <file> | Write output to this file instead of stdout (- = stdout; any other value starting with a dash is refused as the next option, so write a file named -x as ./-x), with an INFO record saying how many bytes went where. An existing file (a dangling link too) is refused before any request (exit 2) unless --force is given; a directory, and a file whose directory is missing or not writable, are refused either way (exit 2, before any request; a file or directory that appears during the request is refused at the write, exit 1). The file is written beside the target and renamed over it when complete, so a write cut short (a full disk) or interrupted (Ctrl-C, SIGTERM, SIGHUP: exit 130, 143, 129) leaves the old file as it was and no half-written or temporary one. A device, a FIFO or the shell's own descriptor (/dev/null, /dev/stdout, >(gzip > x.gz)) is written in place, without --force |
--force | Overwrite the --output file if it already exists, keeping its mode; through a symlink, the file it points to, which is created if the link dangles (only with -o) |
--base-url <url> | API base URL (default https://www.lebensmittelwarnung.de; http:/https: only, no query ? or fragment #, no surrounding whitespace; a literal % in a password is written %25). A user:password@ in it is sent as Basic auth and shown as in error messages. A plain URL to a host other than loopback (, , ) logs one record of () on stderr before the first request (naming the URL's credentials instead when it carries any, never printing them); stdout and the exit code are unchanged |
This CLI is a client — it accesses data it does not own or redistribute. The warnings are © the publishing Länder / the BVL and licensed separately from this tool's code. See DATA_LICENSE.md.
lebensmittelwarnung.de — the portal's content is copyright-protected. Reuse is allowed only unaltered, in full, and with the prescribed source citation (
Portal www.lebensmittelwarnung.de, [Jahr]: [Titel], [URL], Stand: [Datum]); partial/excerpted use and mixing with other sources are not permitted, and once a warning is withdrawn upstream you must delete your copy. This tool provides access; it does not grant you those rights — read DATA_LICENSE.md.
Dual-licensed — use it under either:
See LICENSING.md for details, and CONTRIBUTING.md for the contribution policy (this project does not accept external code contributions). Commercial enquiries: [email protected].
***@http:localhost127.0.0.0/8::1WARNlebensmittel.httprequests to <host> are sent unencrypted (http:, not https:)--timeout <ms> | Time limit per request, reading the whole response included (default 30000; 0 = none; at most 2147483647). It bounds each attempt; the waits between retries come on top |
--user-agent <ua> | User-Agent header value (non-blank, Latin-1, no control characters; else exit 2) |
--max-retries <n> | Retries for transient 429/503 responses and reset connections (0..10, default 2); a refused connection, a DNS failure and a timeout are not retried. Each waits 200 ms × attempt, or the server's Retry-After (seconds or HTTP-date) when that is longer; a Retry-After above 30 s is not retried, and the error names the requested wait. Each retry logs one WARN record of lebensmittel.http before it waits (HTTP 503 from host: retry 1 of 3 in 2 s) |
--max-response-bytes <n> | Cap response body size in bytes (0 = unlimited; default 100 MiB) |