The NemlAI CLI gives you four ways to interact with the platform:
- Web Terminal — a browser-based terminal at /cli
- Local Terminal — install via
pip,yay, oraptand runnemlaifrom your own machine - HTTP API — REST endpoints for
curl, scripts, and AI agents - Python SDK — import
nemlaiin your Python scripts for programmatic access
Quick Start
1. Create an account
curl -X POST https://nemlai.milops.org/cli/api/auth/signup \
-H "Content-Type: application/json" \
-d '{
"name": "Simon",
"email": "[email protected]",
"password": "your-password",
"nemlig_email": "[email protected]",
"nemlig_password": "nemlig-password"
}'
2. Log in and get a token
curl -X POST https://nemlai.milops.org/cli/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "your-password"}'
# Response:
STATUS: ok
TOKEN nemlai_abc123...def456
EXPIRES 2026-03-31T00:00:00Z
3. Use the token
# Save your token
export NEMLAI_TOKEN="nemlai_abc123...def456"
# Use it with any endpoint
curl https://nemlai.milops.org/cli/api/whoami \
-H "Authorization: Bearer $NEMLAI_TOKEN"
Web Terminal
Open /cli in your browser. No installation needed. You get a full terminal with command history (up/down arrows), tab completion, and Ctrl+C to cancel.
The web terminal uses a WebSocket connection, so commands execute instantly with live feedback.
Local Terminal
Install the nemlai package and get the same terminal experience
running on your own machine. All logic stays on the server — the local
CLI is just a thin client that sends commands over HTTP.
Install
# PyPI (all platforms)
pip install nemlai
# Arch Linux (AUR)
yay -S nemlai
# Ubuntu / Debian
sudo add-apt-repository ppa:milops/nemlai
sudo apt install nemlai
Run
# Start the interactive terminal
nemlai
# Or run a single command
nemlai help
nemlai whoami
nemlai basket items
Features
- Same commands as the web terminal
- Tab completion and command history (persisted across sessions)
- Secure login — password input is hidden, token saved to
~/.config/nemlai/token - Colour-coded output (green for success, red for errors)
- Works with
NEMLAI_TOKENenvironment variable for scripts - Single-command mode for piping:
nemlai basket items | grep milk
Configuration
# Point to a different server
nemlai --url http://localhost:8090
# Use a specific token
nemlai --token nemlai_abc123...
# Or use environment variables
export NEMLAI_TOKEN="nemlai_abc123..."
export NEMLAI_URL="http://localhost:8090"
nemlai
Config is stored in ~/.config/nemlai/:
| File | Purpose |
|---|---|
token | Saved auth token (chmod 600) |
base_url | Custom server URL |
history | Command history |
HTTP API
All endpoints live under /cli/api/. Responses are plain text
with a consistent format:
STATUS: ok
<response body>
or on error:
STATUS: error
ERROR <message>
HINT <suggestion>
Public endpoints (no auth required)
| Endpoint | Description |
|---|---|
GET /cli/api/health | Health check |
GET /cli/api/help | List all commands |
POST /cli/api/auth/signup | Create account |
POST /cli/api/auth/login | Log in, get token |
Authenticated endpoints
Include Authorization: Bearer <token> header.
| Endpoint | Description |
|---|---|
GET /cli/api/whoami | Current user info |
POST /cli/api/auth/logout | Invalidate token |
GET /cli/api/household | Household details |
POST /cli/api/orders/refresh | Refresh order history |
GET /cli/api/basket/status | Basket unlock status |
POST /cli/api/basket/generate | Generate a smart basket |
GET /cli/api/basket/items | List basket items |
POST /cli/api/basket/add | Add item to basket |
POST /cli/api/basket/remove | Remove item |
POST /cli/api/basket/adjust | Adjust item quantity |
POST /cli/api/basket/send | Send basket to Nemlig |
POST /cli/api/basket/search | Search products |
GET /cli/api/settings | Show settings |
POST /cli/api/settings | Update a setting |
GET /cli/api/preferences | Show food preferences |
POST /cli/api/preferences | Update a preference |
POST /cli/api/v2/analytics/spend | Aggregate spending by product/category over a date range |
POST /cli/api/v2/analytics/top | Top N products or categories by spend |
POST /cli/api/v2/analytics/trend | Time-series spend buckets (day/week/month/auto) |
POST /cli/api/v2/analytics/drill | Raw order-line breakdown for a product or order |
POST /cli/api/v2/analytics/receipts | Per-order receipts matching product/category filters (save is client-only and rejected with HTTP 422) |
POST /cli/api/v2/analytics/categories-tree | Category hierarchy (main → level → sub) with spend, as JSON |
POST /cli/api/v2/analytics/overview | The dashboard's Overblik tab in one call: spend KPIs vs. the previous period, top main categories, trend and newest orders (JSON only, no filters) |
GET /cli/api/achievements | List achievements |
GET /cli/api/notifications/status | Push notification status |
POST /cli/api/notifications/test | Send test notification |
Generic command endpoint
You can also send any command as text:
curl -X POST https://nemlai.milops.org/cli/api/command \
-H "Authorization: Bearer $NEMLAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "basket generate --size medium --date 2026-03-15"}'
/cli/api/command
endpoint accepts any command as plain text. This is the simplest integration
path — just send commands the same way you would type them in the terminal.
Python SDK
Install
pip install nemlai
Usage
from nemlai import NemlAI
# Log in (returns a client with token)
client = NemlAI.login("[email protected]", "password")
# Or use an existing token
client = NemlAI(token="nemlai_abc123...")
# Account
me = client.whoami()
print(me) # {'user': 'Simon', 'email': '...', ...}
# Generate a basket
result = client.basket.generate(size="medium", date="2026-03-15")
print(result)
# List basket items
items = client.basket.items()
# Search and add a product
results = client.basket.search("oat milk")
client.basket.add(query="oat milk")
# Remove or adjust items
client.basket.remove(item=3)
client.basket.adjust(item=7, quantity=2)
# Send basket to Nemlig
url = client.basket.send()
print(url) # https://nemlig.com/basket?skus=...
# Orders
client.orders.refresh(password="nemlig-password")
# Settings and preferences
client.settings.show()
client.preferences.show()
# Household
client.household.info()
Custom base URL
# For local development
client = NemlAI(token="nemlai_...", base_url="http://localhost:8090")
All Commands
Account
| Command | Description |
|---|---|
signup | Create a new account |
login | Log in and get API token |
logout | Invalidate your token |
whoami | Show your account info |
profile update | Update display name or Nemlig email |
password change | Change your app password |
password reset-nemlig | Request a Nemlig password reset |
account delete | Delete your account |
Basket (Generer kurv)
| Command | Description |
|---|---|
basket status | Check if basket generation is unlocked |
basket generate --size <s> [--date <d>] | Generate a smart basket |
basket items | List items in current basket |
basket add --query <q> | Search and add item |
basket remove --item <#> | Remove item by number |
basket adjust --item <#> --quantity <n> | Set item quantity |
basket send | Send basket to Nemlig |
basket search --query <q> | Search products |
Orders
| Command | Description |
|---|---|
orders refresh --password <p> | Refresh your order history |
orders refresh-member --email <e> --password <p> | Refresh household member's orders |
Settings & Preferences
| Command | Description |
|---|---|
settings show | Show current settings |
settings update --key <k> --value <v> | Update a setting |
preferences show | Show food preferences |
preferences update --key <k> --value <v> | Update a preference |
Analytics
Ask questions about your own spending. Filters accept either repeated flags or a comma-separated string, so multi-product or multi-category reports take one call instead of N.
| Command | Description |
|---|---|
analytics guide | Local cheat sheet — date grammar, scopes, formats, multi-value syntax |
analytics overview [--last <expr> | --from <date> --to <date>] [--compare none] [--bucket day|week|month|auto] [--top-limit <n>] [--recent-orders <n>] [--shop <shop>]... [--hide-excluded] [--scope household|me] [--format json] |
The web dashboard's Overblik tab in one request: total, saved amount, order and product count compared with the previous period (on by default; --compare none turns it off), the top --top-limit (6) main categories, the spend trend and the newest --recent-orders (4) orders. Takes no product/category filters. |
analytics spend [--product <name>]... [--category <name>]... [--sub-category <name>]... [--exclude <name>]... [--exclude-sku <sku>]... [--last <expr> | --from <date> --to <date>] [--compare previous-period] [--limit <n>] [--shop <shop>]... [--hide-excluded] [--scope household|me] [--format table|json|csv|markdown] |
Aggregate spend over a date range, one row per canonical product (rotated product numbers are grouped; the table lists each variant with ↳). --product, --category and --sub-category each accept multiple values and can be combined (a line must match all of them). Use --exclude (fuzzy name) or --exclude-sku (exact product number) to filter out noise. --limit caps the rows; totals still cover every match. |
analytics top <products|categories> [--level main|category_level|sub] [--sub-categories] [--limit <n>] [--compare previous-period] [--product <name>]... [--category <name>]... [--last <expr> | --from <date> --to <date>] [--shop <shop>]... [--hide-excluded] [--scope household|me] [--format ...] | Top N (default 10) products or categories, ranked by spend. Honours every spend filter. --level picks the category depth (--sub-categories = sub). |
analytics trend [--product <name>]... [--category <name>]... --bucket <day|week|month|auto> [--last <expr> | --from <date> --to <date>] [--shop <shop>]... [--hide-excluded] [--scope household|me] [--format ...] | Spend over time as buckets, with a bar per bucket in table mode. week needs a range of at least 14 days and month at least 60; auto picks the finest bucket the range supports, and a too-coarse bucket is downgraded automatically (the table notes it; JSON has granularity, requested_granularity and bucket_adjusted). |
analytics drill [--product <name>]... [--category <name>]... [--order <number|id:N>] [--last <expr> | --from <date> --to <date>] [--limit <n>] [--shop <shop>]... [--hide-excluded] [--scope household|me] [--format ...] | Raw order lines for a product, category, or one order, newest first, with the amount saved per line. --order is Nemlig's order number as text (0042 keeps its zeros) or id:N for the internal id. --limit (500) caps the listing; the table then says showing the newest N of M. |
analytics receipts [--product <name>]... [--category <name>]... [--order <number|id:N>] [--detail compact|full] [--names-only] [--limit <n>] [--last <expr> | --from <date> --to <date>] [--save [path]] [--shop <shop>]... [--hide-excluded] [--scope household|me] [--format ...] | Per-order receipts for matching purchases, newest order first. --detail full adds each order's grand total; --names-only lists the product names each --product term matches; --save writes the output to a local file (SDK only). Totals cover every match even when --limit cuts the listing. |
analytics categories-tree [--last <expr> | --from <date> --to <date>] [--scope household|me] [--shop <name>] [--hide-excluded] [--format json] | Category hierarchy (main → level → sub) with spend, as an indented table (or JSON). Use it to find names for --category-main, --category-level and --category-sub. Uncategorised purchases appear as (ukendt). |
Multi-value examples — both forms are equivalent and combine in one round-trip:
# Repeated flags (kubectl / gh / git convention; shell-safe for any name)
nemlai analytics spend --last 12m \
--product vandmelon --product jordbær --product hindbær --product gifler
# Comma-separated (ergonomic shortcut)
nemlai analytics spend --last 12m --product "vandmelon,jordbær,hindbær,gifler"
# Categories work the same way
nemlai analytics spend --last 3m --category "Grønt,Frost"
nemlai analytics trend --category Mejeri --category Drikke --bucket month --last 12m
analytics spend --product "jordbær,hindbær" --exclude "Yoghurt,Hindbærtærte" --last 12m— exclude fuzzy-matched substring noiseanalytics spend --exclude-category "Kød & fisk" --last 30d— standalone category exclusionanalytics spend --product hindbær --exclude-sku 5070590 --last 12m— exact product-number exclusion, no fuzzy matchinganalytics spend --last since-first-order --shop nemlig— full history, one shop only (--shopacceptsnemlig,bilka,foetex; repeat for several)analytics spend --last ytd --hide-excluded— leave out orders/lines you excluded in the web dashboardanalytics trend --last 7d --bucket auto— let the CLI pick the bucket sizeanalytics top products --last 12m --compare previous-period— compare with the equally long period beforeanalytics overview --from 2026-01-01 --to 2026-06-30 --bucket month— the Overblik tab for a custom rangeanalytics top categories --level category_level --category-main Mejeri --last 12m— rank one level of the hierarchy within a filteranalytics drill --order id:42— one order by its internal id
"Letmælk 1,5%"). For search terms that may contain commas, use repeated flags
rather than the comma-string shortcut. Category names are comma-free, so --category
is always safe with either form.
Date range grammar — --last 7d, --last 30d, --last 3m, --last 12m, --last 2y, this-month, last-month, ytd, this-year, last-year, since-first-order (your whole history; exempt from the 5-year cap, no --compare), or --from 2026-01-01 --to 2026-03-31. With no range, commands default to the last 30 days; the maximum range is 5 years. All terms must be in Danish (Nemlig stores names in Danish: mælk not milk).
Comparisons & savings — --compare previous-period (or a bare --compare) on spend and top adds the equally long period before, with the change in kr and %; JSON has comparison.previous_total, delta_absolute and delta_pct. Campaign discounts are always reported as a positive saved amount (total_discount, per-line discount_amount). Periods are shown as local Copenhagen dates (date_range.start/end).
Scope & format — --scope household (default) covers every order in your household, --scope me only your own. --format table|json|csv|markdown controls the output: the SDK draws the table itself from the server's JSON (canonical variants, comparison change, saved amount, truncation and bucket notes); json, csv and markdown come straight from the server. Errors return HTTP 4xx/5xx with a STATUS: error / ERROR / HINT body. Category names can also be filtered through the catalog hierarchy with --category-main, --category-level and --category-sub (plus --exclude-category-main/-level/-sub). The same data is available in the web dashboard at /analytics.
Python — every command is a typed method: NemlAI(token=...).analytics.overview(last="3m"), .spend(product=["mælk"], category="Mejeri", compare="previous-period"), .top("categories", level="sub"), .trend("month"), .drill(order="0042"), .receipts(...), .categories_tree(). Errors raise nemlai.AnalyticsError with message, hint and status_code.
Other
| Command | Description |
|---|---|
household info | Show household details + invite code |
notifications status | Push notification status |
notifications test | Send a test notification |
achievements | List your achievements |
Authentication
The CLI uses token-based authentication. Tokens are long-lived (30 days by default) and can be revoked with logout.
For the Web Terminal
Just type login --email [email protected] --password yourpass. The token is stored in your WebSocket session automatically.
For the HTTP API
# Get a token
TOKEN=$(curl -s -X POST https://nemlai.milops.org/cli/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "pass"}' \
| grep TOKEN | awk '{print $2}')
# Use it
curl https://nemlai.milops.org/cli/api/whoami \
-H "Authorization: Bearer $TOKEN"
For the Python SDK
# Login returns a client with token
client = NemlAI.login("email", "password")
# Or reuse a saved token
client = NemlAI(token="nemlai_...")
For AI Agents
The CLI is designed to be easy for autonomous agents to use:
- Every response starts with
STATUS: okorSTATUS: error— easy to parse - Error messages include
HINTlines with the exact command to fix the issue - The generic
/cli/api/commandendpoint accepts free-text commands - No HTML, no JSON — just plain text that's readable and parseable
- The Python SDK provides typed methods if you prefer structured access
Recommended agent flow
# 1. Check health
curl https://nemlai.milops.org/cli/api/health
# 2. Log in once, save the token
curl -X POST https://nemlai.milops.org/cli/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "...", "password": "..."}'
# 3. Generate a basket
curl -X POST https://nemlai.milops.org/cli/api/basket/generate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"size": "medium", "date": "2026-03-15"}'
# 4. Review and adjust items
curl https://nemlai.milops.org/cli/api/basket/items \
-H "Authorization: Bearer $TOKEN"
# 5. Send to Nemlig
curl -X POST https://nemlai.milops.org/cli/api/basket/send \
-H "Authorization: Bearer $TOKEN"