Metadata-Version: 2.4
Name: llmbash
Version: 0.1.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Rust
Classifier: Topic :: System :: Shells
License-File: LICENSE
License-File: LICENSES/brush-core.txt
License-File: LICENSES/pi-builtins.txt
License-File: LICENSES/pi-shell-NOTICE.txt
License-File: LICENSES/pi-shell.txt
License-File: LICENSES/pi-walker.txt
Summary: An embeddable bash-compatible shell that minimizes its own output, for LLM agents
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/longDan1991/llm-sh#readme
Project-URL: Issues, https://github.com/longDan1991/llm-sh/issues
Project-URL: Source, https://github.com/longDan1991/llm-sh

# llm-sh

An embeddable bash-compatible shell that minimizes its own output, for LLM agents.

The shell runs **in this process**. Common commands — `cat`, `grep`, `sed`, `ls`,
`find`, `sort`, `jq`, `diff`, and about fifty more — are implemented in Rust
rather than executed as child processes, so they need no system binaries and
cost no `fork`/`exec`. Each command's output is then passed through a per-tool
minimizer before it reaches your context, with the full capture kept for when
you need it.

```
git log -n 60          22,509 →  9,386 bytes   (-58%)
ls -la                  3,288 →    920 bytes   (-72%)
```

> **Status: alpha.** Published on PyPI as `llmbash` (the name `llm-sh` collides
> with existing packages); wheels are also built locally. API is expected to change.

## Install

```bash
pip install llmbash
```

Requires Python 3.11+. One `abi3` wheel covers 3.11 and later; no Rust toolchain
is needed at install time.

## Quickstart

```python
import asyncio, llm_sh

async def main():
    shell = llm_sh.Session()                    # one persistent shell
    result = await shell.run("cargo test", cwd="/repo")

    print(result.exit_code)                     # 0
    print(result.output)                        # minimized transcript
    if result.original is not None:             # only when a filter rewrote it
        print(result.input_bytes, "→", result.output_bytes)

asyncio.run(main())
```

Streaming, as output is produced:

```python
async for chunk in shell.stream("make -j8"):
    if chunk.kind == "output":
        print(chunk.data, end="")
    else:                                       # kind == "exit"
        print("exit", chunk.result.exit_code)
```

## API

| | |
| --- | --- |
| `Session(*, session_env=None, minimize=True)` | A persistent shell. Creating one is cheap. |
| `await session.run(cmd, *, cwd=None, env=None, timeout_ms=None)` | Run to completion → `RunResult`. |
| `session.stream(cmd, *, ...)` | Async iterator of `Chunk`: `"output"` items, then one `"exit"`. |
| `await session.abort()` | Abort the command currently running. |
| `await session.live_background_job_count()` | Live `&` / `nohup` children. |

`RunResult`: `exit_code`, `output`, `original`, `filter`, `input_bytes`,
`output_bytes`, `cancelled`, `timed_out`, `working_dir`.

### Semantics worth knowing

- **A session is stateful.** `cwd`, exported variables and shell functions
  survive across calls: `await s.run("cd /tmp")` affects the next `run`. Relative
  paths resolve against the session's directory, never the host process's.
- **A session runs its own commands one at a time.** There is a single shell
  state behind it, so concurrent awaits on the *same* session queue up. Use
  separate sessions for parallelism — `asyncio.gather(s1.run(a), s2.run(b))`
  really does run both at once.
- **Cancellation reaches the children.** Cancelling an await (or hitting Ctrl-C)
  aborts the command *and* kills its process group. Abandoning a `stream()`
  iterator does the same.
- **A timeout is not an exception.** `timeout_ms` comes back as
  `timed_out=True`; the partial output is preserved.
- **A non-zero exit is not an exception.** Check `exit_code`.
- **Output is one merged transcript** on both `run` and `stream`, matching the
  minimizer's contract. `stderr` is not separated.

### What gets minimized

`filter` names the filter that produced `output` (`"git"`, `"ls"`,
`"pipeline:gradle"`) or the reason none did (`"piped"`, `"too-short"`,
`"disabled"`). A pipeline's output is deliberately left alone: if you are
piping through `jq` or `awk`, you are parsing it, and rewriting it would be
wrong.

`original` is populated only when a filter actually rewrote the output. Persist
it if you want to offer a "show me the full output" handle later.

## What's inside

The shell engine is a fork of [`brush`](https://github.com/reubeno/brush)'s
`brush-core`; the in-process utilities are ports of
[uutils](https://github.com/uutils/coreutils) coreutils/findutils/sed,
[jaq](https://github.com/01mf02/jaq), and moreutils-inspired implementations;
search is backed by [ripgrep](https://github.com/BurntSushi/ripgrep)'s crates.
Consequently a wheel is not small (~9 MB compressed): every bundled command is
reachable at runtime, so nothing can be pruned away.

This package is a thin Python layer over the Rust shell, which lives in
[`oh-my-pi`](https://github.com/can1357/oh-my-pi) — itself a fork of
[Pi](https://github.com/badlogic/pi-mono) by Mario Zechner. All MIT, with the
third-party notices reproduced in that repository.

## License

MIT.

