mini-code-cli 0.1.1.2__tar.gz → 0.1.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. mini_code_cli-0.1.1.3/PKG-INFO +390 -0
  2. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/README.md +26 -3
  3. mini_code_cli-0.1.1.3/mini_code_cli/__init__.py +59 -0
  4. mini_code_cli-0.1.1.3/mini_code_cli/__main__.py +12 -0
  5. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/agent_loop.py +20 -18
  6. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/runner.py +14 -6
  7. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/skills.py +4 -1
  8. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/ui.py +5 -11
  9. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/utils.py +63 -24
  10. mini_code_cli-0.1.1.3/mini_code_cli.egg-info/PKG-INFO +390 -0
  11. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli.egg-info/SOURCES.txt +4 -0
  12. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/pyproject.toml +7 -3
  13. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/tests/test_interactive.py +205 -0
  14. mini_code_cli-0.1.1.3/tests/test_permissions.py +85 -0
  15. mini_code_cli-0.1.1.3/tests/test_release_script.py +240 -0
  16. mini_code_cli-0.1.1.3/tests/test_runner.py +84 -0
  17. mini_code_cli-0.1.1.2/PKG-INFO +0 -1028
  18. mini_code_cli-0.1.1.2/mini_code_cli/__init__.py +0 -40
  19. mini_code_cli-0.1.1.2/mini_code_cli.egg-info/PKG-INFO +0 -1028
  20. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/LICENSE +0 -0
  21. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/config.py +0 -0
  22. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/permissions.py +0 -0
  23. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/persistence.py +0 -0
  24. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/plan.py +0 -0
  25. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/prompts.py +0 -0
  26. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/sandbox.py +0 -0
  27. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/shell_safety_utils.py +0 -0
  28. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/subagents/__init__.py +0 -0
  29. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/subagents/base.py +0 -0
  30. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/subagents/builtin.py +0 -0
  31. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/tokens.py +0 -0
  32. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli/tools.py +0 -0
  33. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli.egg-info/dependency_links.txt +0 -0
  34. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli.egg-info/entry_points.txt +0 -0
  35. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli.egg-info/requires.txt +0 -0
  36. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/mini_code_cli.egg-info/top_level.txt +0 -0
  37. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/setup.cfg +0 -0
  38. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/tests/test_e2e_agent_loop.py +0 -0
  39. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/tests/test_subagents.py +0 -0
  40. {mini_code_cli-0.1.1.2 → mini_code_cli-0.1.1.3}/tests/test_tokens.py +0 -0
