Skip to main content
C carlos.enredando.me CTO · Advisor · Builder

The CLI Wasn't Built for Me. It Was Built for the AI.

·1936 words·10 mins
Carlos Prados
Author
Carlos Prados
Telecommunications Engineer, Entrepreneur, CTO & CIO, Team Leader & Manager, IoT-M2M-Big Data Consultant, Pre-sales Engineer, Product-Service Manager & Strategist.

The Tool I Built for a Reader Who Isn’t Human
#

For thirty years we have designed command-line tools around one assumption: a human is typing them. Short flags so your fingers don’t tire. Cryptic mnemonics you memorise once and keep for a decade. Terse output because a human reads three lines and stops. ls, git, kubectl — every one of them is an exercise in human ergonomics.

Then I spent a weekend building a CLI for our IoT platform and caught myself making the opposite decision at almost every turn. I wasn’t optimising for my fingers. I was optimising for a reader that never gets tired of verbosity, never memorises anything, has no muscle memory to respect — and, crucially, can be told how to use the tool right before it uses it.

I was building for an LLM.

That reframing is small but it changes everything downstream, and I think it marks the start of a genuinely new category of software: CLIs whose primary operator is an AI agent, not a person. This is the story of one of them — og-cli, the unofficial command-line for OpenGate, the industrial IoT platform we build at Amplía Soluciones — and what it taught me about turning a REST API into something an agent can actually operate.

A Different Design Center
#

A human-first CLI and an AI-first CLI pull in opposite directions on nearly every axis:

DecisionBuilt for a humanBuilt for an AI
FlagsShort, memorable (-w)Self-describing, discoverable
OutputPretty tables, colourStructured, parseable on demand (-o json)
Errors“command not found”What went wrong and what to do instead
Discovery--help, then the manualThe tool ships its own usage guide
InputsWhatever you can rememberForgiving; the model’s first guess should work

The last two rows are where it gets interesting, and they are exactly what the Model Context Protocol (MCP) was built to standardise. MCP is not “function calling with extra steps.” It gives a server three distinct primitives, and using all three is the difference between a toy and a tool an agent can drive reliably:

  • Tools — the actions the model can execute.
  • Resources — data the model can read on demand to discover state.
  • Prompts — instructions the server hands the model so it knows how to use the tools correctly before it tries.

Most MCP integrations I see in the wild ship only tools and call it done. That is the equivalent of dropping someone into a cockpit with no checklist. The leverage is in the other two.

The Experiment: og-cli
#

og is a single ~12 MB Go binary with three faces over the same client library:

og               # interactive TUI (Bubble Tea) — for humans
og <command>     # direct CLI — for scripts and humans
og mcp           # MCP server — for the AI

Same code, three interfaces. The TUI is pleasant. The CLI is scriptable. But og mcp --stdio is the one that changed how my colleagues work, and it exposes more than fifty tools — devices, data models, alarms, time series, datasets, operation jobs, IoT data ingestion, and the part I’ll come back to: workspaces and dashboards.

Wiring it into an MCP client is three lines:

{
  "mcpServers": {
    "opengate": { "command": "og", "args": ["mcp", "--stdio"] }
  }
}

After that, an agent in Claude Code, Claude Desktop, or LM Studio can operate a live OpenGate tenant in natural language. That’s the demo. The engineering is in making it reliable, and that’s where the design decisions earn their keep.

MCP Is More Than Tools
#

OpenGate’s North API filters with nested JSON: {"filter":{"eq":{"provision.device.administrativeState":"ACTIVE"}}}. Hand an LLM a tool that takes raw JSON like that and watch it confidently emit malformed filters — wrong nesting, invented operators, mismatched field paths. It’s the single most common failure mode when you bolt an agent onto a real API.

So og doesn’t expose raw JSON as the happy path. It exposes a flat query language — "field operator value" — and parses it into the correct nested structure internally:

# What the model would get wrong:
devices_search(filter: '{"filter":{"eq":{"provision.device.administrativeState":"ACTIVE"}}}')

# What it gets right, every time:
devices_search(query: "provision.device.administrativeState eq ACTIVE")

But a forgiving input is only half the fix. The model still has to know the syntax exists. That’s what the MCP prompt is for. og mcp serves a prompt called opengate-guide that teaches the model, before its first tool call:

  • The query syntax, and an explicit instruction to never build raw JSON.

  • A natural-language-to-operator mapping, bilingual — because our engineers and clients work in Spanish and English:

    "is" / "sea" / "igual a"        → eq
    "contains" / "contiene"          → like
    "greater than" / "mayor que"     → gt
    "at least" / "al menos" / ">="   → gte
    "exists" / "tiene" / "existe"    → exists
  • An entity mapping: “device”/“dispositivo” → devices_search, “alarm”/“alarma” → alarms_search, “launch operation”/“lanzar operación” → jobs_create.

  • The exact JSON shape for creating operation jobs (rebooting equipment, running diagnostics).

  • And twenty-eight worked examples, in both languages.

This is prompt engineering done where it belongs: shipped with the tool, versioned with the tool, instead of copy-pasted into every user’s system prompt and rotting on the first API change.

