CLI

Install and authenticate

Install the Zilfu CLI, authenticate it, and see which commands it ships with.

Updated Aug 21, 2026

@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 create without --draft and without --at publishes immediately. Keep --draft in 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.

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

login is the exception: it reads the base URL from --base-url or ZILFU_BASE_URL only, never from the stored config, and it rewrites the config file wholesale. So re-running plain zilfu login after logging into a non-default host silently drops the stored baseUrl and reverts you to https://zilfu.app/api. Pass --base-url on 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.