Sync runs to the Plune platform
Everything else in the CLI runs fully local: no account, no network, no data leaving your machine.
login, logout and sync are the only three commands that talk to a server, and they are entirely
opt-in — you can use Plune forever without them.
What they add is history. plune run writes its result to .plune/last-run.json and that is where it
stays; plune sync uploads that file to the Plune platform, where runs accumulate into a pass-rate and
cost trend you can look at over time.
Get a token
Section titled “Get a token”-
Sign in to the dashboard — it uses a magic link, so there is no password to manage.
-
Open Settings → API tokens and generate one. The token is shown once; copy it then.
-
Hand it to the CLI (see below). Tokens are per-user and can be revoked from the same screen.
plune login
Section titled “plune login”Saves the token so every later command can use it.
plune login --token plune_xxxxxxxxPassing a secret as an argument leaves it in your shell history, so login also reads the token from
standard input — piped or pasted:
echo "$PLUNE_TOKEN" | plune login # scripted / CIplune login # interactive: paste it, press EnterThe token is written to ~/.config/plune/credentials.json (honouring XDG_CONFIG_HOME) with 0600
permissions inside a 0700 directory. The command prints only the path — the token itself is never
echoed to a terminal or a log:
Logged in. Token saved to /home/you/.config/plune/credentials.jsonAn empty token exits 2 rather than saving an unusable file.
plune logout
Section titled “plune logout”Removes the saved token. It is safe to run when you are not logged in — that is not an error:
plune logout# Logged out. Removed /home/you/.config/plune/credentials.json# …or: Not logged in — nothing to remove.plune sync
Section titled “plune sync”Uploads a run to POST /v1/runs and prints the id the server assigned, plus the URL to read it back:
plune run # produces .plune/last-run.jsonplune sync # uploads itBy default it syncs the most recent run. Point it at a specific file to upload an older or archived one:
plune sync --file reports/nightly-2026-07-27.jsonThe global -c, --config flag works the way it does for plune report: it makes sync look for the run
saved next to that config rather than in the current directory.
plune -c packages/api/plune.yaml syncThe uploaded file is the run exactly as the CLI wrote it — the platform validates it against the same
frozen RunResult contract the CLI produces, so nothing is reshaped on the way out.
Exit codes
Section titled “Exit codes”sync separates “you need to do something” from “the environment failed”, so a CI step can react to the
difference instead of retrying a hopeless upload:
| Code | Meaning | Typical cause |
|---|---|---|
0 | Uploaded | — |
2 | Your input needs fixing | Not logged in, token rejected or revoked, run file missing or invalid |
1 | Environment or server | Network unreachable, API returned 5xx |
Errors are printed as a single readable line; the token never appears in the output, and neither does a stack trace.
There is no environment-variable fallback for the token — sync reads the credentials file and nothing
else. So a CI job logs in first, from a secret, which is exactly what the stdin form is for:
- name: Upload the run to Plune env: PLUNE_TOKEN: ${{ secrets.PLUNE_TOKEN }} run: | npx -y @plune-ai/cli run echo "$PLUNE_TOKEN" | npx -y @plune-ai/cli login npx -y @plune-ai/cli syncProgrammatically
Section titled “Programmatically”The same upload is exported from the package, alongside typed errors — so a caller can tell “log in” apart from “the network is down” without parsing messages:
import { run, sync, NotLoggedInError, SyncNetworkError } from "@plune-ai/cli";
await run({ configPath: "plune.yaml" });
try { const { id, url } = await sync(); console.log(`uploaded ${id} → ${url}`);} catch (err) { if (err instanceof NotLoggedInError) { // ask for a token, then retry } else if (err instanceof SyncNetworkError) { // transient — safe to retry later } else { throw err; }}sync() accepts the same choices the flags expose (file, apiUrl, and injectable fetch/token
loading for tests), and reportSyncFailure(err, write) maps any of the typed errors to the exit code
the CLI itself would use.