@candledottv/cli
v0.9.0
Published
The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal
Readme
@candledottv/cli
The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup
health from the terminal. Zero runtime dependencies; the whole thing is one self-contained
dist/index.js that runs under plain Node.
Quick start
Install the Candle CLI (macOS or Linux):
curl -fsSL https://candle.tv/install.sh | bash
or with Homebrew:
brew install candledottv/tap/candle
Then: candle setupcandle setup authorizes this device from your browser, shows the agent wallets as funding
destinations, prints the skill and MCP install lines, and runs a full health check. candle auth
login on its own does just the authorization step.
The npm package @candledottv/cli stays published for CI, programmatic use, and Windows until
install.ps1 ships; npx -y @candledottv/cli@latest <command> runs it once without installing.
No-npm fallback
The same CLI can run straight from the public repo:
bunx github:candledottv/agentic candle auth loginThat fetches the agentic repo, resolves the candle bin at its root (packages/cli/dist/index.js,
a committed build), and runs it. If bun's git-dependency handling fails on your machine, clone and
build directly:
git clone https://github.com/candledottv/agentic.git
cd agentic
bun install
bun run --cwd packages/cli build
node packages/cli/dist/index.js auth loginCommands
| Command | What it does |
| --- | --- |
| candle setup [--no-browser] | The onboarding wizard: authorizes this device (skipped when already authorized), prints the agent wallets as funding destinations plus the paste-into-your-agent brief, shows the skill/MCP install lines, runs the full doctor check (setup's exit code is doctor's), and links the web console. Safe to re-run. |
| candle auth login [--profile <name>] [--scopes <a,b,c>] [--label <name>] [--no-browser] | Authorizes this device: prints a code, opens (or prints) an approval URL, polls until approved, then stores the resulting device token and API key. |
| candle auth status | Shows which storage backend is in use, both credential prefixes, the config file path, and a live validity check for each credential. |
| candle auth logout [--keep-key] | Revokes the stored API key (skipped with --keep-key), clears local credentials and config, and prints the portal URL for revoking the device itself. |
| candle keys list | Lists this account's API keys: prefix, scopes, environment, timestamps, and which device minted each one. |
| candle keys create [--scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily\|weekly\|monthly\|never]] | Creates a new API key and prints the plaintext exactly once, with the same optional name, expiration, and USD transaction limit the portal's create form takes. Stored locally only if the CLI does not already hold a working key. |
| candle keys revoke <prefix> | Revokes an API key by prefix. Revoking the CLI's own stored key also clears it locally. |
| candle wallets | Shows the account's embedded (launch) wallets and any linked wallets, using the API key, with a Signer column saying whether this machine holds each linked wallet's signing key. |
| candle profile list | Lists profiles on this machine, with cached accounts. |
| candle profile add <name> --api-url <url> | Creates a profile before authenticating it. |
| candle profile use <name> | Makes a profile the active one. |
| candle profile rename <old> <new> | Renames a profile. |
| candle profile remove <name> --yes | Deletes a profile and its stored credentials. |
| candle mcp [--tools <a,b,c>] [--read-only] [--print-config] | Runs the Candle MCP server (built into this binary) with this CLI's stored API key and API URL in its environment, so an MCP client config is just {"mcpServers": {"candle": {"command": "/Users/you/.local/bin/candle", "args": ["mcp"]}}} -- the absolute path, because GUI hosts launch servers with the app's environment and never see your PATH. Run --print-config to print that block filled in for this install. --read-only starts it with no key and only the four keyless read tools; --tools pins an explicit allowlist. The server is bundled into the binary, so the host needs nothing else installed. |
| candle doctor | Runs a full health check (runtime, backend, credentials, API reachability, credential validity, wallet delegation) as a PASS/FAIL/SKIP table. Exits nonzero on any FAIL. |
| candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>] | Verifies a release asset's Sigstore bundle against the trusted root compiled into this binary. No network, no credentials, and nothing else installed: the bundle carries the certificate and the transparency-log entry. --identity defaults to the release identity for the version in a latest.json sitting beside the bundle; --issuer defaults to GitHub Actions'. Prints verified: <identity> and exits 0, or the reason on stderr and exits 1. |
| candle update [--check] [--to <tag>] | Replaces this binary with the latest signed release. The download is renamed over the running binary only after its SHA-256 matches both SHA256SUMS and latest.json AND its Sigstore bundle verifies in process against that exact version's release workflow. --check reports what is available and installs nothing; --to <tag> pins a release (an older one installs, with a warning). A Homebrew or npm install is left alone, with the command that owns it printed instead. |
Every command accepts these global options:
| Flag | Effect |
| --- | --- |
| --api-url <url> | Overrides the API base URL for this invocation, beating CANDLE_API_URL and the stored config value. |
| --profile <name> | Act as a named profile; see Profiles below. |
| --no-verify-account | Skips the check that the stored key belongs to the profile's account. |
| --json | Machine-readable output instead of a formatted table or summary, generally the underlying API response. One exception: auth login's JSON output still omits the plaintext device token and API key, matching its human-readable summary, since login never displays either value in any mode. |
| --help, -h | Prints usage. |
| --version, -v | Prints the CLI version. |
Verify a release
Every release on https://github.com/candledottv/agentic/releases is built and signed by that
repository's release.yaml workflow, and install.sh and candle update already check this for
you. To check a download by hand, three commands, in increasing strength:
curl -fsSLO https://github.com/candledottv/agentic/releases/download/cli-v0.6.1/SHA256SUMS
curl -fsSLO https://github.com/candledottv/agentic/releases/download/cli-v0.6.1/candle-darwin-arm64
grep candle-darwin-arm64 SHA256SUMS | shasum -a 256 -cgh attestation verify candle-darwin-arm64 --repo candledottv/agentic \
--signer-workflow candledottv/agentic/.github/workflows/release.yamlcurl -fsSLO https://github.com/candledottv/agentic/releases/download/cli-v0.6.1/candle-darwin-arm64.sigstore.json
cosign verify-blob --new-bundle-format --bundle candle-darwin-arm64.sigstore.json \
--certificate-identity-regexp '^https://github.com/candledottv/agentic/\.github/workflows/release\.yaml@refs/tags/cli-v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
candle-darwin-arm64--new-bundle-format (cosign 2.2 or newer) says "expect a Sigstore bundle", which is what releases
from 0.6.1 onward are signed as; without it cosign also accepts an older bundle format of its own,
which candle verify cannot read.
No cosign or gh installed? candle verify <file> --bundle <path> (this CLI's own command, see the
table above) runs the same check against the trusted root compiled into the binary, no network
call required. Full walkthrough, including the installer script's own signature and the
transparency log: Verify a Candle release.
The --json contract
For agents and scripts, --json guarantees: stdout carries exactly one JSON value -- the
result on success, or a failure envelope -- and stderr carries diagnostics only. Exit codes:
0 success, 1 failure (the envelope says why), 2 usage error (the arguments themselves were
wrong; nothing ran).
The failure envelope is stable:
{
"ok": false,
"code": "TIER_REQUIRED",
"status": 403,
"message": "Pro tier required",
"suggestion": "Stake CNDL to reach Pro.",
"docsUrl": "https://docs.candle.tv/developers/agent-access"
}code is always present (the API's error code, an RFC 6749 error for the device flow,
NETWORK_UNREACHABLE when the server was never reached, USAGE for argument errors, or a
local precondition code like NO_DEVICE_TOKEN). suggestion is the fix as a command or
setting when one is known; docsUrl appears when the API names a docs page for this error.
Parse stdout, switch on code, run the suggestion.
Credential storage
Two credentials are stored: a device token (cndl_dvc_..., scoped to key management) and an API
key (cndl_live_... or cndl_test_..., scoped to whatever your device authorized). Neither is ever
written to the config file, logged, or printed, with one exception: keys create shows the
plaintext API key exactly once, at the moment it's issued. auth login never prints either
plaintext value, in any mode (including --json) -- both credentials go straight into storage,
since the whole point of the CLI managing them is that you never have to see or copy them.
The CLI picks the best available backend for your machine, in this order:
- macOS Keychain, via the
securityCLI, when available. - Linux Secret Service, via
secret-tool, when the binary is present and a real store/lookup round trip succeeds (a headless box can have the binary installed with no Secret Service actually running; the CLI checks for that rather than trusting the binary's presence alone). - An encrypted file (
~/.config/candle/credentials.enc, AES-256-GCM, PBKDF2-derived key), everywhere else, Windows included. This is a first-class fallback, not an error: headless Linux agents are exactly where this matters most.
candle auth status and candle doctor both report which backend is active.
Profiles
One machine can hold credentials for several accounts and hosts. auth login creates a profile
implicitly when none is already selected, named from --profile <name> or derived from the API
host (staging, production, or the hostname, de-duplicated with a numeric suffix). Which profile a command acts as, highest
wins: --profile, CANDLE_PROFILE, the activeProfile in config.json, the sole profile. With
several profiles and none selected the CLI refuses and lists them; guessing is how a wallet
import once landed on the wrong account.
Re-running auth login refreshes the profile you are already on, in place: the same name, the
same refs, a new device token and key. Use --profile <new name> to add another instead.
auth logout removes the acting profile's entry and its stored credentials, and clears
activeProfile when it pointed there. candle profile list shows every profile with its cached
account and how old that cache is (no network call); profile use <name> makes one active and
refreshes its account from the API; profile add <name> --api-url <url> creates one before
authenticating it; profile rename and profile remove <name> --yes do what they say. Removing a
profile deletes its two stored credentials and nothing else; imported wallet signers belong to the
wallet, not the profile. candle wallets marks, per linked wallet, whether this machine holds its
signer (stored, none, or stale for a revoked wallet whose signer is still here).
Every authenticated command prints Profile: <name> Account: <account> at <api url> before its
own output (--json output is unchanged except auth status, auth login and doctor, which
carry profile and account; auth status and doctor also carry cachedAccount, the account
the profile recorded, whenever a profile is resolved; scripts get identity from
auth status --json). The account is cached
at login from the API. Where the line is printed from that cache and CANDLE_API_KEY or
CANDLE_DEVICE_TOKEN is overriding the stored credential, it reads
Account: unknown (CANDLE_API_KEY override) rather than naming an account that credential was
never checked against; auth status and setup look the account up live and print what they
get, and auth status and doctor name the account the profile recorded beside it when the two
differ (not under an env credential override, where the live answer is not the profile's key's).
Before an authenticated command acts, the CLI asks the profile's stored key which account it
belongs to and refuses if the answer differs from the account the profile recorded, naming
both and the repairs in order of cost. A key that was legitimately re-issued is repaired with
candle profile use <name>, which re-caches the account; candle auth login --profile <name>
re-authenticates instead; --no-verify-account skips the check for one invocation without
repairing anything. An unreachable API turns the check into a warning, never a failure. The
check is skipped when
CANDLE_API_KEY or CANDLE_DEVICE_TOKEN is overriding the stored credential, when a profile has
no cached account or no stored key, and for the commands that only read the identity or repair it:
auth login, auth status, auth logout, doctor, verify (which acts as no identity at all:
two files and a signature) and the profile commands. setup is guarded, because it skips its
login step whenever credentials are already stored.
A pre-profile install is migrated on first run: profile default is created from the existing
settings and the two credentials are copied to profile:default:* refs. The old refs and fields
are left in place so an older CLI keeps working, until an auth logout clears them along with
the profile they were migrated into.
Environment variables
| Variable | Effect |
| --- | --- |
| CANDLE_DEVICE_TOKEN | Overrides the stored device token for this process. Every command that needs the device token checks this first, before the store. |
| CANDLE_API_KEY | Overrides the stored API key for this process, same precedence as above. |
| CANDLE_API_URL | Overrides the API base URL, beating the stored config value (but not an explicit --api-url flag). |
| CANDLE_PROFILE | Selects the profile when --profile is not given. |
| CANDLE_KEYRING_PASSPHRASE | The passphrase for the encrypted-file backend. Without it, a non-interactive process (no TTY) fails with a clear error rather than falling back to writing plaintext; an interactive session is prompted instead. |
| CANDLE_CONFIG_DIR | Overrides where the CLI keeps its config and encrypted-file credentials (default ~/.config/candle). Mainly a testing seam. |
| CANDLE_ALLOW_INSECURE_HTTP | Allows an http:// API URL pointing at a non-loopback host. The CLI attaches a device token or API key to nearly every request, so it refuses cleartext by default. Loopback (localhost, 127.0.0.0/8, ::1) is always allowed and needs no opt-in; set this only for a trusted local endpoint that is not loopback, such as a devcontainer reaching its host. |
CANDLE_DEVICE_TOKEN and CANDLE_API_KEY together mean CI needs no storage backend at all: set
both and every command works without ever touching a keychain or the encrypted file.
What this CLI deliberately does not do
Launch, trade, and order commands. Executing trades and launches belongs to the SDK and MCP server, not this CLI. This CLI's whole job is credential management plus a handful of read-only or administrative operations; anything that moves an agent's actual workload stays with the packages built to run one.
keys limits. There is no command for setting per-key spend limits, because the API route
that sets them (PUT /keys/:prefix/limits) structurally rejects a device token. It only accepts
an agent key or a live session, since a spend limit is fund-movement authority, and this CLI's
device token is scoped narrowly to key management. Manage limits from the portal.
devices list / devices revoke. The device-token endpoints (GET/DELETE
/device/tokens) are session-only by design: this is the self-renewal guard that keeps a stolen
device token from reading your device metadata (labels, timestamps, revocation state) or revoking
a sibling device. A device token cannot list or revoke devices, including itself, which is why
auth logout can revoke the API key it manages but has to send you to the portal to revoke the
device token. Sibling device prefixes are not themselves secret: they appear in keys list's
"minted by" column, which is attribution and grants no capability. Device management is the
portal's job, not this CLI's.