And then there are resources — data the model reads when it needs ground truth instead of guessing. og serves opengate://organizations/{org}/datamodel-fields, which fetches, live from the API, the datastream fields that actually exist in a given organisation’s data models. So when someone asks “show me devices where temperature is above 50,” the model doesn’t hallucinate a field name — it reads the resource, finds the real datastream id, and filters on it. Tools act, prompts instruct, resources ground. Use one, you have a gimmick. Use all three, you have something an agent can lean on.

The Part Everyone Asks About: Dashboards From a Prompt
#

og does plenty — triage alarms by severity, reboot a fleet of devices with one job, export time series to Parquet. All useful. But the feature that made it spread inside Amplía, and got a genuinely warm reception from our more technical clients, is the one I almost didn’t build: generating and publishing workspaces and dashboards.

Here’s why it’s hard, and why it matters. In OpenGate a workspace owns dashboards, and a real dashboard is not a static layout — it’s a grid of widgets, and the interesting widgets carry embedded JavaScript: chart formatters, value transforms, custom rendering. Nine kilobytes of chart code buried inside a JSON blob is not something a human enjoys editing, and it’s certainly not something you can ask an LLM to safely mutate when it’s stringified inside a config tree.

So og turns a dashboard into something both an IDE and an agent can work with. The verb pair is pull and deploy:

og workspace pull <id> --dir wsroot/      # explode into an editable tree
# ... an agent edits the extracted .js and .json ...
og workspace deploy wsroot/<slug> --update  # wrap + publish in one step

pull explodes the workspace into one folder per nesting level and extracts every piece of embedded JavaScript into standalone .js files:

wsroot/dashboards-adif/
  workspace.json
  00__visualizacion-pbi/
    dashboard.json
    00__customchart__1727269767709-0/
      widget.json
      _widgetConfigCode.js        ← 9 KB of chart code, now a real .js file

The extraction is heuristic: any field named formatter, script, operation, code, or _widgetConfigCode is pulled out, plus any string long enough and containing JS keywords. Now the code has syntax highlighting, lints, and — the point — an AI agent can read and rewrite it as code instead of as an escaped string inside JSON.

The cycle is content-lossless: wrap reproduces a JSON with an identical SHA-256 per widget config. And deploy doesn’t naively POST a blob — it replays the exact multi-phase flow the OpenGate web wizard uses, so the dashboards and the JavaScript inside their widgets actually persist:

POST /api/workspaces           ← workspace shell
POST /api/dashboards × N       ← each dashboard with full grid + widgets
PUT  /api/workspaces/{id}      ← shell + dashboards as layout refs

The consequence is the demo that makes people sit up: “Clone the ADIF vibration dashboard into the new tenant and point it at these three sensors.” “Build me a workspace for this fleet with a max-values chart and a comparison view.” The agent pulls a reference, rewrites the layout and the formatter JS, and publishes it to a live tenant — in one conversation. Work that used to be an afternoon of careful clicking in a web UI is now a prompt. That is what an AI-first CLI buys you on top of a platform that was already good.

Safety Isn’t Optional When an AI Holds the Keys
#

Handing an autonomous agent write access to a production IoT platform is exactly as dangerous as it sounds, so a few decisions were non-negotiable:

  • Ownership filter. pull-all only unwraps workspaces and dashboards whose owner matches your profile’s email; foreign items are skipped with a note, and pull aborts on a foreign target unless you explicitly pass --force-owner. An agent can’t accidentally clobber a colleague’s dashboard.
  • Profiles as blast-radius control. You run the MCP server pinned to a profile — --profile staging vs --profile production — so you decide which environment the model can touch, per client config.
  • Resilient sessions. OpenGate allows one active web session per user; log into the web UI in another tab and your token dies. og detects the 401 and transparently re-signs in once before retrying, so a long agent session doesn’t fall over because someone opened the dashboard in a browser.

And to be explicit, because it matters: og-cli is a personal experiment of mine, not an official Amplía product, and it carries a disclaimer to that effect. You point it at a real platform at your own risk. The fact that it’s safe enough that our own engineers reach for it daily is the endorsement I care about.

Why This Is the Cheapest Lever in AI Right Now
#

Strip away the IoT specifics and here is the general lesson, the one I’d give any CTO sitting on a platform with a decent API:

You do not need to fine-tune a model, stand up a RAG pipeline, or build a bespoke agent framework to make AI genuinely useful on top of your product. You need a small, well-designed CLI that speaks MCP — tools to act, resources to ground, prompts to instruct — and forgiving inputs so the model’s first guess works. That’s a weekend, not a quarter. The ROI is absurd.

It also says something about the platform underneath. og-cli is thin; almost all of its leverage comes from the fact that OpenGate’s API was clean and complete enough that a single binary could expose fifty tools over it without fighting the platform. A weekend experiment that lets an AI search a fleet, triage alarms, run operations, and publish dashboards is only possible when the foundation is solid. The CLI gets the applause; the platform earns it.

I think we’re at the very beginning of this. The CLIs we’ve built for ourselves for thirty years assumed a human at the keyboard. The next generation assumes an agent — and the teams that design for that reader, deliberately, will hand their users capabilities the rest are still clicking through by hand.

Build the CLI for the AI. The humans will thank you anyway.


og-cli is open source (Apache 2.0) at github.com/carlosprados/og-cligo install github.com/carlosprados/og-cli@latest. It’s an independent, community tool and not an official product of Amplía Soluciones, the company behind the OpenGate IoT platform. If the dashboard-from-a-prompt workflow sounds useful for your fleet, that’s the conversation I’d love to have.