Boring by Design: Why CLIs are the Perfect Thing to Build with AI

Severin Neumann

Head of Community

Boring by Design: Why CLIs are the Perfect Thing to Build with AI

When I wrote about why I joined Bronto, one of my two tests was vibe coding a Python-based CLI for Bronto. I told my coding agent to "build a CLI for Bronto", and a few minutes later I could explore the telemetry in my account. I also wrote that this was not the CLI I wanted for Bronto eventually, because I hadn't told the LLM what to do before it started building. I didn't ask it to research CLI design, I didn't specify the language, and I didn't outline how it should be built to stay maintainable.

A few months ago I started from scratch, and today it's bronto-cli: a single Go binary for searching and tailing logs, exploring traces, sending events and managing datasets, monitors and friends. I promised it its own post when I introduced bronto-community in August, but then I realized that an observability vendor announcing its CLI would be the most boring thing I could write about.

So instead I am writing about that boringness being the foundation of my instructions to the agent: CLIs have been around for 60+ years, so LLMs have lots and lots of training data on them.

On the shoulders of giants

The phrase "standing on the shoulders of giants" is so famous that it has its own Wikipedia article, translated into 15 languages. According to the article, the phrase is attributed to Bernard of Chartres, and Newton reused it about five hundred years later and got the credit.

The command line has a lot of giants:

  • Louis Pouzin wrote RUNCOM at MIT around 1963 and came up with the word "shell" (and RUNCOM is why your config files end in rc).

  • Doug McIlroy wanted some ways of coupling programs like garden hose in 1964, and in 1973 Ken Thompson built pipes in one night.

  • Steve Francia pulled Cobra out of Hugo, and since then kubectl, gh, helm and now bronto all feel related.

Here's a non-exhaustive list of things bronto-cli does, and who figured them out first:

bronto-cli does this

who solved it

-d/--dataset, --since 1h

getopt, then GNU getopt_long, POSIX, and pflag

bronto completion zsh

TENEX, bash and zsh completion, then Cobra generating it for free

.bronto.toml found by walking up the tree

RUNCOM's .rc, XDG, and git's upward config discovery

JSONL when streaming into a pipe, --jq

McIlroy's text streams, jq (Stephen Dolan, 2012), gojq

messages on stderr, exit codes 0 to 5

Unix file descriptors and BSD's sysexits.h

bronto api GET /...

gh api

tail --include/--highlight

stern

-x expanded view, bronto repl

psql and its \x

"Did you mean ...?"

git

Homebrew, GoReleaser, signed checksums

Max Howell, Carlos Becker, Sigstore

--no-color and NO_COLOR

Joshua Stein (2017)

Thank you, Pouzin, Thompson, McIlroy, Bourne, Brian Fox and Chet Ramey, Junio Hamano, Steve Francia, Carlos Becker, Stephen Dolan, the four authors of clig.dev, Joshua Stein, and everyone I forgot. All of you made building CLIs a boring enterprise.

Boring is a feature

Embracing boring technology is itself a pretty boring idea by now: Dan McKinley wrote Choose Boring Technology more than ten years ago, and others probably said similar things before him. In his article, McKinley says that with boring technology "their failure modes are well understood". In our case this means that the model understands the failure modes of a CLI well, and we humans know what to expect too, because we have used fifty other CLIs before and have typed -o json | jq more times than we'd like to admit!

When prompting the LLM to build bronto-cli, I leaned into this a lot. Before I allowed it to write any code, the agent had to run multiple research passes: CLI best practices, the CLIs everyone uses, and finally which language to pick. That research into 60 years of computer science and human interface history shaped the design specification: bronto api is gh-style, tail is modelled on stern, and auth profiles work like doctl's contexts.

A CLI is just a view

The next piece of good but boring news is that a CLI whose job is to call an API doesn't have to do any heavy lifting, because all of that happens on the server side. The CLI is mostly a wrapper, and apart from a few Easter eggs I put into it, it doesn't require a lot of innovation. It does auth, flags, rendering, paging and piping, and that's about it. That's why a lot of CLIs look alike: noun-verb commands, list|get|create|update|delete, auth login, JSON on request, and an API escape hatch.

On top of that, the API is well defined through the OpenAPI spec. It says what every resource is called, which fields it has and which verbs it supports, so the list of commands the CLI must provide in the end is predefined by it.

Boring is checkable

Of course the agent made mistakes while building the CLI. So there is a lot of testing: roughly as much test code as product code, including a live integration suite against a real Bronto account that runs on every PR. This makes finding issues so much easier, like when 64-bit sequence numbers got rounded through float64 (...516544 turned into ...516500).

Get started in five commands

While I hope this blog post convinced you that building a CLI is the perfect job for your coding agent, I also hope you are now curious about bronto-cli and want to give it a try! All you need is a Bronto account (which you can get through a 14-day trial or by signing up for one of our plans) and some telemetry in it. Then create an API key under Settings → API Keys and run:

brew install --cask bronto-community/tap/bronto  # for macOS; installers for other platforms are available as well!
bronto auth login        # paste the key; it goes into your OS keychain
bronto ping              # check the connection
bronto datasets list     # see what data you have
bronto search "status >= 500" -d <dataset> --since 1h

It figures out your region by itself. Pipe the last one into jq and you get one JSON object per line. There's also a curl | sh installer, go install, Docker images and .deb/.rpm packages; see the install guide. In CI, BRONTO_API_KEY and BRONTO_REGION are enough.

From here you can slice and dice your data, send telemetry to Bronto, and more. The documentation for bronto-cli is available at https://docs.bronto.io/cli, but here are a few things worth trying:

# Filter without learning the query syntax, and see what it compiles to
bronto search -d prod/checkout-service --eq status=502 --gt duration_ms=1000 --since 1h --show-query

# Cluster a firehose of log lines into patterns
bronto search "status >= 500" -d prod/checkout-service --since 1h --patterns

# A histogram, right in your terminal
bronto search "status >= 500" -d prod/checkout-service --since 1h --histogram --slices 10

# A live dashboard: grouped counts, redrawn in place, with a trend sparkline
bronto tail -g status --window 1m

# An ASCII waterfall for a trace, or the aggregated shape of a service's calls
bronto traces show 4bf92f3577b34da6a3ce929d0e0e4736
bronto traces shape -s checkout --sample 20

# 🥚
bronto rumble

Come build with us

bronto-cli is MIT licensed and lives in bronto-community, so it's a community project: free to use and open to contributions. Questions and bugs go to GitHub issues. If you want to contribute, raise a PR or let me know what you are building in the observability space, I am curious to hear about it.

You can get started with Bronto by signing up today for a 2 week free trial.

Enjoyed this post?

Share it with your team or network.

Free trial

Try Bronto free for 14 days

Centralize your agent and infrastructure telemetry in one platform with sub-second search and 12-month hot retention.

  • 14-day free trial
  • No credit card
  • Set up in minutes