Self-contained agentic CLI for local LLMs. REPL + one-shot CLI, with tool whitelisting, SQLite audit, and per-call artifact bundles. The production-grade discipline most local-LLM frameworks lack — in 1 ~MB pip install.
pip install johnalin
ollama pull qwen2.5:7b # or any OAI-compatible local model
johnalin # interactive REPL
johnalin-cli --tools glob,bash -m "count .py files in this repo" # one-shotTwo binaries, one package, zero servers to run separately. Talks to ollama (or any OpenAI-compatible local backend) directly, runs the tool loop in-process, writes a forensic audit bundle for every call.
Most frameworks dump every registered tool into the system prompt on every turn. johnalin lets you specify exactly which tools the agent gets per call:
johnalin-cli --tools file_read,grep -m "find TODO comments under src/"
johnalin-cli --tools bash -m "show the last 3 git commits"Smaller system prompt = lower latency, less "lazy" model behaviour, less cost on hosted backends.
Every call writes a forensic record under ~/.johnalin/artifacts/YYYYMMDD/iter_<utc>Z/<run_id>/:
metadata.json — run_id, agent_tag, host, timings, context-fit verdict
request.json — what we sent (message + tools + cwd + model)
response.json — what we got (text + stopped reason + status)
trace.json — every event the engine emitted (turn_start, usage,
tool_call (with args + result), turn_end, etc.)
Real metadata.json from the showcase below:
{
"run_id": "cli_f4ab113608f5",
"executor": "johnalin",
"duration_ms": 3830,
"context": {
"peak_prompt_tokens": 498,
"max_context": 16384,
"utilization": 0.03,
"fits": true,
"risk": false
}
}Both johnalin and johnalin-cli log every turn to ~/.johnalin/history.db with the same schema. Grep across modes, query timings, replay turns:
SELECT run_id, model, stopped, turns, in_tokens, out_tokens, duration_ms, substr(input_text,1,55)
FROM turns ORDER BY ts DESC LIMIT 5;johnalin-cli returns distinct exit codes so wrappers can branch deterministically:
| Exit | Meaning |
|---|---|
| 0 | stop — model finished cleanly |
| 1 | tool_calls_unparseable — model emitted broken JSON args |
| 2 | repeat_loop — same tool call twice in a row, killed |
| 3 | max_turns — tool loop hit the cap without finishing |
| 4 | http/network error reaching ollama |
| 5 | bad CLI args |
johnalin-cli --tools file_read,grep -m "$prompt" || handle_failure $?Three real johnalin-cli invocations against a local Qwen 27B running in ollama. Replicate with:
export OLLAMA_MODEL=qwen2.5:7b # or whatever model you pulled
export JOHNALIN_HOME=/tmp/johnalin_showcase$ johnalin-cli --tools NONE -m "/no_think Reply with exactly: hello from johnalin v0.1.0"
The user wants me to reply with a specific string. I will output that string.
hello from johnalin v0.1.0
run_id=cli_0b5b0a23321e stopped=stop duration_ms=10461
$ johnalin-cli --tools glob -m "Use glob with pattern '**/*.py' and root='johnalin'.
Reply with just the count." --max-turns 4
18
run_id=cli_f4ab113608f5 stopped=stop duration_ms=3830
The model emitted a single glob({"pattern":"**/*.py","root":"johnalin"}) call,
got 18 file paths back, and replied with the integer. 2 turns, 3.8 seconds.
$ johnalin-cli --tools file_read -m "Read /tmp/test_readme.md.
Reply with just the line count." --max-turns 3
5
run_id=cli_3b18c9f746da stopped=stop duration_ms=1831
$ ls /tmp/johnalin_showcase/
artifacts/ history.db
$ find /tmp/johnalin_showcase/artifacts -name '*.json' | wc -l
12 # 4 files × 3 demos
$ sqlite3 /tmp/johnalin_showcase/history.db \
"SELECT run_id, stopped, turns, in_tokens, duration_ms FROM turns"
cli_0b5b0a23321e | stop | 1 | 135 | 10461
cli_f4ab113608f5 | stop | 2 | 498 | 3830
cli_3b18c9f746da | stop | 2 | 438 | 1831
{
"events": [
{ "type": "system_prompt_built", "systemPromptChars": 440, "toolCount": 1 },
{ "type": "turn_start", "turn": 0 },
{ "type": "usage", "turn": 0, "prompt_tokens": 348, "completion_tokens": 82 },
{ "type": "assistant_message", "turn": 0, "finish_reason": "tool_calls", "tool_call_count": 1 },
{ "type": "tool_call", "turn": 0, "name": "glob",
"args": { "pattern": "**/*.py", "root": "johnalin" }, "ok": true,
"result_preview": "johnalin/__init__.py\njohnalin/__main__.py\njohnalin/audit.py\n..." },
{ "type": "turn_end", "turn": 0, "stopped": "continued" },
{ "type": "turn_start", "turn": 1 },
{ "type": "assistant_message", "turn": 1, "finish_reason": "stop" },
{ "type": "turn_end", "turn": 1, "stopped": "stop" }
]
}Every tool call captured with name, args, ok, result_preview, result_chars — the exact debug surface most local-LLM CLIs don't give you.
pip install johnalin
# Backend: any OpenAI-compatible local LLM
# Easiest is ollama:
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2.5:7b # ~4.7 GB, runs on 8 GB VRAM
ollama serve & # in another terminalThen:
johnalin # REPL
johnalin-cli --tools NONE -m "say hi" # one-shot smoke testAll paths configurable. Zero hardcoded paths anywhere in the package.
| Env var | Default | Purpose |
|---|---|---|
OLLAMA_BASE_URL |
http://127.0.0.1:11434/v1 |
OpenAI-compat endpoint |
OLLAMA_MODEL |
qwen2.5:7b |
Model id sent to the backend |
JOHNALIN_NUM_CTX |
16384 |
Context window passed as options.num_ctx |
JOHNALIN_HOME |
~/.johnalin |
Base dir for db + artifacts |
JOHNALIN_DB |
$JOHNALIN_HOME/history.db |
SQLite history |
JOHNALIN_ARTIFACTS |
$JOHNALIN_HOME/artifacts |
Per-call bundle root |
JOHNALIN_MAX_TURNS |
10 |
Tool-loop cap |
JOHNALIN_TIMEOUT |
900 |
HTTP timeout to backend (seconds) |
JOHNALIN_AGENT_TAG |
user |
Stamped into every metadata.json |
CLI flags (--model, --num-ctx, --base-url, --max-turns, --cwd) override env per call.
| Tool | Schema | Purpose |
|---|---|---|
bash |
command, [timeout] |
Run a shell command; returns [exit N]\n<output> capped at 16 KB |
file_read |
path, [offset], [limit] |
Read UTF-8 file with line numbers, 64 KB cap |
file_write |
path, content |
Overwrite text file (creates parent dirs) |
glob |
pattern, [root] |
Find files by glob (supports **), max 200 results |
grep |
pattern, [path], [glob] |
Search file contents (uses rg if available) |
Custom tools register at runtime — see examples/custom_tool.py.
- Multi-line input: paste freely, Esc-Enter (or Alt-Enter) submits
- Slash commands:
/tools,/notools,/stats,/model,/help,/exit - Per-turn debug panel: input, assistant reply (markdown rendered), tool calls with args + result preview, summary table with token counts + context-fit verdict + duration + artifact path
- Every prompt runs against a fresh context (no chat-history accumulation in memory; SQLite + scrollback are your durable history)
- Auto-detects upstream health on startup; gives you a clear "ollama not running, start it like this" message if down
johnalin-cli --tools <list>|NONE --message <text>
[--max-turns N] [--cwd DIR]
[--model M] [--base-url URL] [--num-ctx N]
[--no-audit] [--quiet]
stdout: model reply text only. stderr: run_id=... stopped=... duration_ms=... (and warnings).
git clone https://github.com/wolfmib/johnalin.git
cd johnalin
python -m venv .venv && source .venv/bin/activate
pip install -e .[dev]
pytest tests/ -v # 26 tests
ruff check johnalin testsMIT — see LICENSE.
The architecture (tool whitelisting + audit bundling + exit-code discipline) was iterated on over months in a private fork before being lifted into this generic, self-contained release. If you've built local-LLM agents and hit the "system-prompt bloat" problem, this is the production-grade discipline that fixes it.
Comparable projects: OpenWebUI, Continue, LangChain, AutoGPT — all powerful, none combine all four differentiators above in one ~600-line agentic CLI you can pip install in 5 seconds.