Skip to content

What ActiveCLI is

ActiveCLI spawns coding agent CLIs as real processes and drives them through one interface. It does not call a vendor’s HTTP API, and it does not depend on an official SDK. The CLI that the user already has installed and logged into is the thing that runs.

That choice is deliberate. A CLI is a published, documented contract that a user can also invoke by hand. An internal protocol is not. When the library depends only on what the CLI itself offers, anything a user can do in a terminal remains reachable from a program.

The design comes from Rails’ ActiveRecord, and it borrows two separate things from it.

The adapter. ActiveRecord defines an AbstractAdapter and ships one concrete subclass per database — mysql2, pg — so an application talks to the adapter it was given. Changing databases is choosing a different subclass, not rewriting every call site.

The relation. ActiveRecord builds a query by accumulating intent and executing at the end: where(...).limit(...) changes nothing until to_a runs, and to_sql shows what would run without running it. Command is the same arrangement for a command line — setters accumulate, toArgv() shows, send() executes.

ActiveRecord ActiveCLI
Relation Command
where(...).limit(...) — accumulates setModel(...).setMessage(...) — accumulates
to_sql — shows without running toArgv() / toPayload() / inspect()
to_a — executes send()
Switching the connection costs nothing until execution command.provider = Codex costs nothing until the next send()

That second borrowing is what makes the public surface small. A caller never constructs a process, an adapter, or spawn options; they describe what they want and say when.

ActiveCLI applies the adapter shape underneath that.

ActiveRecord ActiveCLI
AbstractAdapter AbstractAdapter, minted by an AbstractProvider
Mysql2Adapter, PostgreSQLAdapter ClaudeAdapter, and one per CLI to follow
SQL dialect differences live in the subclass argv, permission-flag vocabulary and output classification live in the subclass
Connection pooling is shared machinery Executable resolution, PATH, and process trees are shared machinery

The division matters more than the analogy. A subclass owns everything its CLI spells differently. It does not own finding the executable, augmenting PATH, or killing process trees — those are the same problem for every CLI, so they live in Process/ and are handed to the adapter rather than reimplemented per provider.

Provider and adapter are not the same thing

Section titled “Provider and adapter are not the same thing”

A caller names a provider (Claude), never an adapter. The distinction is deliberate: an adapter is a working object with collaborators and state, while a provider is the identity of a CLI — the thing you point at when you say “talk to claude” or “switch to codex”.

command.provider = Codex; // a value assignment, not handing over machinery

The provider mints an adapter when the command actually needs one, which is at send() and not before.

Why the CLI’s own commands, and not its internal channel

Section titled “Why the CLI’s own commands, and not its internal channel”

A running CLI exposes an undocumented control channel, and it is tempting: it is more structured than printed output and easier to parse. This library treats it as an optimisation, never a dependency.

Question How this library answers it Rejected alternative
Which MCP servers are configured? claude mcp list, and parse what it printed An undocumented mcp_status control subtype
Is the user logged in? claude auth status --json Reading credential files directly
Which version is installed? claude --version Inspecting the install directory
Stop the current turn Control channel, with stop() as the fallback that always exists Control channel alone

The cost is a parser for human-facing prose, confined to McpOutputParser. The benefit is that the contract is the one the CLI’s own maintainers must not casually break, because their users read that output every day.

Swttch, a Claude Code GUI plugin for JetBrains IDEs, is the first consumer. Its existing CLI-control code in backend/src/core/ is being lifted into this library.

Existing code in Swttch ActiveCLI class
buildClaudeArgs() Adapter.Claude.ClaudeArgv
Assembling a turn and deciding when to spawn Command
claude mcp list output parsing Mcp.McpCommands, Mcp.McpOutputParser
claude auth status / auth login Auth.AuthCommands, Auth.LoginSession
spawn + stream-json parsing Adapter.Claude.ClaudeAdapter
receiving control_request Event.PermissionRequestEvent
sendControlResponseToProcess() PermissionRequestEvent#approve() / #deny()
which-launcher.ts, claude-bin-paths.ts Process.Launcher.*
win-exec.ts, win-job.ts Process.Launcher.WindowsLauncher, Process.Job.JobSpawner
wsl-path.ts Path.WslUncPath
parent-watchdog.ts Process.Watchdog.ParentWatchdog
cli-registry.ts Registry.CliRegistry

There is no new protocol to design here. The work is placing something that already runs into classes, according to the principles.

Provider differences — Codex’s threadId against Claude’s sessionId — only become visible when a second adapter is attached. A contract drawn from a single implementation is a guess; the shape worth committing to is the one that survives a second.

This is also why Command is where the public surface stabilised first. It speaks in intent — a message, a model, a permission mode — and none of those words are Claude’s. A second provider changes what happens under send(), not what a caller writes above it.

The intended order is therefore:

  1. Port ClaudeAdapter accurately.
  2. Attach a second adapter, and extract the common contract from the two.
  3. Only then fix AbstractAdapter.

ActiveRecord also began with MySQL alone.