From log-food
Logs meals to Lose It! from natural language prompts by driving the reverse-engineered `lose-it` CLI: search, describe, dry-run, log, and verify.
How this skill is triggered — by the user, by Claude, or both
Slash command
/log-food:log-foodThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Turns a plain-English food log into Lose It! diary entries by driving the [`lose-it` CLI](https://github.com/phitoduck/lose-it). The CLI is reverse-engineered and the protocol can drift — **always refresh first**.
lose-it from a natural-language promptTurns a plain-English food log into Lose It! diary entries by driving the lose-it CLI. The CLI is reverse-engineered and the protocol can drift — always refresh first.
uv tool install --reinstall git+https://github.com/phitoduck/lose-it
If uv isn't installed: brew install uv (macOS) or pipx install uv. Then:
loseit --help | head -1 # should print "Usage: loseit [OPTIONS] COMMAND [ARGS]..."
If the user has never run loseit login, do that once now (they must already be signed into loseit.com in Chrome or Brave):
loseit login # default --browser chrome; or --browser brave
check-token firstDo not suggest running loseit login reflexively. On macOS, every loseit login causes one (or, with multiple Chrome profiles, several) Keychain authorization dialogs. That's a meaningful friction cost — don't impose it unless you actually need a fresh token.
Always probe first:
loseit -o json check-token
check-token is local-only — it reads ~/.config/loseit/token, decodes the JWT's exp claim, and prints valid / expired / missing / unreadable. It does not open the browser cookie store, so it does not trigger any Keychain prompt. Exits 0 only when status is valid.
Use it as a gate:
status: "valid" → token is good; skip the rest of this section, proceed to STEP 1.status: "expired" / "missing" / "unreadable" → you need a fresh token; continue below.On macOS, every Chrome profile the user has is a separate encrypted cookie store, and loseit login (default) scans all of them — one Keychain prompt per profile. With multiple profiles that's a UX disaster. The --profile <Directory> flag narrows the scan to one profile.
Before asking the user which profile they're signed into loseit.com with, show them the menu. list-profiles reads the filesystem only (cookie store directory names + the browser's plain-JSON Local State for friendly names) — no decryption, no Keychain prompt, fast:
loseit -o json list-profiles --browser chrome
Surface the friendly names ("Eric (Personal)", "Eric (Work)", …) alongside each profile directory ("Default", "Profile 2", …). Ask the user which profile is signed into loseit.com, then re-run login pointed at that one profile:
loseit login --browser chrome --profile "Profile 2"
The --profile value is the directory name from list-profiles, not the friendly name. Even with --profile, the first login from a given profile shows two macOS dialogs (one to use the Chrome Safe Storage item, one to access its key) — that's the OS floor, not a remaining bug.
If the user only has one Chrome profile, you don't have to ask — just call loseit login --browser chrome and move on.
The lose-it repo ships a single-file CLI docset at its README.md. Fetch it once per session — it's the authoritative reference for every subcommand, flag, output format, the full unit alias list, the JSON/TOON schema, and known quirks. Cheaper than guessing.
curl -sL https://raw.githubusercontent.com/phitoduck/lose-it/main/README.md
Or if a clone is already on disk: read it directly from ~/repos/lose-it/README.md. Either way, keep its contents in your working context while you log.
diary-notes.toon — the lessons-learned cacheThe skill maintains a persistent lessons file as a sibling of the CLI config:
~/.config/loseit/config.yaml — the CLI config (written by loseit login)~/.config/loseit/diary-notes.toon — your working memory across sessionsRead it at the start of every run. It's a TOON-formatted array of corrections the user has explicitly made — picks that turned out to be wrong, brands that drift, app-side display quirks, "always use this food_id for X", etc. Treat it as binding guidance: if a note says "for chicken strips, use food_id 4465… not 9aaa… (the latter's per-100g math overcounts in the app)", do that without re-deriving why.
Schema:
notes[N]{food_id,query,brand,note}:
- "<32-char hex>","<search query that found it>","<brand>","<lesson — what to do, what to avoid, why>"
food_id is the stable key when known; fall back to query + brand when the user's correction didn't pin down a specific entry.note is free-form prose — short and actionable. Lead with the rule, follow with a one-line why.Load it:
NOTES="${XDG_CONFIG_HOME:-$HOME/.config}/loseit/diary-notes.toon"
[ -f "$NOTES" ] && cat "$NOTES"
If the file doesn't exist yet, that's fine — start with an empty cache. The first time you record a lesson, create the file with the header row.
diary-notes.toon when corrected mid-sessionIf the user pushes back on a pick — "no, that one's wrong, use the other entry"; "I deleted that, it's the Trader Joe's one, not the Whole Foods one"; "that brand's nutrition is off" — capture the lesson immediately into diary-notes.toon before moving on. The next session will read it back and avoid the same mistake.
Format the new row with the food_id when you have it, plus a brief actionable note. Don't write essays — a single sentence is usually enough. Append, don't rewrite the whole file. If the row's schema header doesn't exist yet, create it.
The point of this file is to make the agent stop relearning the same lessons every session. Treat it as a small, append-only log of the user's preferences and the Lose It! DB's quirks.
Decompose the request into one entry per food. Extract:
| Field | Required? | Examples |
|---|---|---|
| query | yes | "xtreme carb balance tortilla", "avocado", "realgood foods chicken strips" |
| meal | yes (default snacks) | breakfast / lunch / dinner / snacks |
| quantity + unit | yes — see below | 120 g, 1 each, 0.5 cup, 2 tsp, 1 can, 1 container |
| date | optional (default today) | "yesterday" → --date YYYY-MM-DD |
Resolve the meal in this order, without ever asking the user which meal it is:
breakfastlunchdinnersnacks. Do not ask "which meal?" — just log to snacks and move on.The point of this rule is to keep the skill low-friction. The user can correct the meal afterward with loseit delete + re-log if they meant something else; an unprompted snack-default is cheaper than a clarifying question every time.
The CLI accepts these --serving-unit values: tsp, tbsp, cup, piece, each, g, fl_oz, mL, bottle, can, slice, serving, scoop, container, pie. Plus common aliases (cups, grams, tablespoon, floz, milliliter, …).
Heuristics:
--serving-amount 120 --serving-unit g--servings 1 (use the food's native unit)--serving-amount 0.5 --serving-unit cup--serving-amount 1 --serving-unit tsp--serving-amount 1 --serving-unit can--serving-amount 1 --serving-unit container--servings 1fl_oz (volume) or g (weight).ozThe CLI refuses --serving-unit oz because it's ambiguous between weight (~28.35 g) and fluid (~29.57 mL). Ask the user — or default to grams for solids, fl_oz for liquids.
The food DB has many crowd-sourced entries per query, often with subtly different per-serving math. Two operations matter:
search — list candidates (use TOON for compactness)loseit -o toon search "realgood foods chicken strips"
TOON output is ~40-60% fewer tokens than JSON for tabular data. The CLI emits name, brand, category, food_id (the 32-char hex food identifier) by default. Pass -v if you also want the raw pk_bytes array (rarely needed).
describe-food — inspect candidates in one batch (preferred, replaces old probe scripts)Pick the top 3-8 candidate food_id values from search and inspect them in one batched concurrent fetch:
loseit -o toon describe-food <food_id_1> <food_id_2> <food_id_3>
describe-food returns:
primary_serving → {ordinal, unit, native_qty_per_serving} — the food's stored serving size (e.g. unit: "grams", native_qty_per_serving: 1.0 means "1 serving = 1 gram"; unit: "serving", qty: 1.0 means "1 serving = 1 generic serving")cross_class_conversion → {per_serving_g, per_serving_ml} — what the CLI uses to translate between weight and volume units for a foodnutrients_per_serving → labeled dict: {calories, total_fat_g, sat_fat_g, cholesterol_mg, sodium_mg, carb_g, fiber_g, sugar_g, protein_g, serving_weight_g, serving_volume_ml, ...}Pick the right candidate by sanity-checking the labeled values, not by guessing pick indices. Apply these biases in order:
Check diary-notes.toon first. If a prior session recorded a binding lesson for this food (matching query, brand, or a specific food_id), follow it. That entry exists because the user already corrected you on this food before — don't repeat the mistake.
Bias toward the user's requested unit. If the user said "120g of avocado", prefer entries that support gram-based logging — i.e. primary_serving.unit == "grams" OR cross_class_conversion.per_serving_g is populated. Not every avocado entry supports grams: one might only have primary_serving.unit: "cup" with no per_serving_g, in which case --serving-amount 120 --serving-unit g will error. Skim the describe-food output and drop candidates that don't support the asked-for unit before you get to the dry-run. Same logic for mL / fl_oz (look for per_serving_ml or volumetric native units), tsp / tbsp / cup, etc.
Bias toward foods the user has logged before. Run loseit -o toon diary --date <recent> for a few recent days (or grep the user's diary history if it's already in context) and check whether any of your candidate food_ids appear. A prior log is a strong signal the user already approved that entry — re-use it. But don't be blind to it: if the user explicitly names a new brand ("the new Costco chicken strips", "Kirkland greek yogurt — switched from Chobani"), let that override and pick a new entry that matches the new brand. Past usage is a strong prior, not a hard constraint.
Sanity-check calories against common knowledge per the food's native unit (avocado ~160 cal/100g; cooked chicken breast ~165 cal/100g; tomato soup ~80 cal/cup; honey ~20 cal/tsp). If the candidate's per-serving cal is wildly off, skip it.
Prefer entries with a real manufacturer brand (Trader Joe's, Kodiak Cakes, Kirkland Signature, …) over entries whose brand is empty, equals a category name, or equals the user's own username — personal-DB entries can carry buggy per-serving math.
This replaces the old "14-pick Python probe" workflow. Don't write probe scripts; describe-food does it in one call.
loseit log --food-id <hex> -m lunch \
--serving-amount 120 --serving-unit g --dry-run
# 🟡 DRY RUN — would log Lightly Breaded Chicken Strips (id 4465…) → lunch 120 g (143 cal)
The dry-run computes calories using the food's stored per-serving data and the unit/quantity you passed. Compare to a real-label or USDA expectation; if the number is off by more than ~10%, you probably picked the wrong entry. Re-run describe-food to confirm.
Prefer --food-id <hex> to lock onto a specific entry — search result order can drift; food_id is stable.
⚠️
--food-idis mutually exclusive with the positional<query>AND with--pick. Pass exactly one of the three. A common mistake isloseit log "ahi tuna" --food-id 6cad…— the CLI rejects it with❌ --food-id and <query>/--pick are mutually exclusive. Drop the query string when you're using--food-id. Once you've identified a stablefood_idvia search → describe-food, the positional query is just noise.
For non-today logs, pass --date YYYY-MM-DD on log (and delete):
loseit log --food-id <hex> -m snacks --date 2026-06-11 \
--serving-amount 118 --serving-unit g --dry-run
When the answer looks right, run again without --dry-run:
loseit log --food-id <hex> -m lunch --serving-amount 120 --serving-unit g
# ✅ Logged Lightly Breaded Chicken Strips (id 4465…) → lunch 120 g (143 cal)
loseit -o json diary
Each entry includes labeled keys you can read at a glance:
food_name, food_brand, food_measure_unit ("grams", "cup", "serving", "can", …), servings, meal_ordinal (0=breakfast, 1=lunch, 2=dinner, 3=snacks)nutrients_by_label: {calories, total_fat_g, sat_fat_g, protein_g, …} — already named, no ordinal lookup requiredConfirm food_measure_unit matches the unit you logged in (e.g. "grams" for a --serving-unit g log) and nutrients_by_label.calories matches the dry-run number.
For the most efficient readback into your context: loseit -o toon diary | head -50.
Delete by 1-based pick within a meal:
loseit diary # see indices
loseit delete --meal snacks --pick 1 --yes # delete entry #1 from snacks today
loseit delete --meal snacks --pick 5 --date 2026-06-11 --yes # delete from a specific day
delete accepts the same --date YYYY-MM-DD flag as log / diary — use it when cleaning up a day that isn't the server's current "today".
If loseit delete returns HTTP 500, treat it as a parser drift; re-run STEP 0 to make sure you have the latest CLI.
loseit diary (no --date) defaults to the server-side current day, which can lag or lead the user's local calendar by a few hours depending on timezone. Symptom: the user says "I added duplicate entries today" but loseit diary returns count: 0. Don't assume the user is wrong — re-check the day before:
loseit -o toon diary --date $(date -u -v-1d +%Y-%m-%d) # macOS yesterday
loseit -o toon diary --date $(date -u -d 'yesterday' +%Y-%m-%d) # GNU yesterday
If you find the user's "today" entries there, log new entries with --date <that-date> so they land on the day the user actually means.
In order from preferred to last-resort:
loseit describe-food <id> — almost any "what does this food look like?" question is answerable from labeled per-serving data + cross-class conversion fields. Try this first.loseit -o toon diary — token-efficient diary readback with labeled keys. Use when something doesn't match the dry-run.loseit --log-level trace <subcommand> — prints the full GWT request/response bodies. Headers and cookies are suppressed by default (the liauth JWT is a bearer credential), so this is safe to enable. Useful when the parser drops something and you want to inspect the raw wire.loseit --log-level trace --log-headers <subcommand> — opts cookies + headers into the trace. Treat output as sensitive; don't paste sessions into bug reports without scrubbing. The repo's gitleaks config blocks committed JWTs.chrome-mcp-server — if the official Lose It! webapp shows something different from the CLI, drive a browser session via the chrome-mcp tools to capture the network requests the webapp sends and diff against the CLI's payloads. Reserve for last-resort because it requires a Chrome window.If a food consistently logs at the wrong calorie count, capture the food's describe-food output and ask the user to spot-check the daily total in the official Lose It! app — per-serving math on user-edited/personal-DB entries can be subtly broken.
| Symptom | Fix |
|---|---|
loseit: command not found | STEP 0 (uv tool install --reinstall …) |
LoseItAuthError: HTTP 401 | Run loseit check-token first; only re-loseit login if it reports expired/missing/unreadable. On macOS with several Chrome profiles, follow up with loseit list-profiles to surface options, then loseit login --profile <Directory> to avoid a Keychain-prompt storm. |
❌ Missing required setting(s) | loseit login (after check-token says you need to) |
| Dry-run cal is way off from real label | Re-run describe-food on the chosen food_id and verify per-serving cal. Try a different candidate. |
loseit log errors with unit_not_supported | The food's stored unit class doesn't match --serving-unit. Use describe-food to see primary_serving.unit and either match it or fall back to --servings N in the native unit. |
❌ --food-id and <query>/--pick are mutually exclusive | You passed both a positional query and --food-id. Drop the query — --food-id is the entry's stable key and doesn't need a search. |
loseit diary returns count: 0 but the user expects entries | Server-side date may differ from the user's local date by hours. Try --date <yesterday>. |
| Diary shows wrong meal/name/cal | Trust the loseit log success line written at log-time; the diary parser can occasionally mis-render display fields. If still wrong in the official app, capture wire via --log-level trace and inspect. |
loseit delete HTTP 500 | STEP 0, then retry. If still failing, delete via the official Lose It! app or webapp. |
loseit --help
Commands:
login Import the liauth JWT from Chrome or Brave (supports --profile).
check-token Probe the stored JWT for local validity (no Keychain prompt).
list-profiles List browser profiles on disk (no Keychain prompt).
search Search the LoseIt food database.
log Search for a food and log it to a meal.
diary List the diary for a given date (default: today).
describe-food Inspect one or more foods by ID; fetch concurrent.
delete Delete a diary entry by meal + index.
whoami Print the resolved client configuration.
Global flags relevant to this skill:
-o text|json|toon — output format. Prefer toon for any output that will be piped back into context (40-60% fewer tokens than JSON).--log-level trace + --log-headers — wire-level debugging (see Last-resort above).--dry-run (on log / delete) — preview without sending the mutating RPC.Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub phitoduck/lose-it --plugin log-food