NemlAI CLI Documentation

Use NemlAI from CLI, API, SDK or AI agents to automate grocery shopping on nemlig.com in Denmark. Search the catalogue, generate smart baskets, and send them to Nemlig — all from code or the terminal.

The NemlAI CLI gives you four ways to interact with the platform:

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

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/:

FilePurpose
tokenSaved auth token (chmod 600)
base_urlCustom server URL
historyCommand 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)

EndpointDescription
GET /cli/api/healthHealth check
GET /cli/api/helpList all commands
POST /cli/api/auth/signupCreate account
POST /cli/api/auth/loginLog in, get token

Authenticated endpoints

Include Authorization: Bearer <token> header.

EndpointDescription
GET /cli/api/whoamiCurrent user info
POST /cli/api/auth/logoutInvalidate token
GET /cli/api/householdHousehold details
POST /cli/api/orders/refreshRefresh order history
GET /cli/api/basket/statusBasket unlock status
POST /cli/api/basket/generateGenerate a smart basket
GET /cli/api/basket/itemsList basket items
POST /cli/api/basket/addAdd item to basket
POST /cli/api/basket/removeRemove item
POST /cli/api/basket/adjustAdjust item quantity
POST /cli/api/basket/sendSend basket to Nemlig
POST /cli/api/basket/searchSearch products
GET /cli/api/settingsShow settings
POST /cli/api/settingsUpdate a setting
GET /cli/api/preferencesShow food preferences
POST /cli/api/preferencesUpdate a preference
POST /cli/api/v2/analytics/spendAggregate spending by product/category over a date range
POST /cli/api/v2/analytics/topTop N products or categories by spend
POST /cli/api/v2/analytics/trendTime-series spend buckets (day/week/month/auto)
POST /cli/api/v2/analytics/drillRaw order-line breakdown for a product or order
POST /cli/api/v2/analytics/receiptsPer-order receipts matching product/category filters (save is client-only and rejected with HTTP 422)
POST /cli/api/v2/analytics/categories-treeCategory hierarchy (main → level → sub) with spend, as JSON
POST /cli/api/v2/analytics/overviewThe 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/achievementsList achievements
GET /cli/api/notifications/statusPush notification status
POST /cli/api/notifications/testSend 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"}'
Tip for AI agents: The generic /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()
Tip: The SDK is a thin client that wraps the HTTP API. It doesn't include any server code, models, or internal logic — just convenient Python methods for calling the API.

Custom base URL

# For local development
client = NemlAI(token="nemlai_...", base_url="http://localhost:8090")

All Commands

Account

CommandDescription
signupCreate a new account
loginLog in and get API token
logoutInvalidate your token
whoamiShow your account info
profile updateUpdate display name or Nemlig email
password changeChange your app password
password reset-nemligRequest a Nemlig password reset
account deleteDelete your account

Basket (Generer kurv)

CommandDescription
basket statusCheck if basket generation is unlocked
basket generate --size <s> [--date <d>]Generate a smart basket
basket itemsList 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 sendSend basket to Nemlig
basket search --query <q>Search products

Orders

CommandDescription
orders refresh --password <p>Refresh your order history
orders refresh-member --email <e> --password <p>Refresh household member's orders

Settings & Preferences

CommandDescription
settings showShow current settings
settings update --key <k> --value <v>Update a setting
preferences showShow 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.

CommandDescription
analytics guideLocal 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
Comma-split caveat: ~1.7% of Danish product names use comma as a decimal separator (e.g. "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

CommandDescription
household infoShow household details + invite code
notifications statusPush notification status
notifications testSend a test notification
achievementsList 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_...")
Security: Tokens are hashed (SHA-256) before storage. The raw token is only shown once at login. Store it securely.

For AI Agents

The CLI is designed to be easy for autonomous agents to use:

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"

NemlAI CLI v1.0 — Open Terminal