@@ -0,0 +1,390 @@
1
+ Metadata-Version: 2.4
2
+ Name: mini-code-cli
3
+ Version: 0.1.1.3
4
+ Summary: Mini Coding Agent. Allows you to easily experiment with a small coding agent.
5
+ Author-email: Nikolai Rozanov <nikolai.rozanov@gmail.com>
6
+ License-Expression: AGPL-3.0-only
7
+ Project-URL: Homepage, https://github.com/ai-nikolai/mini-code-cli
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: requests<3,>=2.31
18
+ Dynamic: license-file
19
+
20
+ # mini-code-cli
21
+
22
+ ### A minimal coding agent CLI
23
+
24
+ > No bloat, no dependencies, no tracking, no telemetry...
25
+
26
+ [![CI](https://img.shields.io/github/actions/workflow/status/ai-nikolai/mini-code-cli/ci.yml?branch=main&label=CI&logo=github&style=flat-square)](https://github.com/ai-nikolai/mini-code-cli/actions/workflows/ci.yml)
27
+ [![PyPI](https://img.shields.io/pypi/v/mini-code-cli?style=flat-square&logo=pypi&logoColor=white)](https://pypi.org/project/mini-code-cli/)
28
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/mini-code-cli/)
29
+ [![Stars](https://img.shields.io/github/stars/ai-nikolai/mini-code-cli?style=flat-square&logo=github)](https://github.com/ai-nikolai/mini-code-cli/stargazers)
30
+ [![License: AGPL v3](https://img.shields.io/badge/license-AGPL--3.0-blue?style=flat-square)](LICENSE)
31
+ [![Cite](https://img.shields.io/badge/cite-BibTeX-blueviolet?style=flat-square&logo=latex&logoColor=white)](#citation)
32
+
33
+ **Purpose:** a no-bloat, minimal coding CLI to enjoy experimenting and researching!
34
+
35
+ **Install** · [Quick start](#quick-start) · [Modes &amp; safety](#modes--safety) · [Options](#command-line-options) · [Tools](#available-tools) · [Sub-agents](#sub-agents) · [In-session commands](#in-session-commands) · [Development](#development) · [Docs](#documentation) · [Citation](#citation)
36
+
37
+ > [!TIP]
38
+ > **New here?** `pip install mini-code-cli`, point it at a model (a local server on
39
+ > `localhost:30000`, or `MINI_CODE_API_KEY` + `--url`), and run `mini-code`. Everything else
40
+ > is optional.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip3 install mini-code-cli
46
+ ```
47
+
48
+ - **PyPI:** <https://pypi.org/project/mini-code-cli/>
49
+ - **Python:** `>= 3.10` &nbsp;·&nbsp; **runtime dependency:** `requests` only
50
+ - **From source / contributing:** see [docs/README_DEV.md](docs/README_DEV.md)
51
+
52
+ ## Quick start
53
+
54
+ **1 — Local model** (e.g. vLLM or SGLang on port 30000; see [serving a model locally](#serving-a-model-locally)):
55
+
56
+ ```bash
57
+ mini-code # talks to http://0.0.0.0:30000/v1
58
+ ```
59
+
60
+ **2 — Hosted / custom API:**
61
+
62
+ ```bash
63
+ export MINI_CODE_API_KEY="sk-YOUR_API_KEY"
64
+ mini-code --url "https://api.deepseek.com" --model "deepseek-v4-flash"
65
+ ```
66
+
67
+ **3 — The shortcuts** (plan mode, sub-agents, shell, auto-mode, no permission prompts):
68
+
69
+ ```bash
70
+ mini-code --all # = --plan --enable-subagents --enable-shell --auto-mode --sudo
71
+ mini-code --all-last # ...and continue your last session
72
+ ```
73
+
74
+ **4 — A first prompt:**
75
+
76
+ ```txt
77
+ Write an agent.md file for me that helps an LLM write efficient triton kernels,
78
+ given a reference implementation.
79
+ ```
80
+
81
+ > [!NOTE]
82
+ > **Every flag** is listed once, in [Command-line options](#command-line-options) — or run
83
+ > `mini-code --help`. Modes and their caveats are described in [Modes & safety](#modes--safety).
84
+
85
+ ## Command-line options
86
+
87
+ | Option | Default | What it does |
88
+ | --- | --- | --- |
89
+ | `--url URL` | `http://0.0.0.0:30000` | API host |
90
+ | `--model MODEL` | — | model name |
91
+ | `--api-key KEY` | `$MINI_CODE_API_KEY` | API key for authentication |
92
+ | `--max-tokens N` | — | reply budget; also the output headroom used when deriving the compaction limit |
93
+ | `--temperature T` | `1.0` | sampling temperature |
94
+ | `--system-prompt TEXT` | built-in | replace the system prompt (an `--agent-md` file is appended *after* it) |
95
+ | `--prompt TEXT` | — | initial user prompt |
96
+ | `-a`, `--auto-mode` | off | keep querying the model and running tools until no tool calls remain |
97
+ | `--agent-md FILE` | — | append an `agent.md` file after the system prompt (e.g. `./skill.md`) |
98
+ | `--ask-permission` | off | ask before every tool call (ALLOW-tier shell commands still skip the prompt) |
99
+ | `--sudo` | off | **dangerous:** never ask. DENY-tier commands are still refused |
100
+ | `--allowed-dir DIR` | `./` | sandbox for **file** tools (does not restrict the shell) |
101
+ | `-s`, `--enable-shell` | off | allow shell execution (classifier always applies) |
102
+ | `-g`, `--enable-subagents` | off | expose sub-agents as tools |
103
+ | `--skills` | off | enable the `.mini` skills directory (`skill.md`s, `SKILLS.md`, runnable scripts) |
104
+ | `-p`, `--plan` | off | plan mode (`CURRENT_GOAL.md` / `CURRENT_PLAN.md`, `/plan`) |
105
+ | `--llm-timeout S` | `500` | HTTP timeout for LLM requests |
106
+ | `--tool-timeout S` | `120` | timeout for a single tool call |
107
+ | `--max-context-tokens N` | derived | input-token budget before the history is compacted |
108
+ | `--cache-dir DIR` | `~/.cache/mini/sessions/` | where session histories are stored |
109
+ | `--restore-session PATH` | — | continue a specific previous session |
110
+ | `-l`, `--load-last-session` | off | continue the session recorded in the `.mini` pointer |
111
+ | `--all` | off | `--plan --enable-subagents --enable-shell --auto-mode --sudo` |
112
+ | `--all-last` | off | `--all` + `--load-last-session` |
113
+ | `--verbose` | off | print full detail (default: user/assistant turns, token usage, tool calls only) |
114
+ | `-h`, `--help` | — | show the built-in help |
115
+
116
+ ## Available tools
117
+
118
+ The agent can call a small set of tools, all defined in `mini_code_cli/tools.py`. Every
119
+ parameter a model does not need is defaulted, so omitting it keeps the previous behaviour.
120
+
121
+ | Tool | What it does |
122
+ | --- | --- |
123
+ | `read_file` | read a file; `offset` (1-based line) and `limit` page through large files |
124
+ | `write_file` | write/overwrite; `append` adds to the end, `create_dirs` creates parents |
125
+ | `edit` | replace an **exact** string (`old_string` → `new_string`); refuses a missing or ambiguous match unless `replace_all` |
126
+ | `grep` | regex search a file or directory; `recursive`, `include`, `ignore_case`, `line_numbers`, `max_results` (noise dirs skipped) |
127
+ | `glob` | list files matching a glob (one per line, `**` supported), optionally against a `root` |
128
+ | `web_search` | best-effort lookup: related queries + a short Wikipedia extract. Never raises |
129
+ | `read_website` | fetch raw text/HTML of a URL (bounded by `--tool-timeout`), `max_chars` caps size |
130
+ | `run_shell_command` | run a shell command (needs `--enable-shell`); `cwd`, `timeout`, and stdout/stderr/exit code are all reported |
131
+
132
+ ## Sub-agents
133
+
134
+ `--enable-subagents` lets the main agent **delegate a focused task** to a sub-agent, exposed to
135
+ the model as an extra tool. A sub-agent is a self-contained, narrowed agent: it runs unattended
136
+ in auto-mode, in its own isolated session folder, and hands back a short text result (plus an
137
+ optional `report.md`). It reuses all the existing machinery — tools, the shell-safety
138
+ classifier, the `--allowed-dir` sandbox, compaction and `--plan` — and cannot spawn further
139
+ sub-agents.
140
+
141
+ ```bash
142
+ mini-code --enable-subagents # the available sub-agents then show up in /help
143
+ ```
144
+
145
+ | Sub-agent | What it is for |
146
+ | --- | --- |
147
+ | `research` | read-only web research: searches, reads a few sources, returns a sourced summary. Can only use `web_search` / `read_website` — never the filesystem or shell. Optional `sources` hint (e.g. `"wikipedia.org only"`) |
148
+ | `planner` | drafts or refines a step-by-step plan for a goal; always runs in plan mode |
149
+ | `software_engineer` | writes, edits, refactors, fixes and tests code; can run shell commands (tests, builds) to verify |
150
+ | `generic` | general-purpose helper for anything that fits no narrower agent |
151
+
152
+ > [!NOTE]
153
+ > A sub-agent's `tools`, `enable_shell` and `max_steps` are always **intersected with the
154
+ > parent's** permissions, so it can never do anything the main agent could not. Sub-agents
155
+ > compose with `--plan`: each keeps its own `CURRENT_PLAN.md` / `CURRENT_GOAL.md`.
156
+
157
+ <details>
158
+ <summary><b>Adding your own sub-agent</b></summary>
159
+
160
+ A sub-agent is just a small class — subclass `SubAgent` and register it:
161
+
162
+ ```python
163
+ # my_subagents.py
164
+ from mini_code_cli.subagents import SubAgent, register
165
+
166
+ class SummarizeSubAgent(SubAgent):
167
+ name = "summarize"
168
+ description = "Summarize a piece of text the main agent provides."
169
+ tools = ["read_file", "write_file"] # restrict the nested agent's tools
170
+ enable_shell = False
171
+ max_steps = 5
172
+
173
+ register(SummarizeSubAgent())
174
+ ```
175
+
176
+ Make sure the module is imported (e.g. `import my_subagents`) and it is discovered and exposed
177
+ to the model automatically when `--enable-subagents` is set. See
178
+ `mini_code_cli/subagents/builtin.py` for every knob and
179
+ [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) (sections 2–3) for the design.
180
+
181
+ </details>
182
+
183
+ ## In-session commands
184
+
185
+ | Command | What it does |
186
+ | --- | --- |
187
+ | `/history` | list the conversation (turn number, what happened, where) |
188
+ | `/history <n>` | show the full messages recorded for turn `n` |
189
+ | `/history last` | ...for the most recent turn |
190
+ | `/reset [<n>]` | rewind to the end of turn `n` (`last` works too); drops everything after it |
191
+ | `/clear` | clear the screen (keeps the conversation history) |
192
+ | `/plan` | show the current goal + plan (plan mode only) |
193
+ | `/help` | show this help |
194
+ | `/quit`, `/exit` | save the session and exit |
195
+
196
+ > [!NOTE]
197
+ > `/history` and `/reset` are driven by `summary_history.json`, a flat recorder that maps every
198
+ > turn to the `history_{n}.json` segment and message index it lives in — so no full history is
199
+ > read just to list the conversation.
200
+ >
201
+ > * **`<n>` is validated**: `last`, or a positive 1-based turn number. `0`, negatives and
202
+ > non-numbers are rejected with `No such turn` instead of silently showing a different turn.
203
+ > * **A bare `/reset` never destroys anything**: it reports `Already at turn N` when the
204
+ > transcript already ends at the last recorded turn, and *warns instead of truncating* if
205
+ > there are messages that were never recorded. Only `/reset <n>` / `/reset last` rewind.
206
+ > * **A rewind really sticks**: the target `history_{n}.json` is truncated in place (no backup)
207
+ > and every higher `history_{m}.json` / `usage_{m}.json` segment is deleted, so a restart
208
+ > cannot resurrect the dropped turns. All history files are written atomically.
209
+ > * **Token usage is never rolled back**: a rewind does not un-spend tokens, and the session
210
+ > total is preserved. See
211
+ > [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#rewinding-reset-and-what-it-does-to-the-files)
212
+ > for the files involved.
213
+
214
+ ## Development
215
+
216
+ Linting, the test suite, CI and the release process are documented in
217
+ [**docs/README_DEV.md**](docs/README_DEV.md).
218
+
219
+ ```bash
220
+ python3 scripts/lint.py # lint (standard library only)
221
+ python3 -m unittest discover -s tests -p "test_*.py" # run the test suite
222
+ ```
223
+
224
+ Releases are cut by tagging `main`: pushing a `v*` tag triggers
225
+ [`.github/workflows/release.yml`](.github/workflows/release.yml), which builds the sdist + wheel,
226
+ verifies them with `twine check`, and attaches them to a new GitHub release.
227
+
228
+ ## Documentation
229
+
230
+ Everything beyond this README lives in [`docs/`](docs/):
231
+
232
+ | Document | What it covers |
233
+ | --- | --- |
234
+ | [**docs/README_DEV.md**](docs/README_DEV.md) | developer guide — local setup, repository layout, linting, the test suite, CI and the release / PyPI upload process |
235
+ | [**docs/ARCHITECTURE.md**](docs/ARCHITECTURE.md) | design — module layout, on-disk conversation state (`history_{n}.json`, `summary_history.json`, usage files), the sub-agent framework and how to extend it |
236
+
237
+ This README stays the user-facing entry point (install, CLI flags, tools, sub-agents,
238
+ in-session commands, modes &amp; safety); the two documents above go deeper on the developer
239
+ and design side.
240
+
241
+ ## Modes & safety
242
+
243
+ ### Shell execution — `-s` / `--enable-shell`
244
+
245
+ Everything the agent runs from the shell first passes a **safety classifier** that sorts the
246
+ command into one of three tiers:
247
+
248
+ | Tier | Examples | What happens |
249
+ | --- | --- | --- |
250
+ | **DENY** | `rm -rf /`, `dd of=/dev/...`, `mkfs.*`, fork bombs | refused outright, never run |
251
+ | **ASK** | `rm file`, `sudo ...`, `chmod`, `curl ... \| sh` | requires your confirmation |
252
+ | **ALLOW** | `ls`, `cat`, `git status` | runs without prompting |
253
+
254
+ > [!WARNING]
255
+ > The classifier is **always enforced** — even in auto-mode and with `--sudo` — so the agent
256
+ > cannot run destructive commands unattended. It is a heuristic, not a sandbox: use it with care.
257
+ > `--allowed-dir` sandboxes **file tools**, not the shell.
258
+
259
+ ### Plan mode — `-p` / `--plan`
260
+
261
+ The loop keeps two human-readable tracking files in the session directory: `CURRENT_GOAL.md`
262
+ (your latest instruction) and `CURRENT_PLAN.md` (a step-by-step plan for that goal, marked
263
+ finished when it is done). They are authored by the *loop*, not the agent, live outside
264
+ `--allowed-dir`, and can be inspected any time with `/plan`.
265
+
266
+ > [!NOTE]
267
+ > Plan mode also changes **history compaction**: after the transcript is compacted to a summary,
268
+ > `CURRENT_PLAN.md` and `CURRENT_GOAL.md` are re-injected, so the agent keeps working against the
269
+ > same goal. When the work looks done the plan is reviewed and any outstanding task is re-queued
270
+ > before the agent returns to you.
271
+
272
+ ### Sessions — resume, or start fresh
273
+
274
+ Every session is saved under `--cache-dir` (default `~/.cache/mini/sessions/`).
275
+
276
+ > [!TIP]
277
+ > If a previous session exists but you did **not** ask to resume one (neither
278
+ > `--load-last-session` nor `--restore-session`), the prompt offers **Resume / Start new / Exit**
279
+ > before the conversation begins. Ctrl-C or EOF there is a graceful exit. This is the *asking*
280
+ > variant; `--load-last-session` is the *silent* one.
281
+
282
+
283
+ | Flag | Behaviour |
284
+ | --- | --- |
285
+ | `--restore-session PATH` | continue that exact session (a session directory or a bare session id under `--cache-dir`) |
286
+ | `-l`, `--load-last-session` | continue the session recorded in the `.mini/last_session.json` pointer, without asking |
287
+
288
+ `--load-last-session` continues that session **in place** (same id, same folder, same
289
+ messages), so the conversation you were in is the one you get back; an explicit
290
+ `--restore-session` wins if both are given. The "most recent" session comes from a single
291
+ source — the tiny `.mini/last_session.json` pointer written when a session starts or is
292
+ restored inside `--allowed-dir` (deliberately no timestamp fallback: with no pointer, a fresh
293
+ session starts). That pointer lives in the same `.mini` directory as `--skills`, but as its
294
+ own dedicated file, so the two never clash.
295
+
296
+ A session folder is restorable only if it has what this version writes: a valid
297
+ `summary_history.json` recorder, plus the `history_{n}.json` segment its last record names
298
+ (usage files are optional). Anything else — a folder from an older release, a hand-assembled
299
+ transcript, a recorder that does not validate, a missing segment — is **refused with a note
300
+ saying why**, and a fresh session is started instead. Nothing is repaired, guessed at, or
301
+ written into the refused folder.
302
+
303
+ ### Context compaction — `--max-context-tokens`
304
+
305
+ Compaction is decided by **tokens only**, never by message count: a few large tool outputs can
306
+ fill a window that hundreds of short turns would not. The next request is projected from the
307
+ server-reported `prompt_tokens`, plus an estimate of what was appended since, plus the output
308
+ reserve (`--max-tokens`). Unset (default) it is derived from the model's context window minus the
309
+ output reserve and a safety margin. Lower it to compact sooner, raise it to compact later — there
310
+ is no message-count cap to configure.
311
+
312
+ Compaction replaces the older turns with a summary, but it always carries the **turn in flight**
313
+ across the rollover: the message you just typed (or, mid-turn, the tool call the model is
314
+ waiting on) is kept verbatim after the summary. So a compaction that fires just as you send an
315
+ instruction cannot swallow it — the model still answers what you asked, and the turn stays in
316
+ the session file, in `/history` and in a later restore.
317
+
318
+ Because a rollover leaves no trace in the transcript (the old turns really are gone), the
319
+ console says in plain terms what happened — all of it without `--verbose`:
320
+
321
+ ```
322
+ [!] Compacting the conversation: 42 messages -> a summary (51000 tokens in, 8192 reserved for the reply > 52000 budget). The current turn (1 message) is kept verbatim.
323
+ [!] Summary preview: The user asked for a token-based compaction trigger, then fixed ...
324
+ [!] Compacted history_0.json (42 messages) into a summary; history_1.json now starts with 3 messages (summary + 1 kept message).
325
+ ```
326
+
327
+ So you can see *what* was folded away (a preview of the summary itself), *how much* (the
328
+ message count of the segment that was closed), *where it went* (`history_0.json` closed,
329
+ `history_1.json` started) and that nothing you just typed was lost. If the summariser answers
330
+ with an empty summary twice, or the call fails, compaction is abandoned and the note says so
331
+ explicitly — "nothing was lost, but the next request may still be too large" — with the
332
+ transcript and the segment left untouched.
333
+
334
+ ### Exiting — the good-bye box
335
+
336
+ Quitting (`/quit`, `exit`, or EOF on stdin) saves the session and then prints a short yellow
337
+ box: the version, a sign-off, and what the session left behind — session id, folder, the
338
+ history segments on disk, how many turns were recorded, and the token / call totals.
339
+
340
+ ```
341
+ ╔══════════ MiniCodeCLI v0.1.1.2 - goodbye ═════════════════════╗
342
+ ║ Goodbye, and thanks for using MiniCodeCLI! ║
343
+ ║ ║
344
+ ║ session id : 1792666273983619-36ca3c2e ║
345
+ ║ saved under : ~/.cache/mini/sessions/1792…-36ca3c2e/ ║
346
+ ║ history : history_0.json (segment 0), 5 messages ║
347
+ ║ conversation : 2 recorded turns ║
348
+ ║ tokens : 20 in / 10 out (30 total) ║
349
+ ║ calls : 2 LLM, 0 tool ║
350
+ ╚═══════════════════════════════════════════════════════════════╝
351
+ ```
352
+
353
+ It is printed exactly once, and only on a normal exit: a Ctrl-C or an internal error already
354
+ says what went wrong in red, so it does not get a cheerful summary box on top. With `--verbose`
355
+ the old per-field `Token usage (this session)` block is printed above it as well.
356
+
357
+
358
+ ## Serving a model locally
359
+
360
+ Tested with vLLM v0.30 (tool calling enabled):
361
+
362
+ ```bash
363
+ vllm serve ornith-ai/Ornith-1.5-397B-FP8 \
364
+ --tensor-parallel-size 4 \
365
+ --host 0.0.0.0 \
366
+ --port 30000 \
367
+ --enable-auto-tool-choice \
368
+ --tool-call-parser qwen3_xml \
369
+ --reasoning-parser qwen3 \
370
+ --trust-remote-code
371
+ ```
372
+
373
+ ## Citation
374
+
375
+ If you use `mini-code-cli` in your research or writing, please cite it:
376
+
377
+ ```bibtex
378
+ @misc{mini-code-cli,
379
+ author = {Rozanov, Nikolai},
380
+ title = {mini-code-cli: A minimal, dependency-light coding agent},
381
+ year = {2026},
382
+ version = {0.1.1.2},
383
+ howpublished = {\url{https://github.com/ai-nikolai/mini-code-cli}},
384
+ note = {A no-bloat, dependency-light agent CLI for experimenting.}
385
+ }
386
+ ```
387
+
388
+ ## License
389
+
390
+ [AGPL-3.0](LICENSE) — (C) Nikolai Rozanov, 2026 – present.
@@ -11,7 +11,7 @@
11
11
  [![License: AGPL v3](https://img.shields.io/badge/license-AGPL--3.0-blue?style=flat-square)](LICENSE)
12
12
  [![Cite](https://img.shields.io/badge/cite-BibTeX-blueviolet?style=flat-square&logo=latex&logoColor=white)](#citation)
13
13
 
14
- **Purpose:** a no-bloat, minimal coding CLI to enjoy experimenting and researching.
14
+ **Purpose:** a no-bloat, minimal coding CLI to enjoy experimenting and researching!
15
15
 
16
16
  **Install** · [Quick start](#quick-start) · [Modes &amp; safety](#modes--safety) · [Options](#command-line-options) · [Tools](#available-tools) · [Sub-agents](#sub-agents) · [In-session commands](#in-session-commands) · [Development](#development) · [Docs](#documentation) · [Citation](#citation)
17
17
 
@@ -312,6 +312,29 @@ with an empty summary twice, or the call fails, compaction is abandoned and the
312
312
  explicitly — "nothing was lost, but the next request may still be too large" — with the
313
313
  transcript and the segment left untouched.
314
314
 
315
+ ### Exiting — the good-bye box
316
+
317
+ Quitting (`/quit`, `exit`, or EOF on stdin) saves the session and then prints a short yellow
318
+ box: the version, a sign-off, and what the session left behind — session id, folder, the
319
+ history segments on disk, how many turns were recorded, and the token / call totals.
320
+
321
+ ```
322
+ ╔══════════ MiniCodeCLI v0.1.1.2 - goodbye ═════════════════════╗
323
+ ║ Goodbye, and thanks for using MiniCodeCLI! ║
324
+ ║ ║
325
+ ║ session id : 1792666273983619-36ca3c2e ║
326
+ ║ saved under : ~/.cache/mini/sessions/1792…-36ca3c2e/ ║
327
+ ║ history : history_0.json (segment 0), 5 messages ║
328
+ ║ conversation : 2 recorded turns ║
329
+ ║ tokens : 20 in / 10 out (30 total) ║
330
+ ║ calls : 2 LLM, 0 tool ║
331
+ ╚═══════════════════════════════════════════════════════════════╝
332
+ ```
333
+
334
+ It is printed exactly once, and only on a normal exit: a Ctrl-C or an internal error already
335
+ says what went wrong in red, so it does not get a cheerful summary box on top. With `--verbose`
336
+ the old per-field `Token usage (this session)` block is printed above it as well.
337
+
315
338
 
316
339
  ## Serving a model locally
317
340
 
@@ -337,9 +360,9 @@ If you use `mini-code-cli` in your research or writing, please cite it:
337
360
  author = {Rozanov, Nikolai},
338
361
  title = {mini-code-cli: A minimal, dependency-light coding agent},
339
362
  year = {2026},
340
- version = {0.1.1.0},
363
+ version = {0.1.1.2},
341
364
  howpublished = {\url{https://github.com/ai-nikolai/mini-code-cli}},
342
- note = {A no-bloat, dependency-free agent CLI for experimenting.}
365
+ note = {A no-bloat, dependency-light agent CLI for experimenting.}
343
366
  }
344
367
  ```
345
368
 
@@ -0,0 +1,59 @@
1
+ """mini_code_cli -- package metadata (the version shown in the welcome banner).
2
+
3
+ The version is read from ``pyproject.toml`` first: that file is the source of
4
+ truth when running from a checkout, and the *installed* metadata is often stale
5
+ (a plain ``pip install -e .`` keeps the version it was installed at, so bumping
6
+ ``pyproject.toml`` would leave the banner reporting an old number). If the file
7
+ cannot be read, fall back to the installed distribution's metadata and finally
8
+ to ``"unknown"`` so importing the package can never fail over a version lookup.
9
+ """
10
+
11
+ __all__ = ["__version__"]
12
+
13
+
14
+ def _version_from_file(path):
15
+ """Return the ``[project] version`` in ``path``.
16
+
17
+ ``tomllib`` only exists from Python 3.11, and the scan below is not a
18
+ nicety for 3.10: without it this function used to skip the file entirely on
19
+ that interpreter and report the *installed* metadata instead, so a checkout
20
+ with a stale editable install showed the wrong version in the banner (found
21
+ on the 3.10 CI leg). Both paths read the same key, and the section tracking
22
+ matters: a ``version`` key in some other table must not win.
23
+ """
24
+ try:
25
+ import tomllib # Python >= 3.11
26
+ except ImportError: # pragma: no cover - exercised on the 3.10 CI leg
27
+ tomllib = None
28
+ if tomllib is not None:
29
+ with open(path, "rb") as f:
30
+ return str(tomllib.load(f)["project"]["version"])
31
+ section = None
32
+ with open(path, encoding="utf-8") as f:
33
+ for line in f:
34
+ stripped = line.strip()
35
+ if stripped.startswith("["):
36
+ section = stripped
37
+ elif section == "[project]" and stripped.startswith("version"):
38
+ return stripped.split("=", 1)[1].strip().strip('"')
39
+ raise KeyError(f"no version in the [project] table of {path}")
40
+
41
+
42
+ def _read_version():
43
+ """Best-effort package version: pyproject.toml, then installed metadata."""
44
+ try:
45
+ from pathlib import Path
46
+
47
+ pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
48
+ return _version_from_file(pyproject)
49
+ except Exception:
50
+ pass
51
+ try:
52
+ from importlib.metadata import version as _dist_version
53
+
54
+ return _dist_version("mini-code-cli")
55
+ except Exception:
56
+ return "unknown"
57
+
58
+
59
+ __version__ = _read_version()
@@ -0,0 +1,12 @@
1
+ """Allow ``python -m mini_code_cli`` to start the agent.
2
+
3
+ The console script ``mini-code`` (see ``[project.scripts]``) is the normal entry
4
+ point; this module exists so the package also runs straight from a checkout or
5
+ an installed wheel without the script on ``PATH`` -- which is what a reader
6
+ tries first after ``pip install -e .``.
7
+ """
8
+
9
+ from .agent_loop import main
10
+
11
+ if __name__ == "__main__":
12
+ raise SystemExit(main())
@@ -31,7 +31,7 @@ from . import prompts
31
31
  from . import shell_safety_utils
32
32
  from . import tools
33
33
  from . import utils
34
- from .utils import oneline
34
+ from .utils import oneline, plural
35
35
  from . import ui
36
36
  from . import subagents
37
37
  from . import skills
@@ -118,7 +118,12 @@ def _safe_int(value, default=0):
118
118
  # ---------------------------------------------------------------------------
119
119
 
120
120
  def parse_args():
121
- parser = argparse.ArgumentParser(description="Agent loop for interacting with a language model")
121
+ # ``prog`` is pinned so ``--help`` reads the same whether the agent was
122
+ # started through the ``mini-code`` script, ``python -m mini_code_cli`` or
123
+ # a test harness (without it, the usage line said ``__main__.py``).
124
+ parser = argparse.ArgumentParser(
125
+ prog="mini-code",
126
+ description="Agent loop for interacting with a language model")
122
127
  # MODEL / LLM related
123
128
  parser.add_argument("--url", type=str, default=f"{DEFAULT_MODEL_URL}", help=f"API host, default={DEFAULT_MODEL_URL}")
124
129
  parser.add_argument("--max-tokens", type=int, help="Maximum tokens for a single LLM response (the reply budget), default=None. Also used as the output headroom reserved when deriving the compaction limit from the model's context window.")
@@ -298,11 +303,6 @@ def _in_flight_tail(messages):
298
303
  return []
299
304
 
300
305
 
301
- def _plural(count, noun, suffix="s"):
302
- """``"1 message"`` / ``"4 messages"`` -- for messages the user reads."""
303
- return f"{count} {noun}" + ("" if count == 1 else suffix)
304
-
305
-
306
306
  def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats,
307
307
  history_index=0, token_limit=None, reserved_output_tokens=0,
308
308
  current_tokens=None, plan_hook=None, console=None):
@@ -358,12 +358,12 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
358
358
  # stake; a compaction note that stays silent about that is exactly what makes
359
359
  # the rollover look like it might have eaten the message.
360
360
  tail = _in_flight_tail(messages)
361
- kept_note = (f" The current turn ({_plural(len(tail), 'message')}) is kept "
361
+ kept_note = (f" The current turn ({plural(len(tail), 'message')}) is kept "
362
362
  f"verbatim." if tail else "")
363
363
 
364
364
  try:
365
365
  console.note(
366
- f"Compacting the conversation: {_plural(len(messages), 'message')} "
366
+ f"Compacting the conversation: {plural(len(messages), 'message')} "
367
367
  f"-> a summary ({reason}).{kept_note}", "warn")
368
368
  summary_prompt = prompts.compaction_prompt(messages)
369
369
 
@@ -404,7 +404,7 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
404
404
  # may still be too large.
405
405
  console.note(
406
406
  f"Could not compact history: the summariser returned an empty "
407
- f"summary twice. Keeping all {_plural(len(messages), 'message')} "
407
+ f"summary twice. Keeping all {plural(len(messages), 'message')} "
408
408
  f"and history_{history_index}.json as they are -- nothing was "
409
409
  f"lost, but the next request may still be too large "
410
410
  f"({reason}).", "warn")
@@ -449,7 +449,7 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
449
449
  # and *where it went* (which segment file was closed and which was
450
450
  # started).
451
451
  console.note(f"Summary preview: {utils.oneline(summary, 120)}", "warn")
452
- kept = (f"summary + {_plural(len(tail), 'kept message')}" if tail
452
+ kept = (f"summary + {plural(len(tail), 'kept message')}" if tail
453
453
  else "summary only")
454
454
  # Phrased as "old segment -> new segment" rather than "N messages -> M
455
455
  # messages": the replacement can be *longer* than what it replaced (a
@@ -457,9 +457,9 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
457
457
  # counts that sometimes grows reads like a bug.
458
458
  console.note(
459
459
  f"Compacted history_{history_index}.json "
460
- f"({_plural(len(messages), 'message')}) into a summary; "
460
+ f"({plural(len(messages), 'message')}) into a summary; "
461
461
  f"history_{history_index + 1}.json now starts with "
462
- f"{_plural(len(compacted), 'message')} ({kept}).", "warn")
462
+ f"{plural(len(compacted), 'message')} ({kept}).", "warn")
463
463
  # A new segment (history_index + 1) is started on compaction: the
464
464
  # compacted messages are its snapshot, and every later turn appends
465
465
  # to it. The caller records a single high-level 'summary' turn for
@@ -468,7 +468,7 @@ def maybe_compact(messages, server_url, model, api_key, llm_timeout, token_stats
468
468
  except Exception as e:
469
469
  console.note(
470
470
  f"Failed to compact history: {e}. Keeping all "
471
- f"{_plural(len(messages), 'message')} unchanged.", "error")
471
+ f"{plural(len(messages), 'message')} unchanged.", "error")
472
472
 
473
473
  return messages, False, history_index
474
474
 
@@ -1960,7 +1960,9 @@ class AgentSession:
1960
1960
  # just the turns. In verbose mode everything is printed permanently.
1961
1961
  transient = not self.console.verbose
1962
1962
  func = self.tools_dict[name]["function"]
1963
- result = call_function_with_timeout(func, tool_params, name, timeout_seconds=self.args.tool_timeout)
1963
+ result = call_function_with_timeout(func, tool_params, name,
1964
+ timeout_seconds=self.args.tool_timeout,
1965
+ console=self.console)
1964
1966
 
1965
1967
  self.console.tool_call_display(name, tool_params, result, transient=transient)
1966
1968
  self._append_tool_result(call_id, str(result))
@@ -2105,8 +2107,8 @@ class AgentSession:
2105
2107
  ("session id", self.unique_id),
2106
2108
  ("saved under", f"{self.cache_dir}/{self.unique_id}/"),
2107
2109
  ("history", f"{where} (segment {self.history_index}), "
2108
- f"{_plural(len(self.messages), 'message')}"),
2109
- ("conversation", _plural(turns, "recorded turn")),
2110
+ f"{plural(len(self.messages), 'message')}"),
2111
+ ("conversation", plural(turns, "recorded turn")),
2110
2112
  ("tokens", f"{count('prompt_tokens'):,} in / "
2111
2113
  f"{count('completion_tokens'):,} out "
2112
2114
  f"({count('total_tokens'):,} total)"),
@@ -2176,7 +2178,7 @@ class AgentSession:
2176
2178
  save_history(self.messages, self.cache_dir, self.unique_id, self.history_index)
2177
2179
  self._record_turn(
2178
2180
  f"summary: history_{new_index - 1}.json "
2179
- f"({_plural(len(closing_messages), 'message')}) compacted into a "
2181
+ f"({plural(len(closing_messages), 'message')}) compacted into a "
2180
2182
  f"summary; history_{self.history_index}.json started",
2181
2183
  self.messages)
2182
2184