DEV Community

Andrea Debernardi
Andrea Debernardi

Posted on • Originally published at tabularis.dev on

Fable 5 Opened a 1,800-Line PR on Tabularis in 30 Minutes

_ Daily Dev Experiment — a short series where we hand a real task to an AI on the Tabularis codebase and report exactly what happened. No staged demos. Today's run got a little out of hand._

We asked Fable 5 (Anthropic's new model) to add a command-line interface to the app. That's less trivial than it sounds: this is a Tauri app — Rust backend, React frontend — so "a CLI" means reaching every saved connection (keychain passwords, SSH/K8s tunnels, plugin drivers) without booting the GUI or starting the Tauri runtime at all.

One prompt. A coffee break. A draft PR waiting at the end of it.

Here's what was in it.

The numbers

Lines added 1,810
Lines removed 319
Files touched 16
New unit tests 37 (full suite: 692 passing)
Pre-existing bugs fixed 2 — we didn't ask for either
Wall-clock time ~30 minutes

We've spent longer naming a variable.

What we expected

A coherent set of clap subcommands, addressed by connection id or name:

Command What it does
tabularis connections (ls) List saved connections — table or --json
tabularis databases <conn> List databases on the server
tabularis schemas <conn> List schemas
tabularis tables <conn> List tables (-d, --schema, --json)
tabularis describe <conn> <table> Columns, indexes, foreign keys
tabularis query <conn> [SQL] (q) One-shot query, stdin pipe, or interactive shell
tabularis install-cli Symlink the binary into a PATH directory

The detail we like most: query picks its mode from the invocation. SQL argument → one-shot. Piped stdin → executes the piped statement. Interactive TTY with no SQL → drops into a proper SQL shell.

# one-shot query, pipe-friendly
tabularis query my-db "select id, name from customers" --format csv > out.csv

# pipe a statement in
echo "select count(*) from orders" | tabularis q my-db

# no SQL on a TTY → drop into a proper SQL shell
tabularis query my-db

Enter fullscreen mode Exit fullscreen mode

The interactive shell is backed by rustyline: line editing, persistent history (cli_history.txt in the app config dir), multi-line statements that fire on a terminating ;, Ctrl-C drops the current buffer, Ctrl-D exits. Plus psql-style meta commands:

\l list databases \f table|json|csv output format
\dn list schemas \limit N row limit (0 = unlimited)
\dt list tables \schema NAME set schema
\d T describe table \use DB switch database
\q quit \? help

Enter fullscreen mode Exit fullscreen mode

Result data goes to stdout , logs go to stderr — so piping stays clean, and you get a non-zero exit code on failure. Exactly the kind of detail a human means to remember and usually forgets.

It even kept the GUI-launch fallback intact: macOS still passes junk like -psn_* to the binary on launch, and that must keep booting the GUI — while a misspelled subcommand should surface clap's error instead of silently opening a window. It threaded that needle, and wrote tests asserting the exact clap error kinds that fall through to the GUI versus the ones that don't.

What we did NOT expect

1. It found the refactor before writing a single feature.

The app already ships an MCP server, and that server already knew how to resolve a saved connection — decrypt the keychain password, open the SSH/K8s tunnel, register the right plugin driver. Instead of duplicating all of that for the CLI, Fable 5 dug through the codebase, realized the logic was buried inside mcp/mod.rs, and pulled it out into a shared headless.rs module that both the MCP server and the new CLI consume. The MCP server now just wraps the shared helpers with its JSON-RPC error type — zero behavior change on that side.

That's 242 lines deleted from mcp/mod.rs and a 214-line module born. It's the refactor we would have done — unprompted, because it understood why duplicating connection resolution was the wrong move.

It also didn't dump everything into one file. The old 52-line cli.rs became a real module:

src-tauri/src/cli/
├── mod.rs clap definitions, GUI-fallback logic (182 lines)
├── run.rs command dispatch + execution (338 lines)
├── repl.rs the interactive shell (282 lines)
├── output.rs table / JSON / CSV rendering (138 lines)
├── install.rs install-cli symlink logic (97 lines)
└── *_tests.rs args, output, run, install (442 lines)

Enter fullscreen mode Exit fullscreen mode

2. It fixed two real bugs that were already there.

  • keychain_utils was logging with println!, dumping straight to stdout. Harmless in a GUI — silently corrupting any piped CSV/JSON the moment a CLI exists. And it was bypassing the in-app log buffer too. It rerouted everything through the log crate.
  • Headless processes never called sqlx::any::install_default_drivers(), so the default connection-test path panicked. It installed the drivers in the shared headless::register_drivers() — which also quietly fixed the existing --mcp mode.

Neither was in the prompt. It found them because it actually traced the execution path from "user pipes a query" to "bytes hit the terminal" and noticed what would break along the way.

3. It handled multi-database connections properly.

Multi-db connections used to resolve to their first database, which made the others unreachable outside the GUI. Every db-scoped command now takes -d/--database, and the shell's \use <db> doesn't just flip a variable — it validates the switch with a connection test before applying it, and the prompt shows where you are (Demo · MySQL:blog_demo>). Under the hood it reuses the GUI's per-call database-override semantics (DatabaseSelection::Single) instead of inventing a parallel mechanism.

4. It tested its own work against a live database.

The 37 unit tests aren't padding: clap parsing (including the GUI-fallback error kinds), table/CSV/JSON rendering (column alignment, control-character escaping, CSV quoting), limit and database-override semantics, and the install-cli symlink logic — idempotency, refusal to clobber a foreign file, --force.

Then it went further: it ran the new shell against a real MySQL connection, switched across the three demo databases with \use, eyeballed the table/CSV/JSON output, and fixed the formatting it didn't like — before handing over the PR.

So... do we trust it?

No. It's a draft PR and we're reviewing every line before anything ships. There are real limitations — which, to its credit, it flagged itself in the PR description:

  • On Windows release builds the binary has no attached console (windows_subsystem = "windows"), so CLI output is invisible there. Same constraint the --mcp mode always had.
  • Each shell statement runs on its own pooled connection, so session state — SET, transactions, temp tables — doesn't persist between statements. It even documents that caveat inside \?.

But here's the part that stuck with us: the review is genuinely worth doing. This isn't autocomplete spitting out a function body. It's an agent that read the architecture, found the seam we'd have found, refactored toward it, and cleaned up two messes on the way out — then wrote 37 tests and a PR description more thorough than most humans bother with.

Two years ago this was Tab-complete. Today it's a coworker whose work we have to code-review.

👉 Read the full PR — every line, the real description: #313

Where this is going

This experiment isn't a side quest — it's the direction. We're building a database client that's native to this new workflow: a built-in MCP server so agents like the one in this post can work against your databases, with approval gates, a read-only mode, and a full audit log so they do it on your terms. An AI assistant that also runs on open-source models — locally via Ollama, with zero schema data leaving your machine. And now a CLI, born from that same headless core.

An agent wrote today's feature. The point is that tomorrow, your agents get a first-class, safe way to use it.

Top comments (0)