@liauto/cli
v0.1.1-b1
Published
Unofficial CLI for LiAuto/LiXiang vehicles
Readme
licli
licli is a CLI for Li Auto (LiXiang) vehicle APIs. It covers auth, real-time status, travel history, control commands, an HTTP API server, and an MCP server.
Run it without installing anything (PyPI via uvx, or npm via npx):
alias licli='uvx liauto'
licli help
# or
alias licli='npx -y @liauto/cli'
licli versionCommands
licli activate # activate the current device (first activation or re-activation)
licli login # device login, prints an OAuth URL, then persists the pasted token JSON
licli vehicles # list cached vehicles
licli status <VIN> # query vehicle state (--json/--raw)
licli travel <VIN> [YYYY-MM] # monthly travel history; with YYYY-MM, that month's detail
licli control <VIN> <action> # lock/unlock/find/wake + ac <on|off|auto|temp|fast-cool|fast-heat> + seat-vent/seat-heat [pos] <1-3|off> + wheel-heat <on|off> + window [pos] <0-99> + trunk <open|close> (action: result <id>)
licli version # print version
licli serve # HTTP API server (default 127.0.0.1:18001; --addr/--token/--allow-control)
licli mcp # MCP server, stdio (default) or --transport http (default 127.0.0.1:18002)Run licli with no arguments (or licli <unknown>) to print the command list.
Global flags: --json (JSON output), -v / --verbose (verbose logging to stderr).
Exit codes: 0 success, 1 user input error, 2 network failure, 3 HTTP non-200, 4 activation failure, 5 config permission/corrupt, 6 missing/invalid/expired activation code, or code bound to a VIN not in this account.
Activation code
Running licli requires a valid activation code, set as an environment variable:
export LICLI_CODE=<activation code>
licli activate # activate the device (onboarding; also done automatically by login)
licli login # establish the account (auto-activates a device if needed)
licli vehicles # list vehicles (also tells you your VIN)activate, login, vehicles, version, and help work without a code.
serve and mcp also start without one: they bind and answer liveness
(/healthz / tools/list) before login, and check the code per request.
Every other command requires a code.
A code is issued by the tool's publisher (offline license-keygen --vin <VIN>)
and is bound to a VIN: it is admitted only when that VIN appears in the
vehicle list of the account you are logged in to. It is an Ed25519-signed token:
the binary only verifies it with the built-in public key and cannot forge codes.
A missing/invalid/expired code, or a code bound to a VIN not in this account,
exits with code 6.
A code may also carry an optional tier (license-keygen --vin <VIN> --tier air|pro),
recorded for reference only. The tier is not enforced: it changes nothing
about which commands run, and a code without one is valid.
HTTP API server (serve)
licli serve runs a foreground HTTP server (default 127.0.0.1:18001, override with --addr). Bind to a loopback address, set --token, or pass --allow-control to enable the control endpoints.
Endpoints:
| Method | Path | Description |
| --- | --- | --- |
| GET | /healthz | Liveness probe (unauthenticated) |
| GET | /vehicles | List vehicles; query limit (default 20), offset, refresh=true |
| GET | /vehicles/{vin}/status | Vehicle state (state, platform, fetched_at, location when available) |
| GET | /vehicles/{vin}/travel | Travel month history (months) |
| GET | /vehicles/{vin}/travel/{ym} | One month's detail; {ym} is YYYY-MM |
| POST | /vehicles/{vin}/control | Send a control command |
| GET | /vehicles/{vin}/control/{requestId} | Query a prior command's result |
| POST | /login | Returns {auth_url, notice} to start an OAuth login |
| POST | /login/submit | Persists {"token_json": "..."} (strict JSON) |
All endpoints except /healthz require Authorization: Bearer <token> when a token is configured. serve starts and answers /healthz before login: an activation code is checked per request, and every route except /healthz, the two /login routes, and the free-tier GET /vehicles needs one (bound to a VIN in the account). GET /vehicles is ungated like the CLI vehicles command, so a buyer can read their VIN before buying a bound code. The two control endpoints return 403 control_disabled unless the control gate is open (loopback bind, a configured token, or --allow-control); the two /login endpoints require the same control gate.
POST /control body (strict JSON, unknown fields rejected):
{ "action": "lock", "temp": 22.5, "pos": "fl", "level": 2, "open": true }action is one of lock, unlock, find, wake, ac_on, ac_off, ac_auto, ac_temp, ac_fast_cool, ac_fast_heat, seat_vent, seat_heat, wheel_heat, window, trunk. The other fields are per-action; unset fields take the action's default:
| Field | Applies to | Values |
| --- | --- | --- |
| temp | ac_temp | number, 16–32 |
| pos | seat_vent | fl | fr |
| pos | seat_heat | fl | fr | rl | rr |
| pos | window | fl | fr | rl | rr | all |
| level | seat_vent, seat_heat | int, 0–3 (0 = off) |
| level | window | int, 0–99 |
| open | trunk | bool |
pos is only valid for seat_vent/seat_heat/window; any other action rejects it. Errors use a unified envelope {"ok": false, "error": {"code", "message", "retryable"}}.
MCP server (mcp)
licli mcp serves the Model Context Protocol over stdio (default) or Streamable HTTP (--transport http, default 127.0.0.1:18002). It exposes eight tools: vehicles, status, travel_months, travel_month, control, control_result, login, login_submit. An activation code is checked per tool call: vehicles is ungated (free tier — read your VIN), login/login_submit are the bootstrap, and the rest require a code bound to a VIN in the account. The two control* tools are enabled by default under stdio; under --transport http they require --allow-control or --token. The two login* tools require the same control gate, and their token transits the MCP client (the model and the host transcript) — see the warning in the tool description.
Register it with Claude Code (no install needed):
claude mcp add licli -- uvx liauto mcpConfiguration
Credentials are stored under the XDG config directory (~/.config/licli by default, or $XDG_CONFIG_HOME/licli) with 0700 directory and 0600 file permissions:
device.json— activated device + key suiteauth.json— API token, ID token, refresh token, expiry
Docker Compose deployment
Run the HTTP API server (serve) and MCP server (mcp) in containers sharing the host config:
services:
licli-api: &licli
build:
dockerfile_inline: |
FROM ghcr.io/astral-sh/uv:python3.13-alpine
RUN uv tool install liauto
RUN ln -sf /usr/local/bin/liauto /bin/licli
ENV PATH="/root/.local/bin:$${PATH}"
ENV XDG_CONFIG_HOME=/config
ENV LICLI_SERVE_ADDR=0.0.0.0:18001
ENV LICLI_MCP_ADDR=0.0.0.0:18002
ENV LICLI_MCP_TRANSPORT=http
ENTRYPOINT ["liauto"]
command: ["serve", "--allow-control"]
ports:
- "18001:18001"
environment:
LICLI_CODE: ${LICLI_CODE:-}
LICLI_SERVE_TOKEN: ${LICLI_SERVE_TOKEN:-}
volumes:
- ${LICLI_CONFIG_DIR:-~/.config/licli}:/config/licli
restart: unless-stopped
licli-mcp:
<<: *licli
command: ["mcp", "--allow-control"]
ports:
- "18002:18002"licli-api shares its build, env, volume and restart policy with licli-mcp via the &licli anchor. Both read the same host credentials (auth.json / device.json under ~/.config/licli, or $LICLI_CONFIG_DIR). The containers bind non-loopback addresses, so --allow-control enables the control endpoints and LICLI_SERVE_TOKEN gates them behind a bearer token.
Log in once before starting the stack — the interactive login writes auth.json / device.json into the mounted config volume, and both services pick them up:
docker compose run --rm -it licli-api login