@zilfu/cli is a thin wrapper around the Zilfu REST API — it calls the same endpoints through @zilfu/sdk and authenticates with the same API key as the REST API and the MCP server. One key covers all three.
The CLI reads and writes. It lists spaces, connected accounts, posts, slots and analytics, and it creates and deletes posts, uploads media, and manages your posting schedule. The endpoints it cannot reach — everything else in the API — are still available by driving the REST API directly or pointing an assistant at the MCP server.
A token has no scope picker, so a CLI that can write is a CLI that can publish.
zilfu posts createwithout--draftand without--atpublishes immediately. Keep--draftin the loop until you trust the script.
Install
npm install -g @zilfu/cli
That puts a zilfu binary on your PATH. Node.js 18.17 or newer is required.
For a one-off run, skip the install:
npx @zilfu/cli health
health needs no credentials, so it's the fastest way to confirm the binary works before you log in.
Authenticate
There are three ways in. All of them end with a token written to the same config file, and all of them validate that token against /session before writing it — a bad token is never stored.
Browser consent screen
zilfu login --web
This opens a Zilfu consent screen in your default browser and waits for you to approve. Approval hands a one-time code back to a loopback callback on 127.0.0.1 (PKCE-protected), which the CLI exchanges for the token. The authorize URL is printed before the browser opens, so on a headless machine you can copy it to a browser that can reach the printed port. The CLI gives up after five minutes.
You must already be signed in to Zilfu in that browser. If you aren't, the consent screen bounces you to the sign-in page and signing in lands you on /posts — it does not return you to the consent screen. Sign in first, or re-open the printed authorize URL afterwards.
Paste a token
zilfu login
Copy your API key from Settings → AI agents first — the CLI row on that page copies this whole command with the key already in it — then paste it at the Zilfu API key: prompt. The input is hidden and there are no mask characters at all — not even asterisks — so the terminal looks frozen while you type or paste. It isn't.
Non-interactive
zilfu login --token "$TOKEN"
Use this in CI or any non-TTY shell, where plain zilfu login fails with No token provided. A token passed on the command line ends up in shell history and in process listings — see Scripting for the safer unattended pattern.
Keep the quotes. Every Zilfu token starts with
{id}|, and an unquoted|is a pipe to your shell, not part of the argument.
On success, all three print:
Logged in as Ada Lovelace <ada@example.com>.
Credentials stored at /Users/you/.config/zilfu/config.json
What approving actually grants
The consent screen mints a personal access token with full access to your account — a separate token, not your API key, listed under Additional tokens at Settings → REST APIs and revocable on its own. That separation is the point: revoking a laptop's token leaves everything else signed in, and rotating your API key leaves that laptop alone. There is no scope picker: no read-only CLI token, no single-space CLI token. Tokens do not expire.
Where credentials are stored
| Platform | Path |
|---|---|
| macOS, Linux | $XDG_CONFIG_HOME/zilfu/config.json, defaulting to ~/.config/zilfu/config.json |
| Windows | %APPDATA%\zilfu\config.json |
The file holds a token and an optional baseUrl, and is written with mode 600. macOS uses the XDG path, not ~/Library/Application Support.
A config file that can't be read or isn't valid JSON is silently treated as empty. That surfaces later as No API token found. rather than a parse error, so if a logged-in machine suddenly claims it has no token, check the file itself.
Environment variables
| Variable | Effect |
|---|---|
ZILFU_TOKEN |
The API token to authenticate with |
ZILFU_BASE_URL |
The API base URL to call |
Both are read by every command that calls the API — health, whoami, and the three list commands — and both are overridden by the equivalent flag. login is a partial exception: it honours ZILFU_BASE_URL but ignores ZILFU_TOKEN (pass --token or paste at the prompt). logout reads neither.
Token precedence:
| Order | Source |
|---|---|
| 1 | --token <token> |
| 2 | ZILFU_TOKEN |
| 3 | Stored config file |
| — | No default. Without one of the above, every command that calls the API fails — only health, login, and logout run without a token |
Base-URL precedence:
| Order | Source |
|---|---|
| 1 | --base-url <url> |
| 2 | ZILFU_BASE_URL |
| 3 | Stored config file |
| 4 | https://zilfu.app/api |
loginis the exception: it reads the base URL from--base-urlorZILFU_BASE_URLonly, never from the stored config, and it rewrites the config file wholesale. So re-running plainzilfu loginafter logging into a non-default host silently drops the storedbaseUrland reverts you tohttps://zilfu.app/api. Pass--base-urlon every login to a non-default host.
Log out
zilfu logout
Prints Logged out., or No stored credentials found. if there was nothing to delete. Either way it exits 0.
That command deletes the local config file and nothing else. It does not revoke anything server-side — the token stays valid indefinitely, because Zilfu tokens don't expire — and it leaves ZILFU_TOKEN in your environment untouched. To actually kill it, go to Settings → REST APIs: delete the token under Additional tokens, or Rotate if what the machine holds is your API key.
Commands
Sixteen commands. The five marked write change something; the rest only read.
| Command | What it does |
|---|---|
zilfu login |
Authenticate and store a token |
zilfu logout |
Delete the stored credentials |
zilfu whoami |
Show the authenticated user (id, name, email) |
zilfu health |
Check the API is up — the only API call that works without a token |
zilfu spaces list |
List the spaces you can access, with their ids |
zilfu accounts list --space <id> |
List the connected accounts in a space |
zilfu posts list --space <id> |
List posts in a space, with status, account and date filters |
zilfu posts create --space <id> |
write — Create a post: draft, scheduled, or published now |
zilfu posts delete --space <id> --post <id> |
write — Delete a post |
zilfu slots list --space <id> |
List the recurring posting times |
zilfu slots create --space <id> |
write — Add recurring posting times |
zilfu slots delete --space <id> --slot <id> |
write — Remove a posting time |
zilfu analytics overview --space <id> |
Per-account summary across the space |
zilfu analytics account --space <id> --account <id> |
One account's metrics for a window |
zilfu analytics posts --space <id> --account <id> |
Per-post metrics, sorted and paged |
zilfu media upload --file <path> |
write — Upload a file and print a URL to attach to a post |
That's the whole command surface, plus citty's --help and --version. Every value is passed as a flag — there are no positional arguments; list is a subcommand, not an argument.
Each command is documented in full at Commands, and Scripting covers the machine-readable side.
A sensible first run after logging in:
zilfu whoami
zilfu spaces list
spaces list is the one that matters: --space has no default and no environment variable, so the id it prints is what you pass to every other list command.
Flags, output columns, and the caveats on each command are on Commands. Exit codes, --json shapes, and error handling are on Scripting.