Install
openclaw skills install @alex-zwingli/plan-to-eat-cliUse when the user wants to interact with Plan to Eat (plantoeat.com) — meal plan, recipes, planner notes/ingredients/leftovers, freezer, shopping list — and the plan-to-eat CLI is available but the plan-to-eat MCP server is NOT connected. Same capabilities as the MCP server, driven through the shell. Triggers on "what's on my meal plan", "plan X for Wednesday dinner", "add a note to Tuesday breakfast", "move dinner to Friday", "freeze leftovers", "what's in the freezer", "what's on my shopping list", "add milk to the shopping list" — when those must be answered with shell commands.
openclaw skills install @alex-zwingli/plan-to-eat-cliThe plan-to-eat CLI exposes the same 36 capabilities as the plan-to-eat MCP
server, as subcommands. Use this skill when you have a shell but no
plan-to-eat__* tools.
If the MCP tools are available, use them instead — see the plan-to-eat
skill. They avoid a subprocess per call and return structured JSON directly.
plan-to-eat --version
Command not found — the CLI isn't on PATH. Work down this list before giving up:
Run it without installing. The CLI ships in the npm package and needs Node 18+:
npx -y -p plan-to-eat-mcp@0.7.3 plan-to-eat --version # x-release-please-version
Note the -p. The package exposes two bins, and the one named plan-to-eat-mcp — what plain npx plan-to-eat-mcp resolves to — is the MCP server, which will sit and wait on stdio. -p plan-to-eat-mcp plan-to-eat is what selects the CLI. If this works, prefix every command in this skill the same way.
Install it properly. Faster than npx per call, and puts plan-to-eat on PATH. Show the user this rather than running it yourself — it writes outside the working directory:
npm i -g plan-to-eat-mcp@0.7.3 # x-release-please-version
Working from a clone? node <repo>/dist/cli/main.js --version. If dist/ is missing, the build step was skipped — npm install && npm run build in the repo, then retry.
Node missing entirely (node --version fails) — stop and tell the user; don't install a runtime for them.
Keep the version pinned in whichever of these you pass on. npx -y and a bare
npm i -g run whatever the registry serves at that moment, so an unpinned
command picks up every future publish silently. Releases are published from CI
with npm trusted publishing, so each tarball carries a SLSA provenance
attestation binding it to the source repository and the commit it was built
from; npm audit signatures checks the copy that actually got installed.
"Missing PLAN_TO_EAT_USERNAME and/or PLAN_TO_EAT_PASSWORD" — credentials aren't set. They come from the environment or a .env in the working directory:
| Var | Required | Default |
|---|---|---|
PLAN_TO_EAT_USERNAME | yes | — |
PLAN_TO_EAT_PASSWORD | yes | — |
PLAN_TO_EAT_SESSION_FILE | no | ~/.plan-to-eat-session.json |
Ask the user to set them. Never ask them to paste a password into the conversation, and never put credentials in a command you run — no PLAN_TO_EAT_PASSWORD=... plan-to-eat ... one-liners, they land in shell history and in the transcript.
If none of this works, point the user at the project README rather than trying to scrape the web app.
Always pass --json. The default output is a human-readable table meant for
the user's eyes; --json gives you the raw payload to reason over. Show the
user the table form only if they asked to see it.
plan-to-eat <command> [args] --json
Names map from the tool names one-to-one: add_planner_recipe is
add-planner-recipe. Both spellings work.
Discover, don't guess:
plan-to-eat --help # every command, grouped
plan-to-eat add-planner-recipe --help # arguments, which are positional, enum values
Argument forms:
get-recipe 123 = get-recipe --id 123. --help
says which arguments are positional and in what order.--start-date = --start_date.--include_consumed.--event_ids 11 --event_ids 22 = --event_ids '[11,22]'.--ingredients '{"title":"bread","amount":"2"}' (repeat per item).Quote anything with spaces, especially note and ingredient text:
plan-to-eat add-planner-note "Defrost chicken" 2026-05-04 breakfast.
Exit codes: 0 success, 1 for bad arguments, missing credentials, or an
upstream error. Read stderr — argument errors name the offending flag and the
expected type.
Recipe — numeric id. list-recipes --json returns the catalog (~500
summaries); get-recipe <id> --json gives directions, comments, and the full
ingredient list. Recipe IDs are not guessable — look them up first.
Planner event — one calendar entry: { id, date, section, kind, recipe_id?, description?, servings }.
kind is recipe, note, or ingredient. section is breakfast, lunch,
dinner, or snacks. date is YYYY-MM-DD.
Frozen recipe — { id, recipe_id, count, servings, frozen_on }. count is
portions remaining, servings is per-portion size. Consuming is a soft-delete:
the API zeroes count and keeps the row.
Shopping list line — one row of the list, addressed by an item_ids
array, not a scalar id: Plan to Eat merges duplicate ingredients into one line
that keeps every underlying row id. Pass the whole array to update or remove it.
A line also names its store (store_title; store_id is null for the
account's default store) and aisle (grocery_category_title), and recipe_ids
says which planned recipes pulled it in.
A "week" is whatever 7 days the user means. get-planner-week <start> runs
to start + 6 days unless you pass an end date. If the user says "this week"
without a start day, convert relative to today.
Resolving a recipe by name is the usual first step. Filter the catalog rather than eyeballing it:
plan-to-eat list-recipes --json \
| jq -r --arg q 'lasagna' '.[] | select(.title|ascii_downcase|contains($q|ascii_downcase)) | "\(.id)\t\(.title)"'
If jq isn't available, pipe to node -e or read the JSON yourself.
Pass the search term with --arg, never inside the filter. --arg hands
jq the value as data, so a title with regex characters or an apostrophe is
matched literally instead of reparsed. Interpolating it into test("...")
silently matches the wrong thing — searching Chicken (Spicy) as a regex finds
"Chicken Spicy Wings" and misses the recipe you meant — and a title like
Mom's Chili breaks out of the shell quoting entirely.
plan-to-eat get-planner-week 2026-05-04 --json
Recipe events arrive with recipe_title already joined — no second lookup
needed. Group by date, then section, for a readable answer.
plan-to-eat add-planner-recipe <recipe_id> 2026-05-06 dinner --json
The response carries the new event id. If the user named a servings count,
follow with set-planner-servings <event_id> <n>.
plan-to-eat add-planner-note "Defrost chicken" 2026-05-04 breakfast --json
plan-to-eat add-planner-ingredient "2 lbs ground beef" 2026-05-06 dinner --json
Use add-planner-ingredient for grocery-style text tied to a meal. For the real
shopping list, see the shopping list workflows below.
plan-to-eat get-planner-week <week start> --json # find the event id
plan-to-eat move-planner-event <event_id> 2026-05-06 dinner --json
plan-to-eat set-planner-servings <event_id> 4 --json
Recipe events only.
plan-to-eat update-planner-entry-text <id> "X" --json
Note the asymmetry: notes are created with title text but edited through
description. The CLI hides most of this, but it's why the read shape shows
note text under description.
plan-to-eat find-planned-dates <recipe_id> --start-date 2026-05-04 --end-date 2026-05-10 --json
Do this before scheduling if the user is trying to avoid repeats.
plan-to-eat add-leftover-meal <source_event_id> 2026-05-07 dinner --json
One call — it duplicates and moves. Omit the date/section to leave the leftover
in the source slot. For a plain copy, duplicate-planner-event <id>.
plan-to-eat reorder-planner-events --event_ids <pancakes> --event_ids <bacon> --json
All ids must share a date and section.
plan-to-eat freeze-recipe-portions <recipe_id> <event_id> 3 1.0 --json
plan-to-eat list-frozen-recipes --json # confirm, and get the new entry id
event_id is the planner event the portions came from.
plan-to-eat list-frozen-recipes --json
plan-to-eat list-frozen-recipes --include_consumed --json # with history
plan-to-eat delete-frozen-recipe <frozen entry id> --json
Freezer entries carry recipe_id, not titles — get-recipe each one if the
user wants names.
plan-to-eat get-shopping-list
The table is already sorted by store. For a store-by-store readback:
plan-to-eat get-shopping-list --json | jq -r 'group_by(.store_title)[] | "\(.[0].store_title):", (.[] | " \(.amount) \(.unit) \(.title)")'
plan-to-eat add-shopping-list-items --items '[{"title":"Sliced almonds"},{"title":"Tahini","amount":"1","unit":"jar"}]'
Leave category_id and store_id off unless the user named a store or aisle —
Plan to Eat then guesses the aisle and reuses the store last used for that item.
To honor "get it at Trader Joe's", resolve the id first with plan-to-eat list-stores (aisles: plan-to-eat list-grocery-categories).
plan-to-eat get-shopping-list --json \
| jq --arg q 'almond' '.[] | select(.title|ascii_downcase|contains($q|ascii_downcase)) | .item_ids'
plan-to-eat update-shopping-list-items --item_ids '[511232505]' --store_id 138079
plan-to-eat update-shopping-list-items --item_ids '[511232505]' --amount 2 --unit jars
--store_id / --category_id on their own re-file any number of lines at once;
editing text (--title, --amount, --unit, --note) works on one line and
is refused if the ids span two. Only pass what changes. A text edit collapses a
merged line into one row, so read item_ids back off the result.
plan-to-eat remove-shopping-list-items --item_ids '[518771191]'
Soft delete — restore-shopping-list-items --item_ids '[...]' undoes it. Nothing
lists removed lines, so echo the ids back to the user if they might want them.
plan-to-eat delete-planner-event <id> --json
&. Pass them as separate quoted arguments to plan-to-eat, and as --arg
values to jq. This is also why the skill uses contains rather than
test — a literal substring match has no metacharacters to get wrong.section comes back as supper on some accounts. Plan to Eat normalizes
to a per-account preference. Always send dinner; treat supper as the
same slot when reading.description on read, not title —
title is null for notes. Table output already accounts for this.null. The upstream API returns empty
bodies, so the client recovers the new event by diffing the event list before
and after. A concurrent change can defeat that. Re-run get-planner-week to
find the event rather than assuming the write failed.YYYY-MM-DD before it reaches the CLI. The date check only validates shape,
so 2026-13-99 passes argument parsing and fails upstream.add-planner-recipe won't create a recipe. For something not in the book,
create-recipe first, or use a note/ingredient entry for freeform text.find-planned-dates is keyed on recipe_id, not title. To search by
name, filter list-recipes.item_ids, plural — one line can hold several row
ids. Naming any one of them affects the whole line, so precision isn't
required; but a text edit consolidates a merged line into a single row
(keeping the combined quantity), so don't assume every id you sent survives.
Re-read item_ids from the output.store_id: null isn't "no store", it's the account's default store, and
store_title names it. Report the title, not the id.restore-shopping-list-items only works with ids you kept from before.docs/TOOLS.md documents every command's arguments, return shape, and quirks.