forkable-cli
v0.11.0
Published
Unofficial command-line client for Forkable lunch ordering. Built for humans and their agents.
Maintainers
Readme
forkable-cli
An unofficial command-line client for Forkable lunch ordering.
Forkable has no public API - this CLI drives the same private GraphQL API the Member
Console web app uses. Built to be usable both by humans and by AI agents (every
command supports --json).
⚠️ Unofficial and unsupported. It talks to an internal API that can change without notice. Use responsibly and only with your own account.
New here? See QUICKSTART.md for the 4-step setup, or install the Claude Code skill to order lunch by just asking your agent.
Install
npm install -g forkable-cli
forkable init # installs the Claude Code skill + prints setup steps
forkable login # your own Forkable account (run in a real terminal)Requires Node ≥ 20 (uses built-in fetch + cookie handling). After forkable init, restart
Claude Code so it loads the skill, then just talk to it (see QUICKSTART.md).
Working from a clone instead: npm install, then node bin/forkable.js <command>.
Quick start
forkable login # prompts for email + password
forkable week --next # see next week's scheduled meals
forkable menu <deliveryId> # see ranked options for a day
forkable restaurants --next # see which restaurants are on for the week
forkable choose <deliveryId> --best # auto-pick your top match
forkable auto --next # fill the whole week by preference
forkable cancel <deliveryId> # out of office? cancel a day's meal entirelyAuthentication
Login establishes a session cookie that's saved to
~/.config/forkable/session.json (mode 600). Credentials can be supplied three ways:
- Interactive prompt:
forkable login - Env vars:
FORKABLE_EMAILandFORKABLE_PASSWORD(preferred for agents/CI) - Flags exist too, but a
--passwordon the command line is visible topsand lands in shell history - use the prompt or the env vars instead
MFA: forkable login --mfa 123456 if your account requires it.
Each user/agent can keep an isolated session by setting FORKABLE_CONFIG_DIR:
FORKABLE_CONFIG_DIR=~/.forkable-alice forkable whoamiCommands
| Command | What it does |
|---|---|
| init | Install the Claude Code skill + print setup steps |
| login | Authenticate and save the session |
| logout | Clear the saved session |
| whoami | Show the current user + settings |
| week (alias upcoming) | Scheduled meals for a week (--next, --week YYYY-MM-DD) |
| menu <deliveryId> | Ranked menu options for a delivery (--all for full list) |
| restaurants | The week's restaurant lineup per day (--next); --search <name> searches Forkable's whole venue directory; --menu <menuId> shows one venue's full menu |
| choose <deliveryId> | Pick a meal: --best, or --item <id> --menu <id>. Add-ons via --select '{"<modifierId>":[<optionId>]}'; --dry-run to preview the priced configuration |
| auto | Auto-pick the best match for every changeable day (--next, --dry-run) |
| cancel <deliveryId> | Cancel a day's meal so nothing gets delivered (--dry-run to preview); revert restores it |
| prefs show / prefs set <field> <value> | View / edit ordering preferences |
| log <json> / decisions | Append to / read the learning log (suggested → recommended → chosen) |
Add --json before the command for machine-readable output, e.g.
forkable --json week --next. Errors also print as JSON in that mode, and the process
exits non-zero on failure - so agents can branch on both the exit code and ok:false.
Preferences
Stored at ~/.config/forkable/preferences.json. The ranking blends Forkable's own
per-item recommendation score with your local signals:
forkable prefs set likes "chicken,salmon,bowl"
forkable prefs set dislikes "tofu,mushroom"
forkable prefs set avoid "peanut,shellfish" # hard blocks - keyword match, see below
forkable prefs set venueLikes "Thai Villa" # like or dislike whole restaurants by name
forkable prefs set venueDislikes "Kati Shop" # (the "why" goes in a note: forkable prefs add-note)
forkable prefs set diet pescatarian # omnivore|pescatarian|vegetarian|vegan|none
forkable prefs set maxPrice 18 # skip anything pricier (none to disable)
forkable prefs set maxTotal 30 # never place an order over this (none to disable)
forkable prefs set forkableScoreWeight 0.6 # 0..1: trust in Forkable's score vs. your keywordsmaxPrice and maxTotal are different guards and both are useful:
| | maxPrice | maxTotal |
|---|---|---|
| Checks | an item's base price | the real total, base + add-on surcharges |
| When | while ranking candidates | at order time |
| Effect | the item stops being eligible | the order is refused |
maxPrice shapes what gets suggested. maxTotal is a hard ceiling that stops a bad order
landing, which is what makes it safe for an unattended agent to act. A $27 item with a $4
required side is a $31 order, and only maxTotal sees that. Override for one run with
--max-total <n>, or --max-total none to ignore the saved ceiling. A numeric 0 is a
real ceiling of zero dollars, so "spend nothing" means what it says - only the word none
disarms the guard. The ceiling checks the menu total, not your out-of-pocket cost after any
club allowance or copay.
Two honest limits on avoid. It is a case-insensitive keyword match over the item's name,
description and ingredient tags - "peanut" catches "peanut sauce" but a dish described only
as "satay" slips through, so it reduces risk rather than guaranteeing safety. Forkable's own
account-level restrictions still apply server-side, and the CLI checks them before ordering
(the mealRestrictions query). When that check itself fails and you have an avoid list or
Forkable-side restrictions set, the order is refused rather than placed blind.
Decisions you made yourself are protected
Once a decision is logged as human-approved for a day, choose refuses to replace that day's meal
and auto skips it:
$ forkable choose 1236411 --item 52 --menu 16642
error: You already approved "Chicken Shawarma Plate" for 2026-08-12. Refusing to replace it.
Pass --override-approved if you really mean to change it.This exists for unattended runs. An agent reads your preferences but can't tell a meal Forkable
guessed from one you chose, since they look identical in the API. Changed your mind? Pass
--override-approved, or log a newer approved decision for that day.
Decisions default to mode: "approved". An unattended run should log mode: "auto", which carries
no protection, so a scheduled job can't lock itself out. Query with
forkable decisions --mode approved --week 2026-08-10.
Cancelling a day
Out of office, or just don't want lunch that day? cancel empties the slot so nothing gets
delivered and nothing is charged:
forkable cancel 1236409 --dry-run # preview what would be cancelled
forkable cancel 1236409 # remove the day's mealThe removed meal is recorded to the undo log first, so forkable revert puts it back exactly.
One caveat for scheduled agents: a cancelled day looks identical to a never-filled day, so
auto will happily fill it again. Log the cancellation as a human-approved decision for that
day (forkable log '{"day":"...","chose":"cancelled",...}') and auto will skip it.
Undoing a swap or a cancel
Any order that replaces an existing meal - and any cancel - records what it displaced, so it can be put back exactly: same item, same add-ons, same special instructions:
forkable undo-log # swaps and cancels that can still be reverted
forkable revert 1236409 # put one day back
forkable revert 1236409 --dry-run # preview it first
forkable revert --next # revert every day next week that has a recordOnly works while the day is still changeable. Forkable locks a day before delivery and exposes no cutoff timestamp, so revert early rather than counting on a deadline.
Scoring model (see src/prefs.js):
score = w·forkableScore + (1−w)·(keywordScore + venueScore + 0.5·rating), where items failing your
diet / avoid / price constraints are marked ineligible and never auto-selected.
Agent usage example
export FORKABLE_EMAIL=... FORKABLE_PASSWORD=...
forkable login --json >/dev/null
# Order the whole of next week according to saved prefs, capturing the plan:
forkable --json auto --next | jq '.results'How it works
See notes/api-spec.md for the full reverse-engineered API contract.
In short:
- Auth is a cookie-based session (
_easyorder_session, HttpOnly) plus a CSRF token fromGET /api/v2/csrf_tokensent back asX-CSRF-Token, with aForkable-Referrer: mcheader. - Reads/writes go to
POST /api/v2/graphql. Key operations:me,myDeliveries(from:),menus(ids, clubId),mealGenerationScores(...), and three slot mutations:replacePiece(swap a scheduled meal),addPiece(fill an empty slot) andremovePiece(cancel). Login is thecreateSessionmutation.
Layout
bin/forkable.js CLI entry (commander)
src/client.js API client: CSRF, login, cookie jar, GraphQL, mutations
src/queries.js GraphQL operation strings
src/prefs.js preference scoring + modifier-selection builder
src/util.js dates, formatting, menu flattening, change-eligibility
src/config.js session + preferences persistence
src/prompt.js interactive (hidden) prompts
notes/api-spec.md reverse-engineered API documentationTroubleshooting
Not authenticated- runforkable loginagain; sessions expire.- Login fails but credentials are right - the account may use SSO (no password login),
or require MFA (
--mfa). The CLI auto-retries login against the public GraphQL endpoint. This delivery can no longer be changed- you're past the ordering cutoff for that day.
