Unified headless CLI for coding agents (codex, claude, copilot, gemini, opencode, goz). One interface, one JSONL output format, one resume mechanism.
Project description
heru
Unified headless CLI for coding agents. One interface, one JSONL event envelope, one resume mechanism — regardless of which underlying agent you use:
- codex (OpenAI)
- claude (Anthropic)
- copilot (GitHub)
- gemini (Google)
- opencode (any OpenAI-compatible)
- goz (Z.AI)
Why
Every coding-agent CLI has its own flags, its own event format, its own notion of "session", its own quota endpoint, and its own quirks. If you want to drive them headlessly from a script or higher-level orchestrator, you end up writing a separate wrapper for each — and when a new one ships you start over.
heru is that wrapper, once. Point it at any supported CLI and get the same shape out.
Usage
# Fresh run
heru codex "summarize the diff between main and my branch"
# Resume a prior session
heru codex --resume <session-id> "now also run the tests"
# Different engine, same CLI shape
heru claude "find the bug in src/foo.py"
# Apply a launch profile without changing the engine/parser
heru codex --profile zodex "run the subagent smoke test"
All output is streamed as unified JSONL by default. Pass --raw to get
the engine's native JSON/JSONL stream back for debugging.
The engine name is positional. The legacy --engine CLI form was removed
in v2.0.0; see CHANGELOG.md and
docs/migrations/2.0.0-remove-legacy-engine-flag.md.
Cross-provider usage/quota reporting lives in
quse. Use quse directly for
normalized quota windows.
Launch Profiles
Use --profile (-p) when an engine should launch with a different wrapper,
config directory, environment cleanup, or preflight command while keeping the
same engine parser and unified event output.
Profiles are read from ~/.config/heru/profiles.toml, or from the path in
HERU_PROFILES_FILE.
Example for a Z.AI-routed Codex profile managed by
~/git/.agents:
[profiles.zodex]
engine = "codex"
command = "codex"
unset_env_file = "~/git/.agents/config/codex/zodex_env_unset.txt"
preflight = [["~/git/.agents/scripts/zodex_start_proxy.sh"]]
[profiles.zodex.env]
CODEX_HOME = "~/.zodex"
If you have a real wrapper executable, use it as command = "zodex" instead.
Then run:
heru codex --profile zodex "spawn one Python subagent and summarize the result"
Claude-style profile wrappers work the same way:
[profiles.zlaude]
engine = "claude"
command = "claude"
unset_env_file = "~/git/.agents/config/claude/zlaude_env_unset.txt"
[profiles.zlaude.env]
CLAUDE_CONFIG_DIR = "~/.zlaude"
Unified Event Schema
Each stdout line is a UnifiedEvent JSON object with these common fields:
kind:message,tool_call,tool_result,usage,error,status, orcontinuationengine: the emitting engine name (codex,claude,copilot,gemini,opencode,goz)sequence: zero-based event order within the runtimestamp: ISO-8601 timestamp for the emitted eventrole: optional role (assistant,user,system)content: assistant or status text when presenttool_name: tool identifier for tool-related eventstool_input: serialized tool input when presenttool_output: serialized tool output when presenterror: error text when presentusage_delta: per-event usage fields extracted from the provider payloadcontinuation_id: session/thread identifier when the engine emits oneraw: the original provider-native payload
Example:
{"kind":"message","engine":"claude","sequence":0,"timestamp":"2026-04-10T19:00:00+00:00","role":"assistant","content":"Done.","raw":{"type":"assistant","message":{"content":"Done."}}}
Install
pip install heru
# or
uv add heru
Status
v2.0.0 is a physical extraction from litehive, which uses heru as its engine execution layer. Several things from the vision above are still in flight:
- Unified JSONL output format —
herunow emits one documented event envelope across all supported engines by default. - Unified
--resumeacross all engines. - Positional engine argument (
heru codex <prompt>). - Standalone sandboxing — heru currently executes agents directly; sandbox integration is a follow-up.
Layout
heru/
adapters/ per-engine CLI wrappers (codex, claude, copilot, gemini, opencode, goz)
base.py ExternalCLIAdapter base class + CLIInvocation
types.py shared engine/stream/usage types
main.py entrypoint for `heru <engine> <prompt>`
tests/ unit tests for adapters, stream parsing, inactivity timeout
tests/contract/ is the standalone public-API contract suite. Treat it as the
"thou shalt not break" directory: changing assertions in that folder is a
semver-major change and requires the full breaking-change checklist below.
API Contract
The names below are heru's stable public contract. Changes to their signature, return shape, required fields, or documented behavior are breaking changes and require a semver-major release.
Public entrypoints
heru.get_engine(name): resolves a stable engine name such ascodexorclaudeto the adapter instance heru will execute.heru.ENGINE_CHOICES: lists the stable engine names accepted by the CLI andget_engine.heru.main.main(argv=None): provides the publicheruCLI entrypoint and preserves the supported argument contract.heru <engine> --profile <name> <prompt>: applies a launch profile from~/.config/heru/profiles.tomlorHERU_PROFILES_FILEwithout changing engine identity.
Base adapter contract
heru.base.ExternalCLIAdapter: base class callers may subclass against; heru preserves its constructor shape and override points as the adapter contract.heru.base.CLIInvocation: immutable invocation description containing argv, cwd, env, and optional stdin payload for a launch.heru.base.CLIExecutionResult: immutable execution record containing exit status, stdout/stderr, pid, and transcript accessors for a finished run.
Engine adapters
heru.adapters.codex.CodexCLIAdapter: stable adapter for Codex CLI command building, transcript rendering, usage extraction, and continuation discovery.heru.adapters.claude.ClaudeCLIAdapter: stable adapter for Claude Code invocation and unified stream parsing.heru.adapters.copilot.CopilotCLIAdapter: stable adapter for GitHub Copilot CLI invocation and stream parsing.heru.adapters.gemini.GeminiCLIAdapter: stable adapter for Gemini CLI invocation, usage extraction, and continuation parsing.heru.adapters.opencode.OpenCodeAdapter: stable adapter for OpenCode CLI invocation and stream parsing under the current exported class name.heru.adapters.goz.GozCLIAdapter: stable adapter for Goz CLI invocation, transcript extraction, and continuation parsing.
heru.types models
EngineUsageWindow: normalized usage-window counters and reset metadata extracted from provider output.EngineUsageObservation: normalized per-run usage or quota observation reported by adapters.UnifiedEvent: stable JSONL event schema emitted across engines.LiveEvent: aUnifiedEventinstance used inside live timelines.LiveTimeline: ordered live-event container with summary counts and task/subagent metadata.ResourceLimitEvent: normalized process-resource failure details attached to stage reports.RuntimeEngineContinuation: stable continuation/session handle for resume flows across engines.SubagentRef: stable reference to a spawned subagent as reported in pipeline payloads.
Internal modules
The modules below are internal implementation details and may change without notice in any release:
heru._continuationheru.adapters._codex_implheru.adapters._claude_implheru.adapters._copilot_implheru.adapters._gemini_implheru.adapters._opencode_implheru.adapters._goz_impl
The same rule applies to private helpers and any _-prefixed name in a
public module unless this README explicitly lists it as part of the
stable contract.
Stability Matrix
| heru version | Public API breakage |
|---|---|
v0.1.0 |
Initial extracted public contract. No documented breaking public API changes yet. |
v1.0.0 |
Breaking change: provider-specific quota dataclasses and CLI fields replaced by unified short_term / long_term usage windows. |
v2.0.0 |
Breaking change: removed the legacy prompt-first --engine form; positional heru <engine> <prompt> is now required. |
v2.1.0 |
Breaking change: removed heru usage; use quse for normalized quota reporting. |
Submitting A Breaking Change
If you need to break any public API listed above:
- Bump heru's semver major version in
pyproject.toml. - Add a CHANGELOG entry that names the broken public API and the replacement.
- Add a migration note for litehive and any other known consumer that relies on the changed contract.
Tests
uv run pytest
tests/contract/ is included in the default uv run pytest discovery. Those
tests are fixture-only and are intended to pass in a fresh heru-only virtualenv
without litehive, engine CLIs, or network access.
If a PR changes any assertion under tests/contract/, that PR is changing the
documented public contract and must also:
- Bump heru's major version in
pyproject.toml. - Add a
CHANGELOGentry for the break. - Add a migration note describing what downstream users, including litehive, must change.
Development
Install the versioned git hook with:
./scripts/install-hooks.sh
The installer symlinks scripts/pre-commit.sh into your real git hook directory using git rev-parse --git-path hooks/pre-commit, so it works from normal checkouts and git worktrees.
When staged files touch heru/ or tests/, the hook treats that commit as a potential change to heru's public contract with litehive and runs this focused smoke suite in the litehive repo:
cd ../litehive && uv run pytest -q \
tests/test_runner_workflow.py \
tests/test_engine_variants_and_timeline.py \
tests/test_heru_cli.py \
tests/test_observability_and_status.py
The hook looks for litehive in a sibling checkout at ../litehive, then falls back to LITEHIVE_REPO. If neither exists, it prints a warning and skips the smoke run so standalone heru users are not blocked.
The smoke suite is intended to protect the editable-install contract between heru and litehive: litehive imports heru directly from your local checkout, so breaking heru method names, CLI argv shape, or event schema can regress litehive immediately. If you need to bypass the hook intentionally, commit with git commit --no-verify, but that should be the exception rather than the default workflow.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file heru-2.1.1.tar.gz.
File metadata
- Download URL: heru-2.1.1.tar.gz
- Upload date:
- Size: 1.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34ad5570c4f2faf00d6c33b064b99555d04a9ed66124c528b7ffb648ce2da783
|
|
| MD5 |
c71067d5eb90125270e404ad51bebe14
|
|
| BLAKE2b-256 |
3ec1194725c29f12bf43a913e8fa4796711fe9e701e16ab716d30fa08b635a29
|
File details
Details for the file heru-2.1.1-py3-none-any.whl.
File metadata
- Download URL: heru-2.1.1-py3-none-any.whl
- Upload date:
- Size: 52.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e10d361f96fb1b156d9b1ef55a7d79f43d59313c1cc4dc9c59e458e488e197e1
|
|
| MD5 |
41bd37b689259445c22c9618fc5c0c61
|
|
| BLAKE2b-256 |
03b4d90c9986143320e0fec779f0724d55ba6412a726fd0a79942520ea3916a5
|