dailys-cli
v0.2.0
Published
Order lunch from Daily's Robotics without leaving the terminal.
Readme
dailys-cli
Lunch from Daily's Robotics, ordered from the terminal. The binary is dailys.
$ dailys signin order.dailys.inc/sutro/team-lunch --email [email protected] --name "Ana Reyes"
$ dailys menu --week
$ dailys order cabo --date tomorrow --diet "vegetarian, nut-free"
$ dailys my ordersIt speaks dailys.ordering.v1 to api.dailys.inc over ConnectRPC, using the generated types
in packages/proto and nothing hand written, so the CLI, the order page and the MCP server
are three faces of one contract: MenuService for the published calendar, OrdersService
for meals and tracking, AccountsService for the office manager, and TandemService for the
event the CLI shipped for.
dailys mcp serves the same account to an AI agent over the Model Context Protocol, calling
the same client core the commands call.
This is the consumer and office-manager CLI. Kitchen and staff tooling (Yoda) is deliberately out of scope and will arrive as its own binary; nothing in here reaches a staff RPC or asks anyone to sign in with a Daily's account.
Install
Once it is published:
npm install -g dailys-cli
dailys menuFrom this repo, until then:
cd cli
npm install
npm run build
node dist/dailys.js menunpm link inside cli/ puts dailys on your PATH pointing at that build. Running from
source without a build works too: npm run dev -- menu.
Node 20 or newer.
Two doors
An ordering link is the door for everybody. dailys signin <link> --email --name is the
whole of it: the link your office shared is the credential, exactly as it is in a browser,
and the address and name are how the office and the kitchen name you rather than a second
gate. Who may order is two rules and no more (Marco, Aug 7 2026): anyone holding a working
ordering link, and anyone in the office's lunch channel on Slack once the bot is connected.
Two rules, and no third. A link is order.dailys.inc/<office>/<word>; the office's name on
its own is not one of them, and joining with it is refused.
A manager key is the door to manager mode, and dailys login <key> is where it is used.
Daily's mints one per account and shows it once. dailys open hands that session to a
browser without a second sign-in.
Both write the same file and everything below behaves the same afterwards. A signed-out command that needs the wire says which door to use rather than just no.
Two surfaces, one binary
Signed in with an account key, menu and order are your company's ordering platform:
a published calendar, meals on days, an order per day your office manager locks and sends to
the kitchen.
Signed out, they are the Tandem event the CLI shipped for, unchanged: four bowls, one list
per machine, no keys. dailys logout moves back.
Nothing else overlaps. my orders is the platform (the API's list, yours, on every machine
you sign in from); orders is the event list this machine remembers. cancel is the
platform; order remove is the event.
Your account
dailys signin <link> --email <address> --name "<name>"
The consumer door. The link is the one your office shared: the QR on the fridge, the line in
the welcome email, the address in the lunch channel. Paste it whole
(https://order.dailys.inc/sutro/team-lunch) or just its two words (sutro/team-lunch).
A link has a word on the end of it. The office's name alone (sutro) is not a link: it says
which office and invites nobody, an office cannot turn it off when it leaks, and the API
refuses a join that arrives with no link on it. Ask your office manager for the one they hand
out.
$ dailys signin order.dailys.inc/sutro/team-lunch \
--email [email protected] --name "Ana Reyes"
Signed in to Sutro Systems as Ana Reyes.
you [email protected]
link sutro/team-lunch
lunch $15.00 a bowl, you pay $3.00, 11:45 to 12:15
saved /Users/ana/.dailys/key.json
Next dailys menu dailys order <bowl> --date tomorrowBoth flags are required and neither is ever prompted for: this CLI reads nothing from a terminal, which is what lets the same line run under a script, a CI job and Commander. A link the office turned off is refused in the office's own words. Signing in again from the same link is fine and never makes a second you; the office knows you by your address.
Which link you used is remembered, so the office's board can say lunch came in through
team-lunch rather than through web.
dailys login <key> / dailys logout / dailys whoami
The manager door. Daily's opens an account and hands its manager one key; it is shown once
and never again, and everything in manager mode is behind it. It is checked with the API
before it is written to ~/.dailys/key.json, mode 600, and it is never printed back whole.
$ dailys login dk_live_managerkey_c6d9
Signed in to Sutro Systems as a manager.
key dk_liv...c6d9
api https://api.dailys.inc
saved /Users/dana/.dailys/key.json
Next dailys policy show dailys members list dailys day <date>dailys whoami asks the API rather than guessing from the key: a manager key answers with
the account and its policy, a member key answers with neither.
dailys open [--tab <name>] [--print]
Manager sessions only: this terminal's session, handed to a browser. The address carries the key once, the dashboard stores it and takes it straight back out of its own address bar, and no sign-in window stands in the way. Nothing is minted and nothing is rotated.
$ dailys open --print
Open the dashboard here.
https://order.dailys.inc/company/sutro?key=dk_live_managerkey_c6d9
The link signs that browser in once and then takes itself out of the
address bar. Treat it like the key it carries.The address is always printed, so a machine with no browser (SSH, CI) is one paste away. On macOS with a terminal attached the browser is opened as well.
dailys menu [--week] [--from DATE] [--to DATE]
Every published day in the window, closed days included and marked closed. The default window is the next 14 days, the horizon ordering opens for.
$ dailys menu
Menu 2026-08-04 to 2026-08-17
Tuesday, August 4
KYOTO Sweet Chili Shrimp & Coconut Jasmine Bowl
pescatarian, dairy-free
CABO Barbacoa Taco Bowl
gluten-free
Saturday, August 8
closed
Order one dailys order KYOTO --date 2026-08-04dailys order <bowl> --date DATE [--diet TAGS] [--note TEXT]
One bowl, one day. The bowl can be named four ways: the recipe id, the kitchen code, the dish name, or any unambiguous case-insensitive prefix of one of those, resolved against that day's menu. A prefix that matches two bowls is not a guess the CLI will make for you.
$ dailys order cabo --date 2026-08-04 --diet "vegetarian, nut-free" --note "no cilantro"
Ordered.
Barbacoa Taco Bowl (CABO)
Tuesday, August 4
diet vegetarian, nut-free
note no cilantro
$15.00 covered $12.00, you pay $3.00
meal 5c705681-7669-4207-8ff0-583b01bb3f68
order ord_1, open
Track it dailys track 2026-08-04
Cancel it dailys cancel 1Dates are YYYY-MM-DD, or the words today and tomorrow. One bowl per person per day:
cancel and order again to switch. The confirmation names the day in words and says what the
company covers and what you owe, worked out from the account's policy by the server.
Ordering the same bowl for the same day twice is not two bowls. The second one comes back
Already ordered. with the meal that is already there, so a double tap, a retried request or
a bot that lost the answer never costs anybody a second lunch.
dailys usual <bowl> and dailys order usual --date DATE
Save the bowl you always order, once, and the order is one word after that.
$ dailys usual cabo
Your usual is Barbacoa Taco Bowl.
bowl rcp_cabo
Order it dailys order usual --date tomorrowThe bowl is resolved against the next two weeks of menus, so you can type its code or its
name and it is saved as the recipe id the kitchen knows. A usual is not checked against any
one day: menus rotate, and if it is not being served the day you ask for, the API says so and
you pick something else that day. dailys usual none clears it.
dailys my orders [--from DATE] [--to DATE]
Yours, from the API, so it is the same list on every machine you sign in from.
$ dailys my orders
Your meals
#1 Tue Aug 4 Barbacoa Taco Bowl $15.00, you pay $3.00
diet vegetarian, nut-free
note no cilantro
Cancel one dailys cancel 1
Track a day dailys track <date>dailys cancel <#|meal-id>
The number from dailys my orders, the full meal id, or an unambiguous prefix of it. A
locked day cannot be canceled out of, and the API says so in its own words.
dailys track [<date>|<order-id>]
The status of that day's order for your account, and everything that has happened to it. With no argument, today.
$ dailys track 2026-08-04
Tuesday, August 4
order ord_1
status delivered
goes to 5th floor kitchen, left of the sink
09:12 Priya Rao ordered the Barbacoa Taco Bowl.
09:41 Ada Lovelace ordered the Currylicious Tofu & Basmati Rice Bowl.
10:02 Priya Rao canceled the Barbacoa Taco Bowl.
11:00 Locked for the kitchen. 1 bowl going out.
12:14 The van is stuck on the bridge.
12:31 Delivered. 5th floor kitchen, left of the sink.Every line is the server's sentence for that event. Status is one of open, locked,
cooking, delivering, delivered, canceled. The kitchen can add a delayed line with a
note; no new time is ever promised, because a promise that slips is the complaint.
If a bowl came off the menu after you ordered it, the tracker says so above the story and
dailys my orders flags it. Nothing is ever cooked in its place without telling you:
Sweet Chili Shrimp & Coconut Jasmine Bowl came off the menu that day. Pick another bowl.dailys report <#|meal-id|date> --kind KIND [--note TEXT]
One command when something goes wrong: the wrong bowl, a missing one, a dietary restriction
violated, late, cold, or something else. Give the number from dailys my orders (or a meal
id) for one bowl, or a date when nothing arrived at all.
$ dailys report 1 --kind dietary --note "There were peanuts in it."
Reported.
a diet problem on Tuesday, August 4
There were peanuts in it.
report iss_1
Your office manager and Daily's can both see this now. Somebody will
answer it in words, on the day's tracker.
See it dailys track 2026-08-04Kinds are dietary, wrong-item, missing, late, cold, other. No forms, no
photographs of receipts, and dietary reports sort first everywhere they are listed.
dailys receipt <date> and dailys receipt --month / dailys spend
The money, about you. Both verbs answer at the tier of the key that asked: with a manager key they are the office's documents, described further down; with your own key they are yours, and the bottom line is what YOU paid, which is the number an expense form wants.
$ dailys receipt 2026-08-04
Your receipt Tuesday, August 4
Barbacoa Taco Bowl (CABO) $15.00 company $12.00 you $3.00
bowls 1
subtotal $15.00
company $12.00
you paid $3.00
Your month dailys receipt --month
Your meals dailys my orders--month, --year and --from/--to turn either verb into the range document, day by day
with a total on the end of it. dailys receipt --month and dailys spend --month are the
same answer under the two names people reach for.
$ dailys spend --month
Your lunch August 2026
2026-08-01 to 2026-08-31
bowls 9
subtotal $135.00
company $108.00
you paid $27.00
Day by day
Tue Aug 4 1 bowl $15.00 company $12.00 you $3.00
...Every share on it was charged when the office locked the day, so nothing here is a debt: it is money that has already moved. A day you ordered nothing on says so, and an empty range names the days it read.
The office manager
These need a manager key. A member key is turned away by the API, in the API's words.
dailys members list / dailys members add <name> --email ADDRESS [--role ROLE]
$ dailys members add "Priya Rao" --email [email protected]
Added Priya Rao.
role employee
email [email protected]
member mem_3
key dk_live_priyarao_c6d9
That key is shown once and cannot be shown again. Nobody has to be
sent it: Priya Rao orders by opening one of this office's
ordering links, the same as everybody else. Run `dailys links`.
Keep it only if a script or a bot is going to order as them.Only the key's hash is stored, here and on the server, so there is nowhere to read it back
from: send it when you see it, or add the person again to mint another. --json includes the
key, for the one script that has to pipe it somewhere.
Roles are manager, employee and guest; employee is the default.
dailys policy show / dailys policy set <field> <value>
| Field | What it takes |
| --- | --- |
| coverage | the percent of every bowl the company pays, 0 to 100 |
| meal-budget | the most it covers on one meal: 15, 15.50, none |
| week-budget | the most it covers in one week: 60, none |
| horizon | how many days ahead ordering opens: 14 |
| access | qr-name, link-key, password or roster |
$ dailys policy set coverage 80
Updated.
coverage 80% of every bowl
meal budget no cap
week budget no cap
horizon 14 days ahead
access link-keyA value the field cannot take never leaves the machine (exit 2). A value the account cannot take is the server's call, and its refusal is what you see.
dailys day <date>
The delivery schedule: every member's meal for that day, grouped by bowl, with what the day costs.
$ dailys day 2026-08-04
Tuesday, August 4 open
order ord_1
CABO Barbacoa Taco Bowl 2
Priya Rao vegetarian, nut-free note no cilantro
Ada Lovelace
DELHI Currylicious Tofu & Basmati Rice Bowl 1
Zach Kim
3 bowls $45.00 covered $36.00, members pay $9.00dailys lock <date>
Sends the day to the kitchen and closes ordering. One way: after it, nobody on the account
can add or cancel a meal on that day, and dailys track says locked.
dailys delivery-note ["<text>"]
One standing note for the whole account: which floor, which fridge, which door and what the code is. Every day the account orders carries it, and it is repeated on the delivered line of the tracker, so "it says delivered and it is not here" has an answer written down before it is asked.
$ dailys delivery-note "5th floor kitchen, left of the sink"
Updated.
5th floor kitchen, left of the sink
Every day this account orders carries that, and it is repeated on
the delivered line of the tracker.With no argument it prints the note there is now. dailys delivery-note none clears it.
dailys issues [--open] and dailys resolve <#|id> --note TEXT
Everything anybody reported on the account, with what was done about it. Dietary reports sort first, then the newest.
$ dailys issues --open
Reports
#1 Tue Aug 4 dietary open
There were peanuts in it.
iss_1
1 open report
Close one dailys resolve 1 --note "What you did about it."The sentence on resolve is not optional: it goes on the day's tracker, where the person who
reported it reads it. "Resolved" with no words is what a chatbot does.
The Tandem event (signed out)
Unchanged, and still the whole product when no key is signed in.
dailys menu
What is being served: the bowl id, the title the event gave it, the dish, and the description.
$ dailys menu
On the menu
kyoto Thermal Simulation
Sweet Chili Shrimp & Coconut Jasmine Bowl
Broiled shrimp and blanched greens tossed in a glossy sweet chili
glaze over coconut jasmine rice.
...
Order one dailys order kyoto --for "Your Name"dailys order <bowl> --for NAME [--diet TEXT]
Places one order and prints the confirmation, including the order id.
$ dailys order kyoto --for "Priya"
$ dailys order "Thermal Simulation" --for "Priya"
$ dailys order thermal --for "Priya" --diet "Vegetarian, Nut Free"The bowl can be named by its exact id, its exact title, or any unambiguous case-insensitive
prefix of either. hong and struct both find hongkong. A word that matches nothing goes
to the server as typed, and the server answers in its own words.
dailys orders, dailys order edit <#|id>, dailys order remove <#|id>
The orders placed from this machine, numbered. This list is local: it reads
~/.dailys/orders.json and makes no network call. Editing and removing only reach orders in
it, and anything not passed to an edit stays as it was, dietary notes included.
dailys tally
How many of each bowl are ordered so far, everyone's, for the kitchen's running count.
dailys health
Asks GET /health whether the API is up. Exits 0 when it answers and 1 when it does not, so
it works in a shell test:
$ dailys health && echo "we can order"
https://api.dailys.inc is up. (200, 41 ms)Agents
Two ways to hand this account to an AI agent, for the two kinds of host there are.
A host that speaks MCP wants dailys mcp, below: the same capabilities as tools, gated
by the tier of the key.
A host that drives the binary wants the skill. This package ships one, and installs it only when you ask:
$ dailys skill install
Installed the Daily's skill.
/Users/ana/.claude/skills/dailys/SKILL.md
Claude Code reads its skills when a session starts, so restart the
one you are in for this to be there. After that, asking it about
lunch, the menu, or the office's account reaches for this CLI.
Read it dailys skillIt is one page: the two doors and how to tell which somebody needs, what each tier of key can
do, and the laws an agent has to carry (resolve relative dates into explicit --from/--to
because every read defaults forward, never claim emptiness from a window that did not cover
the question, compose commands when no single verb fits, and get the user's yes before adding
--yes to anything that cannot be undone).
dailys skill prints it, and dailys skill path says where the packed copy lives, for a
host that reads skills from somewhere else. Nothing is written anywhere when you npm install
this package: putting files in your home directory as a side effect of installing something
is rude when it works and broken when it does not.
dailys mcp
A Model Context Protocol server on stdin and stdout, so an agent can read the menu, order lunch, cancel, and track a day the way you do at this prompt. It uses the key this machine is signed in with and can do exactly what that key can do.
{
"mcpServers": {
"dailys": {
"command": "dailys",
"args": ["mcp"],
"env": { "DAILYS_KEY": "dk_live_..." }
}
}
}| Tool | Key | What it does |
| --- | --- | --- |
| menu_days | any | the published calendar for a window, closed days marked |
| place_meal | any | order one bowl for one day, or the word usual, resolved against that day |
| cancel_meal | any | cancel one meal by id |
| my_meals | any | the caller's meals in a window, with prices and coverage |
| track_order | any | one day's status, its whole event log and the caller's meals |
| report_issue | any | say something went wrong with a meal or a day |
| day_order | manager | the delivery schedule for a day, with the roster and the totals |
| lock_day | manager | send a day to the kitchen |
| add_member | manager | add a person and mint their key, returned once |
| set_policy | manager | change coverage, budgets, horizon or access mode |
| list_issues | manager | what people reported, dietary first, open or all |
The manager tools are not listed at all for a member key: the server asks the API which tier
it is holding before it answers tools/list, so a model never sees a tool it would only be
refused for. Results are structured JSON, the same objects --json prints. A refusal comes
back as an error result carrying the server's own sentence, which is exactly what an agent
should relay to the person.
stdout carries the protocol, so anything a person would read goes to stderr.
Options
| Option | What it does |
| --- | --- |
| --api <url> | The API to talk to. Beats DAILYS_API. |
| --json | Machine readable output, on every command that prints anything. |
| -h, --help | Help for the CLI or for any one command. |
| --version | The version and nothing else. |
Global options may come before or after the command: dailys --api http://localhost:8790 menu
and dailys menu --api http://localhost:8790 are the same command.
Environment
DAILYS_APIthe API base URL. Defaults tohttps://api.dailys.inc. Point it athttp://localhost:8790to talk to a backend running locally (cd backend && npm run dev).DAILYS_KEYan account key, beating the one on disk and writing nothing to it. This is how the MCP server is usually configured, and how a CI job orders lunch without a login.DAILYS_HOMEwhere the key and the local event order list live. Defaults to~/.dailys.
What is on this machine
~/.dailys/key.json holds the account key, mode 600, in a directory mode 700. Nothing else
about the account is cached: the menu, your meals and the day's order are all read from the
API every time, so two machines never disagree.
{ "version": 1, "key": "dk_live_..." }~/.dailys/orders.json remembers the Tandem event orders placed from this machine. It is
what gives dailys orders its numbers and what lets edit and remove reach an order
without a login: the CLI's version of the localStorage list the web order page keeps.
{
"version": 1,
"orders": [
{
"id": "df30bc1c-d30f-4de0-9478-aa9ac0a56df3",
"name": "Priya",
"bowlId": "cabo",
"diet": "Vegetarian, Nut Free"
}
]
}Nothing secret is in that one, it is safe to delete (you lose the numbers, not the orders), and a mangled file is treated as an empty one rather than as a reason to stop working. The key file is treated the same way, which means a mangled key file reads as signed out.
Exit codes
| Code | When |
| --- | --- |
| 0 | It worked. |
| 1 | The API or the order said no: a bowl that is not on that day's menu, a locked day, a manager's command on a member key, an API that is not answering, or no key on a machine that needs one. |
| 2 | The command line did not make sense: an unknown command or option, a missing --date, a prefix matching two bowls, a policy value the field cannot take. |
Server messages are printed exactly as the server wrote them, because that copy is the
product. The CLI's own messages are prefixed with dailys: so you can tell who is talking.
Development
npm install # also installs packages/proto's dependency
npm run dev -- menu # run from source through tsx
npm test # vitest: both scripted sessions, the units, MCP, the built binary
npm run typecheck
npm run build # bundles to dist/dailys.js, generated proto code included
npm run check # typecheck + testsThe suite runs against a mock Daily's API: a real ConnectRPC server on an ephemeral port
(test/mockApi.ts) implementing all four services from the generated descriptors, with an
in-memory menu calendar, roster, keys, orders and event log, plus /health. It is this
package's own implementation of the contract, not the backend's code, which is the point: two
independent implementations of one proto file, meeting on the wire. It implements the
semantics the contract describes in prose, because those are what the CLI has to be right
about: key tiers, the ordering horizon, one bowl per person per day, locking, and the
coverage math.
Two files are the product as scripted terminal sessions, in order, with exit codes:
test/session.test.ts is a guest at the Tandem event, test/platform.session.test.ts is an
office manager setting an account up and a new hire ordering lunch off it and being locked
out. test/mcp.test.ts drives dailys mcp with a real MCP client over a linked in-memory
transport, and test/built.smoke.test.ts runs the actual bundle as a child process,
including one MCP handshake over real stdio.
No test may touch the internet. test/netGuard.ts replaces fetch with one that throws for
every origin except the mock server a test started, and one of the tests points the CLI at
production to prove the guard fires.
Publishing
npm publish from cli/. prepublishOnly runs the build and files ships dist/ and this
README, nothing else. Bump version in package.json first: dailys --version reads it out
of the bundle, where the build baked it in.
The bundle is self contained. The generated proto code, the Connect transport, the MCP SDK
and zod are all inside dist/dailys.js, and nothing is resolved from node_modules at
runtime, which is what makes npx dailys-cli and a copied-around binary work. The four
dependencies are therefore build inputs that npm still installs for anyone who installs the
package; moving them to devDependencies would make an install dist/ and nothing else, and
is worth doing before the first publish if the MCP SDK's own tree (express, hono, ajv) is not
something we want landing in people's node_modules for no reason.
