Async CLI backend for LLM-powered automation. The agent runs as a single command, loads configuration from TOML, executes tool/function calls in a loop, and emits shell directives via the ADD <cmd> protocol on stdout while showing progress on stderr.
- Install deps:
uv sync --extra dev(orpip install -e .[dev]). - Provide an API key:
export OPENAI_API_KEY=...(configurableapi_key_env). - Install the CLI:
uv tool install .(installscli-agenton PATH). - Run once (e.g.,
cli-agent --version) to bootstrap config and shell plugins at~/.config/cli-agent/plugin.{zsh,bash}(or alongside your chosen--config). - Source the matching plugin in your shell init (
~/.zshrcor~/.bashrc), open a new shell, and use the@prefix (e.g.,@summarize README.md). Use@reset_sessionto clear chat/nl history locally.
Install globally with uv tool install . to make cli-agent available on PATH. Running with no arguments prints nothing and exits successfully.
Search order: --config flag > CLI_AGENT_CONFIG env > ~/.config/cli-agent/config.toml > ./config.toml.
If nothing is found, cli-agent writes a default config to CLI_AGENT_CONFIG (when set) or ~/.config/cli-agent/config.toml; a template copy is available at config.example.toml.
- The system prompt is baked into the binary and not written to configs. Leave
prompt.system_promptempty to use it; override only if you accept weaker safety. - Extra OpenAI parameters (e.g.,
temperature,max_output_tokens,reasoning_effort) can be set under[provider.model_params]and are passed through as-is. agent.follow_cwdcontrols whether the shell follows the agent's working directory after a run (default true).
[provider]
name = "openai"
api_key_env = "OPENAI_API_KEY"
model = "gpt-4.1-mini"
base_url = ""
[provider.model_params]
# temperature = 0.0
# max_output_tokens = 1024
# reasoning_effort = "medium"
[agent]
max_steps = 20
timeout_sec = 90
max_tool_calls_per_step = 10
history_dir = "~/.local/share/cli-agent"
session = "default"
follow_cwd = true
[prompt]
# leave blank to use the built-in secure system prompt (recommended)
system_prompt = ""
custom_prompt = ""
custom_prompt_mode = "developer" # or "system" to append to the system message
[ui]
rich = true
show_tool_args = true
show_step_summary = true
render_markdown = true- CLI builds message history from
history_dir/<session>/chat.jsonland appends the new request. - History is stored as lean lines (
role<TAB>content): user text, tool call summaries (tool\tname(args)), and assistant replies; tool outputs are not persisted. Legacy JSON history is compacted on read. - Async LLM calls (OpenAI client) run under a Rich spinner on stderr.
- Tool calls are executed (
write_file,read_filewith optional line ranges,replace_in_file,run_cmd,ask_user), recorded in history, and iterated until completion ormax_steps. - Final assistant text prints to stderr; lines starting with
ADDare emitted on stdout only for shell execution. - Reset clears
chat.jsonlandnl_history.txtwithout touching config; use@reset_session(or legacy/reset) to trigger it locally without an LLM call. - Prompts: leave
prompt.system_promptempty to use the built-in secure prompt.prompt.custom_promptis added as a developer message (or appended to the system message whenprompt.custom_prompt_modeis"system"). - Legacy keys from older configs (e.g.,
default_mode,history_length,prompt_indicator,system_prompt_file/system_prompt_text) are ignored.
- Chat and tool traces:
history_dir/<session>/chat.jsonl - Natural-language shell prefix entries:
history_dir/<session>/nl_history.txt cli-agent Resetorcli-agent --resettruncates both files atomically.
The CLI writes plugin.zsh and plugin.bash next to your active config (default ~/.config/cli-agent/) and prints a reminder when it creates/updates them. Source the one that matches your shell to enable the @ prefix and nl-history navigation.
- Common: Enter runs
cli-agent "<payload>"; stdoutADD ...lines are executed; stderr shows the Rich UI. - zsh:
source ~/.config/cli-agent/plugin.zsh; Up/Down arrows cycle throughnl_history.txtwhen the prefix is present. - bash:
source ~/.config/cli-agent/plugin.bash; Up/Down arrows walknl_history.txtfor prefixed input while regular shell history keeps working otherwise; the plugin hookscommand_not_found_handleto reroute@...commands (existing handlers are preserved). Bash treats/in command names as paths, so use@reset_session(not@/reset) for local reset.
@reset_session— truncate chat/nl history for the current session.@show_config— print the active config file.@show_help— short help on prefix usage.@update config <text>— send a config change request to the agent (forwarded to the LLM).
- Tests:
uv run pytest(smoke tests mock the LLM client; no network needed). - Coding style: PEP 8, stdout reserved for
ADDdirectives, UI on stderr. - Entry point:
main.py; core modules live inagent/config.py,agent/history.py,agent/llm_client.py,agent/tools.py,agent/loop.py,agent/ui.py.