flipstream
Flipstream CLI — query your Flipstream data from the terminal. A thin OAuth2 client that authenticates once and prints clean structured JSON for AI coding agents.
Architecture: this supersedes
flipstream-cli-kickoff.md. SeeCLAUDE.mdfor the corrected model (thin client, two hosts, no natural-language "ask").
Install
npm i -g flipstream # requires Node.js 20.19+ or 22.12+
Usage
flipstream --help
flipstream --version
flipstream version --json # {version,node,platform,contractVersion,sdkVersion}
flipstream auth login # opens the browser to sign in (OAuth2 + PKCE)
flipstream whoami # your organization and you, with full IDs
flipstream ws list # list your organization's workspaces
flipstream ds show <id> # one dataset: status, workspace, export
For agents (Claude Code / Codex / MCP)
The CLI is self-describing. Discover the whole surface in one call, then invoke:
flipstream commands --json # full machine-readable catalog: every command's
# id, summary, description, flags, args, aliases
flipstream <command> --help # detail for one command
Output is a deterministic agent contract — stdout = data (JSON via --json,
NDJSON via --ndjson), stderr = diagnostics, and on failure a single
{"error":{"code","message","exit"}} envelope on stdout so
flipstream <cmd> --json 2>/dev/null | jq . works on success and failure.
Branch on error.code + the exit code: every code comes from one closed list
the CLI owns, never from a class name. The full contract (exit codes, that error
taxonomy, per-command shapes, CONTRACT_VERSION) lives in
docs/AGENT-CONTRACT.md and is enforced by a
conformance suite.
Every request also says which client, which agent and which conversation sent
it, for your organisation's query log, and carries a request id that a failure
reports as request_id. An agent the CLI doesn't detect should set
FLIPSTREAM_AGENT. flipstream config set identify off (or DO_NOT_TRACK=1)
leaves out the agent and the conversation.
Development
Built with Bun; the published CLI runs under Node.js 20.19+ or 22.12+.
bun install
bun run build
./bin/run.js --help # run from source (or during dev: bun bin/dev.js --help)
Scripts:
bun run build— compile TypeScript todist/bun run lint— eslint (flat config)bun run check:secrets— static deny-list gate oversrc/lib/auth(CI-enforced)bun run format— prettierbun test— run the test suitebun run smoke— pack the tarball + run the packaged binary (E5-4)bun run dev— run the CLI from source (./bin/dev.js)
Security
The CLI is a public OAuth2 client (Authorization Code + PKCE, a fixed
first-party client_id) — it holds no client secret.
- Tokens live only in the OS keychain (through
@flipstream/sdk's keychain store, service idio.flipstream.cli.tokens, one item per sign-in host); nothing is written to disk in plaintext, and the non-secret config under~/.config/flipstream-clinever contains tokens.FLIPSTREAM_NO_KEYRING=1keeps them in memory for one command instead. - Commands running at once refresh one at a time. Each sign-in host has a lock
file under
~/.config/flipstream-cli/locks/(it holds no token), so two commands never spend the same refresh token, which would sign you out. A command killed while refreshing makes the next one wait up to 10 seconds.auth clear-allleaves only a lock file a running command still holds; it goes when that command finishes. - Updating from 0.9.x signs you out once. Earlier versions kept the sign-in
under
io.flipstream.cli; this one never reads it. Runflipstream auth loginonce;auth logoutandauth clear-alldelete the old item. - Tokens are never printed. Every log/error/
--jsonpath is routed through a redactor that masks JWT-shaped values,Authorization: Bearerheaders, and secret URL/form params.auth loginprints only the decoded (display-only) claims. - Sign-in runs on
@flipstream/sdk's loopback: PKCE S256 only, the redirect'sstateandisschecked before its code is exchanged, and the redirect port bound to127.0.0.1only (RFC 8252), never0.0.0.0/::. Where the browser can't reach that port (SSH, tmux), paste the address it ended on:auth loginoffers that prompt on a terminal. - No command exposes a
--token/--secret/--verifier/--bearerflag. - No telemetry, no phone-home. The CLI collects no analytics and makes no
network calls beyond the OAuth provider and the data host you query. There is no
update-notifier dependency; were one ever added, its notice would go to stderr
only (never stdout — it would corrupt
--json/NDJSON), honorNO_COLOR, and be disableable viaFLIPSTREAM_DISABLE_UPDATE_CHECK.
bun run check:secrets (a CI gate) and test/security/no-secrets.test.ts enforce
these invariants on every change.
Where CI runs
Every job runs on our own MacBook M3 runners (runs-on: [self-hosted, macOS, ARM64]), not on
GitHub's machines: GitHub-hosted minutes are too expensive, and the organisation's Actions budget
ran out on 30 Oct 2026 (Michel, 31 Oct 2026). The IONOS cube runners are not used.
- Exceptions: two jobs in
release.yml, both only when Release Please has just made a release.publishstays onubuntu-latest: npm's Trusted Publishing works only on GitHub-hosted runners.windowsruns the tests, a global install andflipstream --versiononwindows-latest, at the release's tag, so Windows is checked once per release for a few hosted minutes.publishwaits for it, so a Windows failure stops that version reaching npm. By then the tag and the GitHub release exist, so a real Windows bug ships its fix as the next patch. No PR runs Windows.- After a flake in
windows, use Re-run failed jobs on that Release run. It keeps Release Please's outputs, sowindowsand thenpublishrun again at the same tag. Re-run all jobs runs Release Please again, which then makes no release: both jobs are skipped and nothing is staged, and no failure says so.
- Linux runs on the Mac too, in containers pinned by digest:
test (linux)inoven/bun:1.4.3-debian, and thepackagejob's Linux install innode:22.23.2-bookworm-slim. - Four runners serve every repository, on purpose: more made the laptop too heavy. A job waits when all four are busy.
- Each job starts clean: an empty workspace, its own
HOMEandTMPDIRin$RUNNER_TEMP, and the workspace emptied again at the end. - It also keeps git off Michel's login Keychain:
GIT_CONFIG_NOSYSTEM=1,GIT_TERMINAL_PROMPT=0andGCM_INTERACTIVE=never, set before the checkout (os D1358). macOS's system git config setscredential.helper=osxkeychain, which a job's ownHOMEdoesn't keep out. On 31 Oct an os job's git blocked on that Keychain for over 10 minutes, and every git push on the laptop hung behind it. - Where a job ran: its "Set up job" log says
Runner name: 'actions-runner…'.
test/workflows/runners.test.ts holds all of this.
Uninstall / purge
flipstream auth clear-all removes every keychain entry under
io.flipstream.cli.tokens (then best-effort revoking each host's sign-in at the sign-in service),
and 0.9.x's under io.flipstream.cli, and deletes ~/.config/flipstream-cli. Run it before removing the binary —
uninstalling the package/Homebrew/Scoop artifact does not clean these up, so
without it the keychain items and config dir are orphaned.
If you already removed the binary, clean up manually:
- macOS: Keychain Access → search
io.flipstream.cli→ delete the items (bothio.flipstream.cli.tokensand 0.9.x'sio.flipstream.cli). - Linux:
secret-tool clear service io.flipstream.cli.tokensandsecret-tool clear service io.flipstream.cli(libsecret). - Windows: Credential Manager → remove the
io.flipstream.cli.tokensandio.flipstream.clientries. - All:
rm -rf ~/.config/flipstream-cli.
Commands
The full reference — every command, flag, argument and example — is generated from the commands themselves and lives in docs/COMMANDS.md.
For a quick look without leaving the terminal:
flipstream --help # topics and commands
flipstream catalog --help # one command's flags
flipstream commands --json # the whole surface, machine-readable (agents want this)