nomnom stores nothing about food; it computes what you feed it.
Architecture contract: contributors and coding agents must follow
AGENTS.mdanddocs/ARCHITECTURE.md.
nomnomcli is an agent-first nutrition ledger. Version 0.4 ships zero food records: no food
database, synonym corpus, or piece-weight table is hidden in the package. It resolves food at
runtime, performs nutrition arithmetic in code, and stores successful logs plus a user-owned cache
in SQLite. There is no LLM in the program and no invented nutrition fallback.
Raw voice, text, or photo is the primary user interface: the surrounding system should resolve or capture it safely instead of asking the user to research nutrition facts or operate a database.
flowchart LR
A[Human or agent] -->|food and quantity| N[nomnom CLI]
N --> L[(user food aliases)]
N --> C[(user food cache)]
N -->|free-text search| O[Open Food Facts API]
N -->|when key is configured| U[USDA FoodData Central API]
A -->|barcode or extracted label facts| N
N -->|resolved JSON or structured error| A
curl -fsSL https://raw.githubusercontent.com/maxjustships/nomnomcli/main/install.sh | shThis creates or updates a user-level nomnom command in $HOME/.local/bin. The installer tries
uv tool install first, then pipx, then a non-virtualenv Python 3.11+ user-site install. Every
installer selection, invocation, and executable lookup runs in a target-user sanitized environment:
inherited agent UV_TOOL_*, PIPX_*, and XDG tool-location overrides are ignored, and uv/pipx are
explicitly directed to target-home defaults. It verifies the actual executable, nomnom --version,
and nomnom doctor --json in that sanitized user/system-only environment. Bootstrap verification
uses $HOME/.config; every NOMNOM_* override is ignored. It never opens the meal database, cache,
or aliases.
For machine-readable agent output:
curl -fsSL https://raw.githubusercontent.com/maxjustships/nomnomcli/main/install.sh \
| sh -s -- --status-jsonThe structured status is installed_base_ready, installed_and_ready,
installed_path_repair_needed, or error. A healthy no-token install is
installed_base_ready; installed_and_ready is reserved for configured, reachable USDA enhanced
coverage. JSON also reports generic_coverage as base or enhanced and an
optional_usda_setup action when relevant. PATH repair remains the top-level status while retaining
those capability fields. No credential value is emitted. --dry-run remains available.
Check the optional generic-food connection without a prompt:
nomnom setup --status --jsonWithout a key it reports status: base_ready and generic_coverage: base; base cache, aliases,
OFF barcode/full-text, and source-backed label capture are ready. USDA provides optional broader no-photo generic/raw-food coverage. A configured and reachable key reports status: connected and
generic_coverage: enhanced. Run nomnom setup only when that broader coverage is wanted or a
specific food cannot be safely resolved. Setup validates the key before saving and never opens a
browser by itself.
USDA credentials are local user configuration, never repository or database content. The default
path is $XDG_CONFIG_HOME/nomnomcli/config.toml or ~/.config/nomnomcli/config.toml, written with
owner-only 0600 permissions. NOMNOM_USDA_KEY is the non-interactive/CI option and takes
precedence over the stored key. Avoid putting it in commands, logs, or checked-in environment
files.
nomnom log --parse "rice 150 g, eggs 2 pieces" --json
nomnom log --food "chickpeas, cooked" --grams 120 --jsonFor a remembered meal from a prior local calendar day, pass an explicit ISO date to either form:
nomnom log --parse "rice 150 g, eggs 2 pieces" --date 2026-07-20 --json
nomnom log --food "chickpeas, cooked" --grams 120 --date 2026-07-20 --json--date accepts only YYYY-MM-DD; it never parses a time or a date from the food text. The date
is interpreted in the user's local timezone and stored deterministically at local noon on that
day. Successful log JSON includes the effective offset-aware logged_at and local_date, whether
or not --date was supplied. Without --date, logging keeps its existing current-local-time
behavior. Malformed, impossible, and future dates are rejected without a database write; today is
allowed. Use this CLI path for remembered meals—never edit the SQLite database directly.
Resolution is deterministic and ordered:
- exact phrase in the user's alias table, pointing to an exact local cache name;
- exact match in the user's
food_cache; - safe cache search backed by canonical name/source identity, never
lookup_querymetadata alone; - an exact Open Food Facts product only when the input explicitly matches its brand/SKU;
- a safe USDA FoodData Central Foundation/SR Legacy generic proxy, when a setup key or
NOMNOM_USDA_KEYis configured; - a strictly matching Open Food Facts generic proxy, including a labelled branded candidate only when its returned product name/type is safe for the unbranded input;
- actionable JSON error—never a guessed food.
Open Food Facts candidates need complete normalized query-token coverage from the returned product
name, a matching food type/category, a source identity/barcode, and complete positive finite kcal,
protein, fat, and carbs. An exact product requires a user-supplied barcode, an explicitly matched
brand/SKU, or an exact local pin/alias. Candidate confidence never establishes exact identity. A
safe branded OFF candidate used for unbranded text is always a generic_proxy, retains its source,
brand, barcode, and explicit assumption, and remains subject to the configured proxy policy. An
unsafe candidate returns food_needs_source. Rejected results are neither cached nor logged, and a
generic-proxy cache entry cannot satisfy a later branded query.
For every automatic local match, lookup_query is retrieval metadata rather than identity proof.
An exact normalized cache name, explicit alias, exact barcode, or safe existing source-backed
identity is required. Unsafe fuzzy and legacy hits remain visible through nomnom search, but
resolution falls through to a provider or an actionable refusal instead of silently logging them.
Successful API results are cached in the user's database, so the same food can resolve locally
later. Existing cache records, logs, recipes, and aliases are preserved when v0.4 opens the
database.
nomnom search QUERY searches this user cache; it is not a packaged food catalog.
Open Food Facts free-text search goes directly through the official legacy v1 endpoint,
https://world.openfoodfacts.org/cgi/search.pl, with search_terms, search_simple=1,
action=process, json=1, page_size, the supported response fields, and a descriptive
nomnomcli User-Agent. API v2 search is structured/filter-only: nomnom never sends free-text
search_terms to v2 and never falls back to unfiltered v2 catalog rows. Direct requests calls are
intentional because this small provider contract remains explicit and replay-testable.
Both OFF capabilities use bounded backoff for HTTP 429 and 5xx responses and safely honor a
numeric, bounded Retry-After. If v1 remains unavailable after retries, the OFF client raises the
typed retryable error openfoodfacts_unavailable; the no-key resolver preserves it inside
food_needs_source.provider_error. It never silently changes endpoint or returns unrelated
products. Product normalization and the token/category confidence checks still apply to every v1
candidate.
Check first, then run the one-time guided connection only if needed:
nomnom setup --status --json
nomnom setup
nomnom doctor --jsonGet a free key at the official signup page https://fdc.nal.usda.gov/api-key-signup.html. For non-interactive/CI use only, set it in the environment:
export NOMNOM_USDA_KEY="your-key"Without a key, a food that OFF cannot safely resolve returns food_needs_source, not a provider-key
failure. Its first-screen actions offer a package photo, barcode, source-backed capture label, and
the user's local cache/aliases. USDA appears separately as an optional enhancement for broader
no-photo generic/raw-food coverage. The nested provider_error preserves OFF candidate,
alternatives, retryability, and other technical diagnostics. Nothing unsafe is cached or logged.
When enabled, USDA search requires complete positive kcal/protein/fat/carbs, scores query token
overlap together with data type and category, prefers Foundation and SR Legacy, and enforces a
confidence floor. Weak matches return usda_low_confidence with candidate alternatives and are
never cached.
The default generic policy is allow_for_unbranded. A USDA result becomes a generic proxy only
when it is an unbranded Foundation or SR Legacy record with an FDC id and every
normalized query token is covered by its name. Brand/SKU-like or unmatched input and branded USDA
records return exact_resolution_required without cache or log writes. Accepted proxies expose
resolution_mode=generic_proxy, source=usda, the FDC source_id, provenance=usda, confidence,
and an explicit assumption in log JSON.
Choose a stricter policy in the user config:
[resolution]
generic_proxy_policy = "ask" # or "exact_only"NOMNOM_GENERIC_PROXY_POLICY overrides the file. ask returns
generic_proxy_confirmation_required with the candidate but writes nothing; exact_only requires
barcode or package-label capture. Supported values are allow_for_unbranded, ask, and
exact_only.
Set NOMNOM_OFFLINE=1 to prevent all remote food lookup. Set NOMNOM_DISABLE_OFF=1 to skip OFF
while retaining USDA when its key is configured.
When an exact packaged product is needed, use its barcode or ask the user for a package photo. A barcode capture calls only the Open Food Facts v2 product endpoint; it never sends free text:
nomnom capture barcode "0123456789012" --jsonIf the barcode is absent or OFF lacks complete core nutrition, the agent reads the supplied label photo and passes the extracted per-100 g facts to the CLI:
nomnom capture label \
--name "chicken pastrami" --brand "Example" \
--kcal 110 --protein 20 --fat 2 --carbs 3 \
--serving-grams 75 \
--source-note "image:sha256:LOCAL_REFERENCE" --json--source-note is required. Use a nonempty local or opaque image/barcode reference that lets the
user trace the facts without putting the image itself in SQLite. The CLI has no OCR or vision
dependency, never receives or stores the photo, never estimates missing macros, and rejects
non-finite/negative values or a non-positive serving weight without writing. Both capture paths
persist resolution_mode=exact_product, source identity, provenance, and the normalized nutrition
facts; the canonical name can then be used in an alias and logged offline.
nomnom add remains available for existing manual workflows. For a new packaged product, prefer
the source-backed capture commands above. Use only verified per-100 g label values:
nomnom add \
--name "whole-grain bread" --brand "Example Bakery" \
--kcal 250 --protein 9 --fat 4 --carbs 45 \
--piece-grams 40 --jsonThe optional --piece-grams is the only manual piece-weight input. It makes later piece counts
work from the cached record.
Aliases are explicit user-owned mappings stored only in the same user SQLite database as the food cache. Their targets must be exact canonical names already present in that cache; creating or using an alias never performs a remote target lookup. Alias matching is exact after case and whitespace normalization, so an alias does not match unrelated longer food names.
nomnom alias add "хлеб harry's" "harry's american sandwich — Harry's" --json
nomnom alias list --json
nomnom alias remove "хлеб harry's" --jsonAdd or resolve the canonical food first. A missing target returns alias_target_not_found; adding
an existing phrase returns alias_exists; removing a missing phrase returns alias_not_found.
For one unstructured diary meal, use the versioned agent-first flow. Discover each raw item without opening the diary:
nomnom agent candidates --input "raw tomato 60 g" --jsonDiscovery returns deterministic provider metadata, candidate status, and opaque references such as
usda:123 or off:0123456789012; it returns no nutrition fields for an agent plan.
agent_selection_eligible means the provider record is source-unbranded and structurally valid for
an external agent's explicit semantic choice. pending_capture_required must become an explicit
pending item, and identity_rejected must not be selected. Candidate output also reports whether
the older direct source_ref form satisfies strict literal identity.
Commit all items as one strict plan and one journal event:
nomnom agent intake --plan '{
"version": 1,
"items": [
{
"input": "raw tomato 60 g",
"grams": 60,
"selection": {
"source_ref": "usda:123",
"relation": "semantic_equivalent",
"assumption": "Interpreted raw tomato as the source's ripe raw tomato record."
}
},
{
"input": "Example Brand bread two slices",
"pending_capture": {"status": "pending_capture", "action": "photo_or_barcode"}
}
]
}' --jsonThe item key allowlist is input, optional positive grams, and exactly one of direct source_ref,
selection, or pending_capture. A selection contains exactly source_ref,
relation="semantic_equivalent", and a nonempty human-readable assumption. The top level allows
only version, items, and optional portion_estimates, whose shape is the existing external
estimate contract below. Calories, macros, nutrition, source facts, unknown keys, duplicate refs,
non-finite numbers, and mismatched estimates reject the whole plan before a journal write.
On commit, nomnom re-fetches every chosen ref, validates source identity plus complete finite
nutrition, applies generic policy, calculates, and persists. It never uses a cache hit or agent
nutrition. An accepted external choice is always selection_mode=agent_generic,
resolution_mode=generic_proxy, and provenance=agent_selected; output and the journal retain raw
input, canonical source name/ref, relation, and assumption. Direct source_ref keeps strict literal
identity behavior. A branded/SKU source—including an OFF text-ranked result—cannot be selected as
exact or generic: preserve it with pending_capture, then use barcode/photo capture and explicit
nomnom log remove LOG_ID --confirm --json plus a replacement entry when corrected. Intake output
exposes log_id and each pending item_id. Pending items have no nutrition fields; intake and stats
report nutrition_status=incomplete and resolved-only totals.
The canonical shape supplied to nomnom is food name + quantity + unit + optional modifiers.
For example: egg 3 pieces, rice 150 g, or bread 2 pieces at 40 g. Quantity and unit are
required; modifiers may express a fraction, size, or explicit per-piece mass.
An agent translates the user's language into this contract before invoking nomnom. Translation may choose a canonical food name already known to the user cache or an explicit user alias. Nutrition resolution is a separate deterministic step: alias → local cache → exact intent or generic provider proxy → error. The agent must not translate by inventing nutrition values or silently substituting another food.
The parser accepts kilograms, grams, millilitres, pieces, fractions, and explicit per-piece grams.
English and Russian size words such as small and небольшой remain valid syntax, but a size word
does not make nomnom guess a weight. By default, piece grams come only from explicit user input or
serving data on the resolved cached/pinned/API food record. Assumptions identify the provider,
source field, and returned value. When serving data is absent, the default strict portion policy
keeps the existing piece_weight_unknown response and writes no log. Explicit grams always win,
including 3 pieces FOOD at 38g → 114g.
nomnom log --parse "bread 2 pieces at 40g" --json
nomnom log --parse "яичница из 3 небольших яиц" --jsonSupported dish prefixes split only the ingredients the user stated. nomnom never silently adds oil or another ingredient. Millilitres use a resolved density when available and otherwise retain the documented 1 g/ml conversion.
An external agent may supply masses for unresolved counts, fractions, and size descriptions. This
is opt-in: use --portion-policy estimate together with inline --portion-estimates JSON. nomnom
does not contain an LLM, generate a mass, bundle portion weights, or treat the supplied mass as
measured or source-backed. It uses each supplied central grams value in the existing deterministic
nutrition calculator; the lower/upper range is provenance only and is not used to calculate a
nutrition interval.
The payload has exactly one top-level items array. Every entry has exactly these fields:
item_index: zero-based position after nomnom splits the original--parsetext;input: the exact trimmed item phrase from that text, matched byte-for-byte;grams,lower_grams,upper_grams: finite nonnegative numbers satisfyinglower_grams <= grams <= upper_grams;confidence: finite number from 0 through 1;method: the literal stringagent_estimate;assumption: a nonempty human-readable explanation.
For example:
nomnom log --parse \
"3 small fried eggs, half small tomato, half small onion, whole wheat bread 180 g, milk 110 g, 15 dates" \
--portion-policy estimate \
--portion-estimates '{"items":[
{"item_index":0,"input":"3 small fried eggs","grams":135,"lower_grams":120,"upper_grams":150,"confidence":0.72,"method":"agent_estimate","assumption":"Three small fried eggs estimated at 45 g each."},
{"item_index":1,"input":"half small tomato","grams":35,"lower_grams":25,"upper_grams":45,"confidence":0.65,"method":"agent_estimate","assumption":"Half of a small tomato estimated at 35 g."},
{"item_index":2,"input":"half small onion","grams":30,"lower_grams":20,"upper_grams":40,"confidence":0.63,"method":"agent_estimate","assumption":"Half of a small onion estimated at 30 g."},
{"item_index":5,"input":"15 dates","grams":120,"lower_grams":90,"upper_grams":150,"confidence":0.58,"method":"agent_estimate","assumption":"Fifteen dates estimated at 8 g each."}
]}' --jsonEvery unresolved fuzzy item must have exactly one matching entry. Missing, extra, duplicate, or
non-exact mappings and malformed/invalid values reject the complete meal without a log write;
entries for explicit gram items are also rejected. Only inline JSON is supported. ask returns a
structured portion_estimate_required response with the exact index/input and writes nothing.
strict remains the default. Set a persistent default in user config or override it in the
environment:
[resolution]
portion_policy = "ask" # or "estimate"; default is "strict"NOMNOM_PORTION_POLICY accepts strict, ask, or estimate; the CLI flag has highest precedence.
Estimated log items persist approximate=true, portion_provenance=agent_estimate, and the full
portion_estimate object next to food-resolution provenance. Log JSON and date stats expose these
fields. Human output gives one correction prompt toward scale grams, a photo, or a barcode. Existing
logs without these additive fields remain readable.
Provide an agent-side translator that emits the canonical contract and, when desired, create
user-specific food-name mappings with nomnom alias add. Food translations are user data, not
package data.
Parser syntax aliases for ordinary units, fractions, sizes, per-piece markers, dish prefixes, and
conjunctions are declarative tables in nomnomcli/parser.py. Add reviewed forms to those tables and
their tests; parser code changes are not required for ordinary unit aliases. A new language must
still provide quantity/unit forms unambiguously, and its agent translator remains responsible for
food names.
Add --json for stable machine-readable output. User-correctable failures are written to stderr as
an error object and exit with status 2. Important codes include:
food_needs_source: use the returned photo, barcode, label-capture, or local-cache path; inspect nestedprovider_errorfor OFF diagnostics. USDA is an explicitly optional enhancement.off_low_confidence: retained as nested technical detail when OFF candidates fail strict safety checks.usda_low_confidence: inspect the FDC candidate/data type/category and retry more specifically; no near match was cached.usda_invalid_nutrition: every USDA candidate lacked one or more complete positive core values.generic_proxy_confirmation_required: show the named provider candidate and ask before changing the configured policy; nothing was cached or logged.exact_resolution_required: request the barcode or a package photo for source-backed capture.invalid_barcode/barcode_not_found/barcode_nutrition_incomplete: correct the barcode or request a package photo; failed captures write nothing.invalid_source_note/invalid_nutrition: correct the extracted label facts; failed captures write nothing.piece_weight_unknown: ask for grams or add a verified--piece-gramsvalue.portion_estimate_required/portion_estimate_missing: supply exactly matched external estimate metadata or use explicit grams; nothing is partially logged.portion_estimates_malformed/portion_estimate_invalid/portion_estimate_mismatch: correct the complete inline payload and retry.alias_target_not_found: add/resolve the exact cached target or remove the stale alias.invalid_log_id/log_not_found: supply the positive ID of an existing journal record.log_removal_confirmation_required: inspect the selected record, then repeat the removal with explicit--confirm; no record was deleted.openfoodfacts_unavailable/usda_unavailable: retry later or use a manual label.
Provider-unavailable errors include a retryable boolean. In nomnom doctor --json, OFF reports:
product_lookup_reachable: the v2 product-by-barcode endpoint answered after bounded retries; this says nothing about free-text search.full_text_search_ready: the same v1 CGI capability used by runtime free-text resolution answered with a valid product-list payload; when false, OFF free-text resolution is unavailable.
configured means OFF needs no credential. USDA retains configured, reachable, and
key_source. Doctor never includes credential values.
Successful logs are stored immediately. Agents should show the returned names, grams, confidence, alternatives, and assumptions before treating the resolution as confirmed.
To reverse one mistaken journal entry, use the public confirmed correction path:
nomnom log remove LOG_ID --confirm --jsonThe ID must be a positive existing log ID. Missing confirmation, invalid IDs, and missing records return structured errors without deleting anything. A successful call transactionally removes exactly that record and returns its ID, timestamp, kind, label, and totals; subsequent stats exclude it. Do not edit the SQLite database directly.
nomnom stats today --json
nomnom stats week --json
nomnom stats date 2026-07-20 --json
nomnom recipe add "https://example.com/recipe" --servings 4 --json
nomnom recipe log "Recipe name" --portions 1.5 --jsonstats date uses the same user-local calendar-day boundary as log --date: local midnight is
inclusive and the next local midnight is exclusive. Existing today and week behavior is
unchanged.
Recipe ingredients use the same runtime resolver. An unresolved ingredient fails the whole import instead of storing partial nutrition.
User data defaults to ~/.local/share/nomnomcli/nomnom.sqlite3. Override it with
NOMNOM_DB_PATH. Schema v4 upgrades preserve cached foods, logs, recipes, and aliases in place and
add resolution mode, source identity/note, provenance, and assumption fields to the food cache.
The repository agent workflow is skill/SKILL.md. It teaches agents to use
nomnom's JSON, accept only safe generic proxies, request a barcode/package photo for exact products,
capture extracted label facts with a source note, and never estimate nutrition in their own
context.
python -m pip install -e '.[dev]'
PYTHONPATH=. pytest -q
ruff check .All API tests use payloads under tests/fixtures/ and mocked HTTP; the test suite never requires
network access.
GNU Affero General Public License v3.0. See LICENSE.