<p align="center">
<img src="assets/cli_subprocess_core.svg" alt="CliSubprocessCore logo" width="240" />
</p>
# CliSubprocessCore
<p align="center">
<a href="https://hex.pm/packages/cli_subprocess_core">
<img src="https://img.shields.io/hexpm/v/cli_subprocess_core.svg" alt="Hex" />
</a>
<a href="https://hexdocs.pm/cli_subprocess_core">
<img src="https://img.shields.io/badge/hexdocs-docs-blue.svg" alt="HexDocs" />
</a>
<a href="https://github.com/nshkrdotcom/cli_subprocess_core">
<img src="https://img.shields.io/badge/github-nshkrdotcom%2Fcli__subprocess__core-24292e.svg" alt="GitHub" />
</a>
</p>
`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
- `CliSubprocessCore.Command` for provider-aware one-shot CLI execution.
- `CliSubprocessCore.RawSession` for provider-agnostic long-lived raw sessions.
- `CliSubprocessCore.Session` for normalized provider sessions and event fanout.
- `CliSubprocessCore.Channel`, `CliSubprocessCore.ProtocolSession`, and
`CliSubprocessCore.JSONRPC` for framed or protocol-driven CLI interactions.
- `CliSubprocessCore.ProviderProfile`, `CliSubprocessCore.ProviderRegistry`,
and `CliSubprocessCore.ProviderProfiles.*` for provider-specific command
planning and parsing.
- `CliSubprocessCore.Event`, `CliSubprocessCore.Payload.*`, and
`CliSubprocessCore.Runtime` for the shared runtime vocabulary.
- `CliSubprocessCore.ModelRegistry`, `CliSubprocessCore.ModelInput`, and
related catalog helpers for centralized model policy.
## 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`:
- `ExternalRuntimeTransport.ExecutionSurface`
- `ExternalRuntimeTransport.Transport`
- adapter registry and transport contracts
- built-in `:local_subprocess`, `:ssh_exec`, and `:guest_bridge` families
- shared `ProcessExit`, `LineFraming`, and transport result types
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
```elixir
def deps do
[
{:cli_subprocess_core, "~> 0.1.0"}
]
end
```
## Quick Start
Run a provider-aware one-shot command:
```elixir
{: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:
```elixir
{: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:
```elixir
{: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:
```elixir
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:
- `contract_version`
- `surface_kind`
- `transport_options`
- `target_id`
- `lease_ref`
- `surface_ref`
- `boundary_class`
- `observability`
It does not expose adapter module names. Public callers do not choose
`LocalSubprocess`, `SSHExec`, or `GuestBridge` directly.
Callers may supply that value either as:
- `execution_surface: [...]`
- `execution_surface: %{...}`
- `execution_surface: %CliSubprocessCore.ExecutionSurface{}`
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
- `guides/getting-started.md` for the main public entrypoints.
- `guides/external-runtime-transport.md` for the shared placement seam.
- `guides/execution-surface-compatibility.md` for the compatibility facade
exported for downstream packages.
- `guides/command-api.md`, `guides/channel-api.md`, and `guides/session-api.md`
for the primary runtime APIs.
- `guides/raw-transport.md` and `guides/shutdown-and-timeouts.md` for the
transport boundary surfaced through `RawSession`.
- `guides/developer-guide-adding-transports.md` for the ownership rule after
extraction.
- `examples/README.md` for runnable examples.
## 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.
- shared provider-profile transport options retain `max_buffer_size`,
`oversize_line_chunk_bytes`, `max_recoverable_line_bytes`, `oversize_line_mode`, and
`buffer_overflow_mode`
- the common capability vocabulary now has a stable place for session-history, resume, pause, and
intervention support
- higher layers can reason about fatal data-loss boundaries without re-inventing transport-specific
heuristics
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.