assist-kitchen
Read and write the kitchen module (consumption journal + inventory), via the bundled
kitchen-axi CLI — a script in this skill's scripts/ directory (not on PATH).
Invoke it by its path relative to this skill's base directory:
scripts/kitchen-axi # home: today's totals + eat-first + questions
scripts/kitchen-axi --help # full command list
scripts/kitchen-axi <group> --help # usage for any command group
Output is TOON (compact tables); pass --json for raw API
JSON. The server defaults to http://localhost:2529; override with the
CLAUDE_ASSIST_SERVER environment variable. The full behavioral contract is
specs/modules/kitchen.md — the rules below are the ones that decide a write; when
in doubt, read the spec.
Decisive rules (read before writing)
Base vs effective macros. Every entry stores and returns base macros (
calories,protein_g, …) plus aportion_multiplier. The wire never carries pre-scaled numbers. Effective is alwaysbase × portion_multiplier, computed by the consumer — the CLI'sentries show/listand the home totals already do this. When you report what someone actually ate, use the effective value, not the base field.--multiplieris the post-hoc "I only ate half" knob. It rescales the base and nothing else: it never re-queues estimation, never changessource, and never 409s. It always rescales from the base, so it's idempotent —--multiplier 0.5then--multiplier 0.75yields0.75 × base, not0.375 × base. Reach for it whenever the amount eaten differs from what was logged; do not hand-edit macro fields for that.A macro override is terminal. Setting any macro on
entries patch(--calories,--protein, …) marks the entrysource: manual— the owner's correction, which no later model pass may overwrite (a model re-estimate attempt 409s). Only override when you mean to pin the number; use--note/--label(which re-queue estimation) or--multiplier(which rescales) otherwise. An override sets the base; a multiplier still scales it.Backdating a meal? Supply a specific local time — a bare date is only a backstop.
--atsetslogged_at, the moment the meal actually happened (a gallery photo logged hours later belongs on the meal's day, not the logging day). When you know the time, send a full local timestamp with offset (--at 2026-04-29T14:30:00-04:00) so it buckets exactly. A bareYYYY-MM-DDis accepted as a fallback and coerces to noon in the machine's local timezone — never midnight UTC, which is the previous evening across US zones and would land the entry a day early. Same rule applies toentries patch --atandexpenditure log --at. Omit--atentirely to default to now.The
dayfield is authoritative for bucketing — never derive a day from a timestamp. Every entry, expenditure, and weigh-in row carries aday(YYYY-MM-DD) computed by the server in the instance owner's timezone, and displayed instants render in that zone (e.g.2026-07-25T20:47:00-04:00), never a bare…Z. When you group, filter, or ask "what did I eat on ", key offday. Do not parselogged_at/occurred_at(the raw UTC instant, kept only for ordering) and slice a date off it — a meal logged at00:47Zis the previous evening across US zones, and hand-bucketing by the UTC date is the recurring error this field exists to kill. You never supply or compute a timezone; the module owns it.Multi-day or weekly totals? Call
daysonce — never list entries and hand-sum them.kitchen-axi days [--since <n|date>]returns one pre-computed row per owner-local day (the eight-field panel + calories + the net line), bucketed server-side. Reach for it for any "how did the week go", weekly-review, or per-day-trend question. Summingentries listrows yourself is both the expensive way and the exact operation that mis-buckets by UTC —daysis the correct, single-call aggregate. The home view already reports today's owner-local totals;daysextends that across the window.Already have the exact macros? State them at log time — never log-then-patch. When a caller has already computed the full panel (a page/UI totalled it, a resolved label scan, an import), pass the fields on
entries log(--calories/--protein/…--label). That records a born-manual, terminal entry verbatim, with no estimation ever enqueued — mutually exclusive with--recipe/--component. Do not log the raw inputs (--component) and thenpatchthe totals: that path re-derives numbers you already hold (lossy) and is racy — the estimation job it kicks off can land after your patch and clobber it. If you know the answer, state it; the module won't re-guess it.Partial toss vs full toss. On
inventory event <ulid> tossed,--fractionis the amount tossed, not the amount remaining. A partial toss (--fraction 0.25) decrementson_hand_fractionand keeps the item alive (inventory is directional and self-heals at the next event). The item only goes terminaltossedwhen--fractionis omitted (full toss) or the remainder hits zero. Contrastopened(where--fractionis the absolute remaining fraction) andfinished(always terminal, zeroed).Never retire a record with
finishedortossedjust to get it out of the way. Those are claims about food:finishedsays it was eaten,tossedsays it was wasted and feeds waste telemetry someone will later act on. For a record that was never real stock — a phantom, a mis-scan, a housewares line — useinventory dismiss <ulid>, the one terminal that claims neither. If two records describe one physical package, useinventory merge <dupe> --into <survivor>instead: dismiss retires a row but relinks nothing, so the duplicate's consumption entries, receipt line, and conversions would be stranded. Pick the survivor by whose clock is honest (usually the earlieracquired_at) — merge fills only its empty fields and never sums quantities. If you have already closed something wrongly,inventory recount <ulid> --state stockedresurrects it first.Fixing a recipe? Push it again under the same name — never push a variant.
recipes pushupserts: the key is the normalized name (case- and spacing-insensitive), or--ulidfor one specific record, and the output says whether it created or replaced. Recipes are tapped from the strip by name, so a second same-named recipe is indistinguishable from the first and the stale one keeps logging wrong numbers on every tap. Never work around a collision by appending "v2" or "(fixed)" to the name. A name already held by apromotedorsheetrecipe is a409naming the record — rename, or pass--ulidif you really mean to replace that one. To retire a recipe,recipes delete <ulid>: it archives (off the strip permanently, still resolvable so entries logged from it and prepped items derived from it keep working). There is no hard delete.Never fix a logged entry by deleting and re-logging it. All nine panel fields are correctable in place with
entries patch(--calories --protein --fat --sat-fat --carbs --sugar --added-sugar --fiber --sodium).entries delete+entries logmints a new ULID, which breaks the inventory link, the reselect strip'sentry_ulid, and anything else pointing at the entry. Delete is for an entry that shouldn't exist, not for a wrong number.Total sugar and added sugar are two different numbers, and only one has a line.
--sugaris TOTAL sugar (intrinsic + added) and carries no target — there is no established guideline for it, so it is context, never a breach.--added-sugaris the share added in processing or preparation and is the one with a ceiling. Unprocessed whole foods (fruit, vegetables, plain dairy, eggs, meat, fish, plain grains) are--added-sugar 0— a 0 you state, not a field you omit: an omitted field isnull(unknown) and silently drops the day's added-sugar total. Fruit juice counts as added even when it's "100% juice". Don't read a bigsugarnumber as a problem; readadded_sugar.Deliberate actions never go through the ambient classifier. These commands are the deliberate paths — logging a meal, scanning a receipt, an explicit
inventory event— and they hit the module directly. The free-textinventory remarkresolver is for passing remarks ("opened the oat milk"); it best-effort matches an item and infers the event, and an unmatched remark returns{matched:false}— that's normal, not an error. Don't escalate an unmatched remark; fall back toinventory list+ an explicitevent.Photos are ephemeral. Meal and receipt/label photos are held in memory only for the model call and discarded on every outcome — never persisted. Records carry no image data, and there's nothing to re-OCR later. If a scan needs retrying, re-post the photo files.
Prepped food is a
convert, never a plaininventory add. When the owner has prepared something — overnight-oats jars, hard-boiled eggs, cooked quinoa, a batch of anything — record it withinventory convert, notinventory add. Only aconvertthat carries a--torecipe_ulidmints a consume-eligible item (the one-tapinventory consume/ the app's "Ready in your kitchen" shelf reads its macros from that recipe). A plaininventory addmakes an ordinary grocery-style item with no recipe provenance — it can never reach the shelf, so a jar added that way is a dead end the owner has to log the slow way forever.--fromis optional: pass the raw sources to decrement them, or omit it entirely for a source-less "I made this" when the inputs weren't tracked. So: raw groceries →inventory add; anything you made →inventory convert --to '{…,"recipe_ulid":"…"}'. If no recipe exists yet,recipes pushone first (its components fix the macros), then convert against it.A cost of
nullmeans unknown, and never zero. Both money reads —products pricesandinventory waste— are derived from the prices receipts already recorded, so they are honest about gaps. An unreadable line price, a manually-seeded item, or a product bought before receipt scanning has no price on file: the row readscost null/basis unknown, and the waste totals sum only the known rows and count the unknown ones separately. Never report a null as free or fold it in as a 0 — that would make waste look costless, which is the opposite of what the read is for. Same rule on the price side: a nullper_100gmeans no package size resolved for that purchase, not a free package, and it is fixed by scanning the label or setting--package-size, never by assuming a size.
Common workflows
- "Log what I just ate" →
scripts/kitchen-axi entries log "<description>"(the deliberate no-model path). For a known recipe, add--recipe <ulid>and--component "label=grams"for exact, deterministic macros. - "I only ate half of that" →
scripts/kitchen-axi entries patch <ulid> --multiplier 0.5. - "Fix the calories on that entry" →
scripts/kitchen-axi entries patch <ulid> --calories N(terminal manual override — use only when you mean to pin it). - "Log a meal whose macros I already computed" (a page/UI totalled it) →
scripts/kitchen-axi entries log --calories N --protein N --sat-fat N --sugar N --added-sugar N --fiber N --sodium N --label "<meal>"(directly-stated panel: born-manual, terminal, no estimation, no race — not--component+patch). - "What's today looking like?" → bare
scripts/kitchen-axi(home view: effective totals, pending estimates, eat-first items, open questions). Fields with an owner-set daily target render aslogged / targetwith remaining — amaxcounts down what's left, amincounts up to met; never derived from the day's burn. - "What should I use up?" →
scripts/kitchen-axi inventory list(eat-first order). - "I made a batch of X" (meal prep) → record it as a conversion so it lands
consume-eligible, not a plain add. First ensure a recipe exists (
recipes list; if not,recipes push '{"name":"…","components":[{"label":"…","default_qty_g":N,"per_100g":{"calories":N,"protein_g":N,"sat_fat_g":N}}]}'— components fix the macros). Thenscripts/kitchen-axi inventory convert --to '{"name":"<prepared item>","units_total":<N jars/portions>,"recipe_ulid":"<recipe>"}', adding--from <ulid>[:amount]…for each raw source you want decremented (omit--fromentirely if the inputs weren't tracked). The derived dish defaults to thepreparedshelf-life class (~4 days from when it was made); pass"shelf_life_class":"produce"for something that keeps ~a week (hard-boiled eggs) or"very_perishable"for a 2-3-day item (cut fruit). The result is on the "Ready in your kitchen" shelf for one-tapinventory consumelater. - "I opened / finished / tossed X" →
scripts/kitchen-axi inventory remark "<what happened>", orinventory event <ulid> <type>when you have the item. - "Scan this receipt" →
scripts/kitchen-axi receipts scan <photo…> --store "<store>", thenreceipts show <ulid>to see extracted lines (parses asynchronously). - "What has this cost me / is it cheaper at the other store?" →
scripts/kitchen-axi products prices <ulid>(add--store "<store>"to scope). Compare theper_100g/per_100mlcolumns, never the raw prices — packages differ in size between purchases and stores, which is exactly what the normalization is for. - "How much food am I throwing away?" →
scripts/kitchen-axi inventory waste [--since DATE]. Each row is one toss with the amount discarded and its cost, scaled to the fraction (or sealed units) actually thrown out. Read the totals as known-cost-plus-unknown-rows, not as one number.
Commands
Entries
scripts/kitchen-axi entries list [--since DATE] [--limit N]— newest-first consumption entries (base macros + portion_multiplier; effective = base × multiplier)scripts/kitchen-axi entries show <ulid>— one entry with full nutrition, source, and statusscripts/kitchen-axi entries log [note…] [--recipe ULID] [--component "label=grams"]… [--at TIME] [--calories N] [--protein N] [--fat N] [--sat-fat N] [--carbs N] [--sugar N] [--added-sugar N] [--fiber N] [--sodium N] [--label T]— log a deliberate, no-model entry (note and/or recipe + component quantities); recipe-referenced entries are computed deterministically; --at sets logged_at (default now — prefer a full local timestamp with offset; a bare YYYY-MM-DD backstops to local noon that day, never midnight UTC); a directly-stated panel (--calories/--protein/…, optionally --label) records a born-manual, terminal entry verbatim with NO estimation (mutually exclusive with --recipe/--component); --sugar is TOTAL sugar (untargeted context) while --added-sugar is the processed/prepared share that carries the ceiling — whole foods are --added-sugar 0, never omittedscripts/kitchen-axi entries patch <ulid> [--note T] [--label T] [--calories N] [--protein N] [--fat N] [--sat-fat N] [--carbs N] [--sugar N] [--added-sugar N] [--fiber N] [--sodium N] [--portion-basis T] [--multiplier M] [--at TIME]— edit an entry: note/label re-queue estimation; any of the NINE macro flags sets a terminal manual override (the same panellogaccepts — every field is correctable in place, so never delete + re-log to fix a number); --multiplier rescales the base post-hoc and --at backdates logged_at (prefer a full local timestamp with offset; a bare YYYY-MM-DD backstops to local noon that day; neither re-queues, neither changes source)scripts/kitchen-axi entries questions [--limit N]— entries whose HUMAN-supplied note nobody has reconciled against the computed panel — a condiment, a splash of oil, an extra the component list never covered. The entries-side twin ofinventory questions, and part of the home view's open-question countscripts/kitchen-axi entries review <ulid>— mark one note looked at. Records that a human READ it, NOT that anything changed — most extras are immaterial and "seen, costs nothing" is the honest outcome. If it DOES move the numbers,patchthem first, then reviewscripts/kitchen-axi entries delete <ulid>— remove an entry from all rollups
Stores
scripts/kitchen-axi stores list— every store string seen, from the lexicon and inventory itemsscripts/kitchen-axi stores merge <from> --into <to>— fold one store spelling into another: re-points its lexicon rows and items, then records the alias so the old string resolves onto the survivor. A store accumulates spellings and the lexicon keys on the string, so mappings under one can never match receipts printing another. NOT REVERSIBLE — sharing a word is not enough, and when unsure leave them apart
Prep worksheets
scripts/kitchen-axi prep publish --slug S --label T [--recipe <recipe-ulid>] [--component <product-ulid>=<g>]… [--component-item <item-ulid>=<g>]… [--component-unit <item-ulid>=<n>]… [--step T]… [--cook eaten|packed] [--units N] [--shelf-life C] [--yields-recipe <recipe-ulid>] [--source <item-ulid>[:amount]]…— build a prep WORKSHEET from the catalog and publish it. Components are named by ULID and resolve to the product's stored per-100g panel, so no reference number is transcribed by hand; a product with no panel is refused rather than guessed at, and a missing field contributes 'unknown' rather than zero. An EATEN sheet decrements the items its components name when submitted, so name stock with --component-item (grams) or --component-unit (whole units for counted stock). --recipe seeds rows from a recipe's lines (which carry their own per-100g inline, so they need no catalog lookup). --cook makes submitting the sheet the write itself (eaten → one entry; packed → one conversion). On a packed sheet, --yields-recipe sets the batch's macro provenance — WITHOUT it the derived item can never be one-tap consumed or named as a component, because its macros live nowhere. Publishing writes NOTHING to the ledger
Daily rollup
scripts/kitchen-axi days [--since <n|date>]— per-owner-local-day rollup: one row per day (nine-field panel + calories + net line when a TDEE base is set), bucketed by the instance's OWNER timezone SERVER-SIDE. --since is a day count (7 / 7d) or a date; default last 7 days. USE THIS for any multi-day or weekly total — never list entries and hand-sum them by timestamp (UTC-vs-local mis-bucketing is the exact footgun this retires; group only by thedayfield)
Expenditure
scripts/kitchen-axi expenditure log "<label>" --kcal N [--duration M] [--avg-hr H] [--at TIME] [--source S] [--ulid U]— record a stated burn (active calories — a device said it or you did; never model-estimated); feeds the daily net line, which is context, not a spend-it budget; --at defaults to now — prefer a full local timestamp with offset, a bare YYYY-MM-DD backstops to local noon that day. STRAVA ACTIVITIES SYNC THEMSELVES (a scheduled server feed pulls the trailing week every ~30 min, idempotently) — never manually log a burn the feed will also import. But the feed only imports an activity with a calorie value; one with none is skipped forever (never retried, never written as 0) — and most manually-entered Strava activities have exactly no calorie value, so THOSE are yours to state here. Checkexpenditure list --include-skippedfor what the feed is sitting onscripts/kitchen-axi expenditure list [--since DATE] [--limit N] [--include-skipped]— recent expenditures, newest first; --include-skipped also lists Strava activities the sync will never import (no calorie value)scripts/kitchen-axi expenditure delete <ulid>— remove an expenditure from all rollups
Weigh-ins
scripts/kitchen-axi weigh-ins log --weight KG [--body-fat PCT] [--at TIME]— record a manual weigh-in (source: manual; --at defaults to now — prefer a full local timestamp, a bare YYYY-MM-DD backstops to local noon that day; a naive time gets this machine's local offset attached because the server refuses to guess a zone)scripts/kitchen-axi weigh-ins list [--since DATE] [--limit N]— raw readings, newest first — every reading is a row (repeats included); noise collapses at read time, never by rewritingscripts/kitchen-axi weight trend [--days N]— derived view (default 30 days): one line per local day with readings (median weight + median body-fat + count, bucketed by each reading's OWN recorded offset) plus a 7-day rolling mean over existing days — no interpolation; context for the owner's judgment, never an auto-tuner
Inventory
scripts/kitchen-axi inventory list [--state S] [--closed] [--limit N]— on-hand items in eat-first (eat_by ascending) order; --state filters, --closed includes finished/tossedscripts/kitchen-axi inventory show <ulid>— one inventory item with derived eat_by / days-until / agescripts/kitchen-axi inventory add [--raw-label T] [--product-ulid U] [--store S] [--acquired-at DATE] [--fraction F] [--units-total N] [--unit-seal individual|shared] [--state S] [--needs-info] [--shelf-life C] [--notes T] [--ulid U]— create an item directly (manual/verbal purchase or seed); --units-total makes it a counted item instead of fraction-modeled, and --unit-seal says what that package seals — shared for one seal over N units (a 4-link pack, a sliced loaf), individual (default) for separately-sealed units (a can 3-pack); a supplied --shelf-life derives an eat_by even with --needs-info; idempotent when --ulid suppliedscripts/kitchen-axi inventory event <ulid> <opened|finished|finished-unit|tossed|moved> [--fraction F] [--to CLASS] [--at DATE]— explicit event; for tossed, --fraction is the AMOUNT TOSSED (partial toss decrements + stays alive, terminal only at zero remainder or when omitted); finished-unit is a counted item's integer one-unit decrement, whose outcome depends on --unit-seal (individual — back to a fresh sealed clock; shared — stays open on the container's clock). 'moved --to ' is the STORAGE MOVE: an item physically changed appliance (freezer to fridge to thaw, fridge to freezer to park a clock), so its clock RESTARTS from the move date on the destination class — state and opened_at are untouched, and --at is the date of the ACT, not of the intention. --at (here and on every inventory verb) is a CALENDAR DAY resolved in the instance's own timezone: omitted it is today where the food is, never today in UTC, so an evening event and the meal entry it depletes agree on the day; a bare YYYY-MM-DD is taken verbatim, so you never compute an offset. The consumption/waste verbs never reach the fifth state, dismissed, which has its own verb ('inventory dismiss')scripts/kitchen-axi inventory recount <ulid> [--fraction F] [--units-remaining N] [--units-total N] [--uncounted] [--unit-seal individual|shared] [--state stocked|open] [--opened-at DATE] [--shelf-life C] [--needs-info|--no-needs-info] [--product-ulid U|--unlink-product] [--notes T]— RECONCILE the ledger to observed reality ("it's actually 75% full", "really 2 of 3 cans", "never opened", "this was always a fridge item", "this is that product") — a correction, NOT a consumption event; never invents a clock, re-derives eat_by (never settable), can reclassify the unit model + seal, corrects the class against the EXISTING anchor (a real storage move is 'event ... moved' instead), clears or re-queues needs-info without a label scan, re-points product_ulid, and --state can resurrect a mis-closed item. If you MEASURED an amount you ate, that is 'inventory eat', never this — a recount carries no consumption claimscripts/kitchen-axi inventory dismiss <ulid> [--non-inventory]— RETIRE a record that was never real stock — a phantom item, or a non-grocery receipt line (housewares). The ONLY terminal that claims neither consumption nor waste, so it never pollutes either telemetry: never close a phantom with 'event finished' (a consumption that didn't happen) or 'event tossed' (waste that didn't happen). --non-inventory also dismisses same-line siblings and teaches future receipts to skip the line. 409 if the item is already terminalscripts/kitchen-axi inventory merge <ulid> --into <ulid>— fold a DUPLICATE item (two records, ONE physical package) into a survivor: fills only the survivor's EMPTY identity fields, relinks its entries/receipt line/conversions, then retires it as dismissed. Quantities are never summed and the survivor keeps its OWN clock — use this, not dismiss, whenever either record has historyscripts/kitchen-axi inventory remark "<free text>" [--at DATE]— free-text event resolver — matches a remark to an item and infers opened/finished/tossed; prints matched/unmatched honestly (unmatched is normal, not an error)scripts/kitchen-axi inventory candidates <ulid> [--limit N]— ranked candidate products for an item whose receipt line never matched exactly, scored on line-text similarity, product-name similarity and price agreement. SUGGESTIONS ONLY — nothing is attached by a score; the only automatic attachment is an exact lexicon hit, which replays a human decision rather than making one. A null price signal means there was nothing to compare, not that the prices disagreescripts/kitchen-axi inventory questions [--limit N]— open needs-info items as one-time questionsscripts/kitchen-axi inventory waste [--since DATE] [--until DATE] [--limit N]— the COSTED toss log — every recorded toss with the amount discarded and what it cost, scaled to the fraction (or sealed units) actually thrown out, never the whole package. Price comes from the item's OWN receipt line when knowable, else the nearest priced purchase of the product (cost_basis says which). A null cost with basis 'unknown' means no price is on file for that product — NOT that the food was free; totals sum only known costs and report the unknown rows separately. Dismissed/merged-away records never appear (retracting the record retracts its waste)scripts/kitchen-axi inventory convert [--from <ulid>[:amount]…] --to '<derived spec json>' [--at DATE]— prep transform: create a NEW derived item with its own clock + provenance (--to takes unit_seal alongside units_total for a counted batch: shared for a tray under one lid, individual for separately-lidded jars), optionally decrementing source item(s) (count or fraction); --from is OPTIONAL — with none it is a source-less "I made this" that decrements nothing. Pass --to recipe_ulid to make the item one-tap consume-eligible. THIS is how prepped food reaches the consume shelf — never plain 'inventory add'scripts/kitchen-axi inventory consume <item-ulid> [--quantity N] [--at DATE] [--ulid ENTRY_ULID]— one-tap: log a consumption entry with the item's EXACT known macros (no model call) and deplete it, in ONE atomic step; qualifies via EITHER of two channels — a recipe-linked derived item, or a counted purchased item whose linked product has a complete nutrition panel + unit_edible_g (neither channel covers a fraction/divisible item — use 'eat' there); else 400; idempotent on --ulidscripts/kitchen-axi inventory eat <item-ulid> [--grams N|--fraction F] [--entry-ulid ENTRY_ULID] [--at DATE]— STATED-WEIGHT CONSUMPTION: a KNOWN weight or fraction eaten off an open DIVISIBLE item — a consumption, never a recount. Fraction-modeled items only (400 on a counted one — use finished-unit/consume there). Exactly one of --grams/--fraction; --grams needs the linked product's net_content_g and is REFUSED (400) without one, never guessed — pass --fraction instead. Reaching/passing zero goes terminal 'finished' (consumed, never tossed); a positive remainder stays open. --entry-ulid links an ALREADY-LOGGED consuming entry atomically with the deplete; the idempotency key is the (entry, item) PAIR, so a retry of the same pair is a safe no-op while the SAME entry against another item is the meal's next component and applies normally
Receipts
scripts/kitchen-axi receipts list [--limit N]— recent purchase batches with parse statusscripts/kitchen-axi receipts show <ulid>— one batch plus its parsed line outcomes (matched/unmatched/pending)scripts/kitchen-axi receipts scan <photo…> [--store S] [--purchased-at DATE] [--ulid U]— post a purchase batch from receipt photos (parses asynchronously); meta is sent as a form field per the module's part-type rule
Recipes
scripts/kitchen-axi recipes list [--q TEXT] [--limit N]— the reselect strip — merged sheet + pushed + promoted recipes plus recent/frequent logged items; --q substring-matches recipe names AND recent labelsscripts/kitchen-axi recipes push '<recipe json>' [--ulid U]— agent-authored template: {"name": "...", "components": [{label, default_qty_g, per_100g:{calories, protein_g, sat_fat_g}}]}. UPSERTS — a correction REPLACES rather than forks: the key is the normalized name (case/spacing-insensitive), or --ulid for one specific record. Prints created vs replaced. A name already held by a promoted or sheet-sourced recipe is a 409 naming it (rename, or pass --ulid deliberately) — never a silent clobber and never a second same-named pill on the stripscripts/kitchen-axi recipes delete <ulid>— ARCHIVE a recipe — off the reselect strip permanently, but still resolvable by ulid, so entries logged from it and prepped items derived from it keep working. Idempotent; 404 for an unknown or sheet-sourced ulid (the meal-bank sheet is never written from here). There is no hard deletescripts/kitchen-axi recipes promote <entry-ulid> --name NAME— create a reusable recipe from a logged entry
Products & lexicon
scripts/kitchen-axi products list [--q TEXT] [--limit N]— products (durable item facts); --q substring-matches name/aliasesscripts/kitchen-axi products add --name NAME [--ulid U] [--shelf-life C] [--aliases a,b] [--package-size S] [--nutrition '<json>'] [--negligible] [--shelf-life-days-unopened N] [--shelf-life-days-opened N]— seed a product — UPSERTS on --ulid (create/replace) or the normalized name (enrich in place)scripts/kitchen-axi products update <ulid> [--name NAME] [--nutrition '<json>'] [--negligible|--no-negligible|--force-negligible] [any add flag]— correct a product in place — partial, only the flags you pass change (the door for adding nutrition later). --negligible is REFUSED for anything salt-bearing (garlic powder qualifies; garlic salt does not) — --force-negligible overridesscripts/kitchen-axi products prices <ulid> [--store S] [--limit N]— PRICE HISTORY — every recorded purchase of one product, newest first, with the printed price, the per-package price (line price ÷ quantity), and a NORMALIZED per-100g/per-100ml unit price plus the basis that produced it (a measure on the line, the store's lexicon package size, the product's label net content, or its package-size string). Compare the per-100 columns, never the raw prices: a 12 oz and a 16 oz package are not comparable at face value. A null unit price means no package size resolved for that purchase — nothing is guessed. Derived at read time from the receipt lines themselves, so nothing is stored and a corrected line corrects the historyscripts/kitchen-axi products merge <ulid> --into <ulid>— fold a duplicate into a survivor: relink its items/lexicon/batch lines, then retire itscripts/kitchen-axi products archive <ulid>— retire a product (soft — still resolvable by ulid so linked items keep working)scripts/kitchen-axi products panel-basis-report— READ-ONLY: products whose stored per-100g disagrees with the value derivable from their own serving basis (nutrition_per_serving ÷ serving_size_g × 100) beyond an 8% + 0.6 per-field tolerance. Never rewrites anything — flagged rows needproducts update <ulid>once you know which number is rightscripts/kitchen-axi lexicon list [--store S] [--limit N]— receipt-line → product mappings per storescripts/kitchen-axi lexicon add --store S --line-text T --product-ulid U [--package-size S] [--shelf-life C]— map a store's receipt line to a product (upserts on store+line; future receipts auto-resolve)
Notes
- Every command is a thin veneer over one documented
/api/kitchen/*endpoint; the CLI adds no semantics the API lacks. 4xx responses surface as a non-zero exit with the server's error message. - Model-backed steps degrade gracefully: with no model key configured, receipts still post
(batch stays
parsing) and label intake returns 503 — the inventory surface stays "roughly right, self-healing". - This skill is a generic toolkit surface — no instance identity. Installation, hooks, and which commands a chat surface exposes are the operator's concern.