CliSubprocessCore logo

CliSubprocessCore

HexHexDocsGitHub

cli_subprocess_core is the shared runtime for provider-facing CLIs. It owns provider profile resolution, normalized command/session APIs, event and payload shaping, model policy helpers, and the built-in first-party profiles for Claude, Codex, Gemini, and Amp.

The raw execution substrate now lives in external_runtime_transport. cli_subprocess_core consumes that package through one public placement seam: execution_surface.

For downstream packages that still type against the historical module name, CliSubprocessCore.ExecutionSurface remains available as a compatibility facade over ExternalRuntimeTransport.ExecutionSurface.

What This Package Owns

What This Package Does Not Own

cli_subprocess_core no longer owns the raw execution substrate modules. The following are owned by external_runtime_transport:

That separation keeps provider/runtime behavior in the core while leaving raw process placement reusable by non-CLI stacks.

The compatibility facade does not change that ownership boundary. Transport validation, capabilities, and dispatch still live in ExternalRuntimeTransport.ExecutionSurface.

Installation

def deps do
  [
    {:cli_subprocess_core, "~> 0.1.0"}
  ]
end

Quick Start

Run a provider-aware one-shot command:

{:ok, result} =
  CliSubprocessCore.Command.run(
    provider: :claude,
    prompt: "Summarize this repository"
  )

IO.inspect(result.output)

Move that command onto SSH through the generic placement seam:

{:ok, result} =
  CliSubprocessCore.Command.run(
    provider: :codex,
    prompt: "Review the latest diff",
    execution_surface: [
      surface_kind: :ssh_exec,
      transport_options: [
        destination: "buildbox.example",
        ssh_user: "deploy"
      ]
    ]
  )

Use RawSession when you need exact-byte ownership and normalized collection:

{:ok, session} =
  CliSubprocessCore.RawSession.start("sh", ["-c", "cat"], stdin?: true)

:ok = CliSubprocessCore.RawSession.send_input(session, "alpha")
:ok = CliSubprocessCore.RawSession.close_input(session)

{:ok, result} = CliSubprocessCore.RawSession.collect(session, 5_000)
IO.inspect({result.stdout, result.exit.code})

Use Session when you want normalized provider events:

ref = make_ref()

{:ok, _session, info} =
  CliSubprocessCore.Session.start_session(
    provider: :gemini,
    prompt: "Hello from the shared runtime",
    subscriber: {self(), ref}
  )

IO.inspect(info.delivery)

Execution Surface

cli_subprocess_core keeps the public placement seam intentionally narrow. The only public way to choose where a command runs is one execution_surface value.

That contract carries:

It does not expose adapter module names. Public callers do not choose LocalSubprocess, SSHExec, or GuestBridge directly.

Callers may supply that value either as:

The first two are the preferred long-term shapes. The struct form remains for downstream compatibility.

When that surface needs to cross a boundary, use CliSubprocessCore.ExecutionSurface.to_map/1 or ExternalRuntimeTransport.ExecutionSurface.to_map/1 to project the versioned map form.

Documentation

Emergency Hardening Surfaces

cli_subprocess_core now preserves the transport hardening controls that matter to upper layers instead of flattening them away inside provider defaults.

This repo is still not a retry engine. It is the boundary that keeps subprocess and provider profiles honest about what can be recovered and what must fail.