mnml-agent 0.2.0__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 (72) hide show
  1. mnml_agent-0.2.0/.github/workflows/ci.yml +24 -0
  2. mnml_agent-0.2.0/.github/workflows/release.yml +33 -0
  3. mnml_agent-0.2.0/.gitignore +5 -0
  4. mnml_agent-0.2.0/.python-version +1 -0
  5. mnml_agent-0.2.0/CHANGELOG.md +38 -0
  6. mnml_agent-0.2.0/LICENSE +21 -0
  7. mnml_agent-0.2.0/PKG-INFO +367 -0
  8. mnml_agent-0.2.0/README.md +341 -0
  9. mnml_agent-0.2.0/assets/banner.svg +5 -0
  10. mnml_agent-0.2.0/docs/extensions.md +310 -0
  11. mnml_agent-0.2.0/docs/settings.md +139 -0
  12. mnml_agent-0.2.0/extensions/compact.py +103 -0
  13. mnml_agent-0.2.0/extensions/example.py +53 -0
  14. mnml_agent-0.2.0/extensions/tools/subagent.py +47 -0
  15. mnml_agent-0.2.0/extensions/tools/web_search.py +84 -0
  16. mnml_agent-0.2.0/pyproject.toml +56 -0
  17. mnml_agent-0.2.0/src/minimal/__init__.py +16 -0
  18. mnml_agent-0.2.0/src/minimal/__main__.py +5 -0
  19. mnml_agent-0.2.0/src/minimal/api.py +26 -0
  20. mnml_agent-0.2.0/src/minimal/cli.py +213 -0
  21. mnml_agent-0.2.0/src/minimal/config.py +448 -0
  22. mnml_agent-0.2.0/src/minimal/core/__init__.py +1 -0
  23. mnml_agent-0.2.0/src/minimal/core/agent.py +211 -0
  24. mnml_agent-0.2.0/src/minimal/core/app.py +479 -0
  25. mnml_agent-0.2.0/src/minimal/core/context.py +177 -0
  26. mnml_agent-0.2.0/src/minimal/core/permissions.py +98 -0
  27. mnml_agent-0.2.0/src/minimal/core/prompt.md +13 -0
  28. mnml_agent-0.2.0/src/minimal/core/session.py +106 -0
  29. mnml_agent-0.2.0/src/minimal/extensions/__init__.py +1 -0
  30. mnml_agent-0.2.0/src/minimal/extensions/audit.py +251 -0
  31. mnml_agent-0.2.0/src/minimal/extensions/facade.py +271 -0
  32. mnml_agent-0.2.0/src/minimal/extensions/loader.py +409 -0
  33. mnml_agent-0.2.0/src/minimal/extensions/logo.py +124 -0
  34. mnml_agent-0.2.0/src/minimal/extensions/marketplace.py +729 -0
  35. mnml_agent-0.2.0/src/minimal/extensions/metadata.py +101 -0
  36. mnml_agent-0.2.0/src/minimal/extensions/registry.py +487 -0
  37. mnml_agent-0.2.0/src/minimal/providers/__init__.py +29 -0
  38. mnml_agent-0.2.0/src/minimal/providers/anthropic.py +89 -0
  39. mnml_agent-0.2.0/src/minimal/providers/base.py +135 -0
  40. mnml_agent-0.2.0/src/minimal/providers/claude.py +38 -0
  41. mnml_agent-0.2.0/src/minimal/providers/claude_auth.py +236 -0
  42. mnml_agent-0.2.0/src/minimal/providers/gemini.py +256 -0
  43. mnml_agent-0.2.0/src/minimal/providers/openai.py +181 -0
  44. mnml_agent-0.2.0/src/minimal/tools/__init__.py +1 -0
  45. mnml_agent-0.2.0/src/minimal/tools/builtin.py +23 -0
  46. mnml_agent-0.2.0/src/minimal/tools/common.py +40 -0
  47. mnml_agent-0.2.0/src/minimal/tools/files.py +165 -0
  48. mnml_agent-0.2.0/src/minimal/tools/reload.py +27 -0
  49. mnml_agent-0.2.0/src/minimal/tools/search.py +226 -0
  50. mnml_agent-0.2.0/src/minimal/tools/shell.py +131 -0
  51. mnml_agent-0.2.0/src/minimal/ui/__init__.py +1 -0
  52. mnml_agent-0.2.0/src/minimal/ui/commands.py +440 -0
  53. mnml_agent-0.2.0/src/minimal/ui/setup.py +125 -0
  54. mnml_agent-0.2.0/src/minimal/ui/tui/__init__.py +1 -0
  55. mnml_agent-0.2.0/src/minimal/ui/tui/app.py +556 -0
  56. mnml_agent-0.2.0/src/minimal/ui/tui/frontend.py +119 -0
  57. mnml_agent-0.2.0/src/minimal/ui/tui/screens.py +203 -0
  58. mnml_agent-0.2.0/src/minimal/ui/tui/theme.py +174 -0
  59. mnml_agent-0.2.0/src/minimal/ui/tui/widgets.py +353 -0
  60. mnml_agent-0.2.0/tests/test_bundled_extensions.py +272 -0
  61. mnml_agent-0.2.0/tests/test_claude_auth.py +85 -0
  62. mnml_agent-0.2.0/tests/test_config.py +151 -0
  63. mnml_agent-0.2.0/tests/test_extension_api.py +236 -0
  64. mnml_agent-0.2.0/tests/test_logo.py +45 -0
  65. mnml_agent-0.2.0/tests/test_marketplace.py +401 -0
  66. mnml_agent-0.2.0/tests/test_permissions.py +114 -0
  67. mnml_agent-0.2.0/tests/test_providers.py +234 -0
  68. mnml_agent-0.2.0/tests/test_security.py +562 -0
  69. mnml_agent-0.2.0/tests/test_settings.py +210 -0
  70. mnml_agent-0.2.0/tests/test_smoke.py +278 -0
  71. mnml_agent-0.2.0/tests/test_tui.py +398 -0
  72. mnml_agent-0.2.0/uv.lock +1370 -0
@@ -0,0 +1,24 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ permissions:
7
+ contents: read
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ with:
18
+ persist-credentials: false
19
+ - uses: astral-sh/setup-uv@v10.3.3
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - run: uv sync --locked
23
+ - run: uv run ruff check
24
+ - run: uv run python -m unittest discover -s tests
@@ -0,0 +1,33 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+ permissions:
6
+ contents: read
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v7
12
+ with:
13
+ persist-credentials: false
14
+ - uses: astral-sh/setup-uv@v10.3.3
15
+ - run: uv build
16
+ - name: Tag matches minimal.__version__
17
+ run: test "${GITHUB_REF_NAME#v}" = "$(uv run python -c 'import minimal; print(minimal.__version__)')"
18
+ - uses: actions/upload-artifact@v4
19
+ with:
20
+ name: dist
21
+ path: dist/
22
+ publish:
23
+ needs: build
24
+ runs-on: ubuntu-latest
25
+ environment: pypi
26
+ permissions:
27
+ id-token: write
28
+ steps:
29
+ - uses: actions/download-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ dist/
4
+ ._*
5
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Breaking
6
+
7
+ - **Stable extension API.** `register(minimal)` now receives an `Extension` facade instead of the internal
8
+ `Registry`, and every `ctx` (tools, commands, events, widgets, status segments) is a `Context` instead of the
9
+ internal `App`. `minimal.api` is the only import surface; `Registry`, `run_agent`, `open_session`, `new_id`,
10
+ `ID_RE`, `SESSIONS_DIR`, `build_system_prompt`, `tool_specs`, `raise_if_cancelled`, `reload_summary` and
11
+ `BUILTIN_COMMANDS` were removed from it. See [docs/extensions.md](docs/extensions.md) for the API and a
12
+ migration table from 0.1.
13
+ - `ctx.cancelled` is a `bool`; `ctx.check_cancelled()` raises `TurnInterrupted`.
14
+ - Event payloads carry `ctx` (`tool_call`, `tool_result`) and `nested` (`turn_end`); `on()` rejects unknown events.
15
+ - `tool_result` handlers see a command's whole output; the agent loop clips what the model sees afterwards.
16
+ - The PyPI distribution is now **`mnml-agent`** (`uv tool install mnml-agent`); the command and the
17
+ import package stay `minimal`.
18
+
19
+ ### Added
20
+
21
+ - **Layered settings**: `~/.minimal/settings.json` < `.minimal/settings.json` < `.minimal/settings.local.json`,
22
+ then `$MINIMAL_PROVIDER`/`$MINIMAL_MODEL`, then the command line. Provider, model and theme; the base prompt,
23
+ appended prompt text and instruction files; permissions; extension enable/disable and per-extension settings;
24
+ marketplaces. `/settings` shows the files and where each value comes from. See [docs/settings.md](docs/settings.md).
25
+ - **Migration**: on first start, `config.json`, `theme.json` and `marketplaces.json` are moved into
26
+ `~/.minimal/settings.json`; `prompt.md` is kept and referenced as `prompt.base`.
27
+ - **Permissions**: `permissions.allow` rules (`bash(git status*)`, `write(src/**)`) run matching tool calls without
28
+ asking; `permissions.deny` rules block them, even with `-y`. A project's allow rules, disabled extensions and
29
+ extension settings apply only once the project is trusted (`/trust`), which now reviews them alongside its extensions.
30
+ - **Extension metadata and packages**: `__version__` and `__requires__ = ">=0.2"`; an extension may be a folder
31
+ with an `__init__.py` and helper modules. `extensions.disabled` turns extensions off by id.
32
+ - `Extension` decorators (`@minimal.tool`, `@minimal.command`, `@minimal.on`), `minimal.declare_settings(...)`,
33
+ and `Context` helpers: `ctx.ui`, `ctx.complete`, `ctx.run_agent`, `ctx.new_session`, `ctx.sessions`,
34
+ `ctx.read_session`, `ctx.reload`.
35
+ - **`minimal ext check <path>...`** validates extension files, packages or a whole marketplace checkout; the
36
+ marketplace runs it in CI. The model's `ext_search` searches every configured marketplace; `ext_install` and
37
+ `ext_publish` take an optional `marketplace`.
38
+ - Releases are published to PyPI from `v*` tags.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RafaSG13
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,367 @@
1
+ Metadata-Version: 2.5
2
+ Name: mnml-agent
3
+ Version: 0.2.0
4
+ Summary: Minimal, extensible coding-agent harness
5
+ Project-URL: Homepage, https://github.com/RafaSG13/Minimal
6
+ Project-URL: Repository, https://github.com/RafaSG13/Minimal
7
+ Project-URL: Issues, https://github.com/RafaSG13/Minimal/issues
8
+ Project-URL: Marketplace, https://github.com/RafaSG13/minimal-marketplace
9
+ Author-email: RafaSG13 <rafasrrg13@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,claude,cli,gemini,llm,tui
13
+ Classifier: Environment :: Console
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: anthropic>=2.3.0
21
+ Requires-Dist: google-genai>=2.34.0
22
+ Requires-Dist: openai>=3.49.0
23
+ Requires-Dist: questionary>=2
24
+ Requires-Dist: textual>=8.2
25
+ Description-Content-Type: text/markdown
26
+
27
+ <p align="center">
28
+ <img src="assets/banner.svg" alt="minimal" width="100%">
29
+ </p>
30
+
31
+ <h1 align="center">minimal</h1>
32
+
33
+ <p align="center">
34
+ <a href="https://github.com/RafaSG13/Minimal">Repository</a> · <a href="https://github.com/RafaSG13/minimal-marketplace">Marketplace</a> · <a href="https://github.com/RafaSG13/Minimal/issues">Issues</a>
35
+ </p>
36
+
37
+ <p align="center">
38
+ <img src="https://img.shields.io/badge/version-0.2.0-6f86f4?style=for-the-badge" alt="Version 0.2.0">
39
+ <img src="https://img.shields.io/badge/Python-3.11%2B-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.11+">
40
+ <img src="https://img.shields.io/badge/UI-Textual-b665ea?style=for-the-badge" alt="Textual UI">
41
+ <img src="https://img.shields.io/badge/Claude-D97757?style=for-the-badge&logo=anthropic&logoColor=white" alt="Claude">
42
+ <img src="https://img.shields.io/badge/Gemini-4285F4?style=for-the-badge&logo=googlegemini&logoColor=white" alt="Gemini">
43
+ <a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/badge/managed%20with-uv-DE5FE9?style=for-the-badge" alt="Managed with uv"></a>
44
+ </p>
45
+
46
+ **A minimal, extensible coding-agent harness for the terminal.** minimal is a small agent loop — model → tools → model — wrapped in a Textual terminal UI, written so that every piece can be read in one sitting and replaced from a single Python file. Tools, slash commands, providers, hooks, widgets, themes and even the header logo are registered by extensions, and the agent can write, install and hot-reload its own extensions mid-conversation.
47
+
48
+ Use Claude through an API key or a Pro/Max subscription, Gemini through the free tier, or any OpenAI-compatible server (OpenAI, OpenRouter, Ollama, LM Studio) — and switch providers mid-conversation without losing history. More model integrations are one class away.
49
+
50
+ <table>
51
+ <tr><td><b>A real terminal interface</b></td><td>Textual UI with a welcome header, Markdown replies, tool-call blocks coloured by state (running, done, failed), live diffs for edits, a confirm dialog, a command palette (<code>Ctrl+P</code>), <code>Esc</code> to interrupt a turn, and a status line with model, provider, directory, git branch and context size.</td></tr>
52
+ <tr><td><b>Provider-neutral history</b></td><td>One canonical message format shaped like the Anthropic Messages API. Each provider converts it on every request, so any session can be resumed with any provider, and <code>/provider</code> switches mid-conversation.</td></tr>
53
+ <tr><td><b>Extensions all the way down</b></td><td>A Python file with <code>register(minimal)</code> can add tools, commands, providers, <code>tool_call</code>/<code>tool_result</code>/<code>turn_end</code> hooks, system-prompt notes, sidebar/top/bottom widgets, colour themes and a pixel-art logo.</td></tr>
54
+ <tr><td><b>Self-extending, hot-reloaded</b></td><td>The model knows the extension API. It writes an extension, calls <code>reload_extensions</code>, and the new tools are offered on its very next request — in the middle of a turn, without a restart. Load errors go back to the model so it can fix them.</td></tr>
55
+ <tr><td><b>Git-based marketplace</b></td><td>Browse, install, update, remove and publish extensions from any git repository with <code>/ext</code>. Publishing opens a pull request; the default branch is never pushed to.</td></tr>
56
+ <tr><td><b>Safe by default</b></td><td>Asks before <code>write</code>, <code>edit</code>, <code>bash</code>, <code>reload_extensions</code>, <code>ext_install</code> and <code>ext_publish</code>. Permission rules in settings pre-approve or forbid calls; hooks can veto any tool call or rewrite any result. Saved keys are stored with mode <code>0600</code>.</td></tr>
57
+ <tr><td><b>Small and readable</b></td><td>Five runtime dependencies. Every module has a docstring describing its role — start at <code>src/minimal/__init__.py</code>.</td></tr>
58
+ </table>
59
+
60
+ ---
61
+
62
+ ## Quick Install
63
+
64
+ Requires Python 3.11+ and [uv](https://github.com/astral-sh/uv).
65
+
66
+ ### Install the `minimal` command
67
+
68
+ The package is published on PyPI as `mnml-agent`; the command it installs is `minimal`.
69
+
70
+ ```bash
71
+ uv tool install mnml-agent
72
+ minimal
73
+ ```
74
+
75
+ ### Run from source
76
+
77
+ ```bash
78
+ git clone https://github.com/RafaSG13/Minimal.git
79
+ cd Minimal
80
+ uv sync
81
+ uv run minimal # first run: setup, then the terminal UI
82
+ uv tool install . # optional: install the `minimal` command globally
83
+ ```
84
+
85
+ On the first start, setup asks (arrow keys) for the default provider, its credentials (a browser login for `claude`; for an API-key provider, the key from the environment or a new one) and the model, then saves the choice in `~/.minimal/settings.json`. It runs again whenever the saved provider can no longer start (for example after `minimal --logout`), and on demand with `minimal --setup` or `/setup`. Without a terminal (a pipe, CI) there is no setup: save a choice first or pass `--provider`.
86
+
87
+ ---
88
+
89
+ ## Getting Started
90
+
91
+ ```bash
92
+ minimal # interactive terminal UI
93
+ minimal -c # resume the last session
94
+ minimal -s <id> # open (or create) a specific session
95
+ minimal -e extensions/example.py # load an extension file or folder (repeatable)
96
+ minimal --setup # change the default provider, login/key or model
97
+ minimal --login # sign in with a Claude Pro/Max subscription
98
+ minimal --logout # forget the Claude login
99
+ minimal -y # skip tool confirmations
100
+ minimal --trust-project # apply ./.minimal extensions and trust-gated settings without /trust
101
+ minimal ext check <path>... # validate extension files, package folders or a marketplace checkout
102
+ ```
103
+
104
+ If an `AGENTS.md` exists in the current directory, it is added to the system prompt (`prompt.files` in [settings](#settings) changes which files). minimal needs a terminal, and a turn can only be started from inside its interface: there is no one-shot `minimal -p "prompt"` mode that runs the agent straight from the shell.
105
+
106
+ **Confirmations.** `write`, `edit`, `bash`, `reload_extensions`, `ext_install` and `ext_publish` ask before running, and so do `read`, `grep` and `glob` for paths outside the working directory, minimal's own source and `~/.minimal/extensions` (a symlink counts where it leads). `-y` skips these questions. `permissions.allow` rules in [settings](#settings) skip them for matching calls (`bash(git status*)`, `write(src/**)`), and `permissions.deny` rules block matching calls even with `-y`. A `tool_call` handler that raises blocks the call. Session files are private to your user (`~/.minimal/sessions`, mode 0700).
107
+
108
+ ### Providers
109
+
110
+ | Provider | `--provider` | Credentials | Default model |
111
+ | ------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------- | ------------------- |
112
+ | Anthropic | `anthropic` | `ANTHROPIC_API_KEY` or a key saved in setup | `claude-sonnet-5-5` |
113
+ | Claude subscription | `claude` | Browser login in setup, `minimal --login` or `/login` | `claude-sonnet-5-5` |
114
+ | Google Gemini | `gemini` | `GEMINI_API_KEY` / `GOOGLE_API_KEY` or a key saved in setup ([get one](https://aistudio.google.com/docs/api-key), free tier) | `gemini-3.8-flash` |
115
+ | OpenAI-compatible | `openai` | `OPENAI_API_KEY` or a key saved in setup; `OPENAI_BASE_URL` points it at OpenRouter, Ollama, LM Studio (local servers accept any key) | `gpt-5.5` |
116
+
117
+ **Resolution order.** Provider: `--provider` → `MINIMAL_PROVIDER` → `provider` in [settings](#settings). Model: `-m` → `MINIMAL_MODEL` → `model` in settings (if the provider is the settings' one) → the provider's default. Keys entered in setup are saved in `~/.minimal/keys.json` and take precedence over environment variables. They are removed from the environment of `bash` commands; export a key in your shell instead (and remove it from `keys.json`) if commands need it.
118
+
119
+ ### Slash commands
120
+
121
+ | Command | What it does |
122
+ | ---------------------------- | -------------------------------------------------------------------------- |
123
+ | `/help` | Show the commands, including those added by extensions |
124
+ | `/new`, `/sessions` | Start a new session, list saved sessions |
125
+ | `/resume [id]` | Resume a saved session: pick one of the latest, or give its id |
126
+ | `/model [number\|name]` | List models, or change model |
127
+ | `/provider [name [model]]` | List providers, or switch for this session and keep the conversation |
128
+ | `/tools` | List the tools |
129
+ | `/theme [name]` | Pick the colour theme; the choice is saved in `~/.minimal/settings.json` |
130
+ | `/reload` | Reload extensions and settings from disk |
131
+ | `/settings` | Show the settings files and each effective setting with where it comes from |
132
+ | `/trust` | Scan this project's extensions and trust-gated settings, then choose whether to trust and apply them |
133
+ | `/ext …` | Install, update, remove, list and publish extensions |
134
+ | `/setup`, `/login`, `/logout`| Change the saved default, sign in to Claude, forget the login |
135
+ | `/exit` | Quit (also `Ctrl+Q`) |
136
+
137
+ `/setup` and `/login` run with the UI suspended, in the plain terminal. `/setup` changes the saved default; `/provider` only affects the current session.
138
+
139
+ `Esc` interrupts the turn in progress: a running `bash` command is killed and the turn is dropped from the history, as when it fails. It takes effect at the next streamed chunk, tool call or model request. Interrupting during an automatic compaction aborts it and drops the turn like any other interrupt. Resuming a session (`-c`, `-s`, `/resume`) redraws its last 200 messages in the transcript.
140
+
141
+ Typing `/` and part of a command suggests the rest; `→` accepts it. Click a tool call to expand its output (up to 500 lines) and again to collapse it; edits show a diff with the file's line numbers, and `read`/`write` show syntax-highlighted code. `Ctrl+B` hides or shows the sidebar.
142
+
143
+ ### Environment
144
+
145
+ | Variable | Purpose |
146
+ | --------------------------------- | ------------------------------------------------------------------------------------------- |
147
+ | `ANTHROPIC_API_KEY` | Key for `--provider anthropic` |
148
+ | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | Key for `--provider gemini` |
149
+ | `OPENAI_API_KEY`, `OPENAI_BASE_URL` | Key for `--provider openai`, and the server it talks to (default: OpenAI) |
150
+ | `MINIMAL_PROVIDER`, `MINIMAL_MODEL` | Default provider and model |
151
+ | `MINIMAL_MARKETPLACE` | Git URL or local path of a marketplace used by the model's tools and listed first by `/ext` |
152
+
153
+ ---
154
+
155
+ ## Settings
156
+
157
+ Settings are layered JSON files: `~/.minimal/settings.json` (global) < `.minimal/settings.json` (project, shared with the repository) < `.minimal/settings.local.json` (local, personal), then the environment, then the command line. They hold the provider, model and theme, extra prompt text and prompt files, permission rules, disabled extensions and per-extension settings, and (global only) the marketplaces and base prompt. `/settings` shows where each value comes from.
158
+
159
+ ```json
160
+ {
161
+ "theme": "dark",
162
+ "permissions": {"allow": ["bash(git status*)", "write(src/**)"], "deny": ["read(.env)"]},
163
+ "extensions": {"disabled": ["widgets/token_sparkline"], "settings": {"compact": {"compact_at": 150000}}}
164
+ }
165
+ ```
166
+
167
+ A project's `permissions.allow`, `extensions.disabled` and `extensions.settings` apply only after `/trust`. 0.1's `~/.minimal/config.json`, `theme.json` and `marketplaces.json` are moved into `~/.minimal/settings.json` on the first start, and `prompt.md` becomes `prompt.base`. The keys, merge rules, permission syntax and examples are in [docs/settings.md](docs/settings.md).
168
+
169
+ ---
170
+
171
+ ## Extensions
172
+
173
+ An extension is a Python file, or a folder with an `__init__.py`, defining `register(minimal)`. Extensions are loaded from `~/.minimal/extensions/`, `./.minimal/extensions/` and any path passed with `--ext`; names starting with `_` or `.` are skipped. Each folder's own entries load first, then those in its `themes/`, `tools/` and `widgets/` subfolders, where the marketplace installs those categories. `extensions.disabled` in [settings](#settings) skips some by id.
174
+
175
+ A project's `./.minimal/extensions/` is code from the repository, so minimal does not run it until you trust it. Untrusted, it is skipped without asking: the transcript says so and the status line shows `⚠ N untrusted`. `/trust` prints a static scan of each file (what it can do: run commands, use the network, read environment variables or credential files, write files, decode hidden data, hook tool calls…; a list of capabilities, not of intent) and the project's trust-gated settings, and asks once. The answer is remembered for exactly what was scanned (`~/.minimal/trusted.json`); adding or editing a file or one of those settings makes the project untrusted again, including files written by the model or installed by `/ext`. `--trust-project` applies them without `/trust`; `-y` does not.
176
+
177
+ ```python
178
+ """Blocks force pushes and tells the time."""
179
+
180
+ from datetime import datetime
181
+
182
+ __version__ = "1.0"
183
+ __requires__ = ">=0.2"
184
+
185
+
186
+ def register(minimal):
187
+ settings = minimal.declare_settings({"blocked": "git push --force"}) # extensions.settings.<id> in settings.json
188
+
189
+ @minimal.tool(input_schema={"type": "object", "properties": {}})
190
+ def now(input, ctx):
191
+ """Return the current date and time."""
192
+ return datetime.now().isoformat()
193
+
194
+ @minimal.command(description="show the session size")
195
+ def stats(args, ctx):
196
+ print(f"{len(ctx.messages)} messages · {ctx.model}")
197
+
198
+ @minimal.on("tool_call")
199
+ def guard(event):
200
+ if settings["blocked"] in event["input"].get("command", ""):
201
+ return {"block": True, "reason": "blocked by settings"}
202
+
203
+ minimal.append_system_prompt("Always reply in the user's language.")
204
+ ```
205
+
206
+ `register(minimal)` receives an `Extension` with decorators and `register_*` methods for tools, commands, providers, events, prompt notes, widgets, status segments, themes and a header logo. Tools, commands, events and widgets receive a `Context` as `ctx`: the session, model and tools, `ctx.ui` for questions, `ctx.complete` and `ctx.run_agent` for model calls, `ctx.new_session`. Import anything else only from `minimal.api`. Errors raised by a tool are returned to the model as an `is_error` result, so it can correct itself. **[docs/extensions.md](docs/extensions.md)** is the full guide: package folders, metadata (`__version__`, `__requires__`), every member, event payloads, settings, widgets, themes, logos, publishing, and migrating from 0.1. See [`extensions/example.py`](extensions/example.py) for a complete extension with a tool, a command, a guardrail with a setting, a prompt note and a sidebar widget.
207
+
208
+ The repository has three more, loaded the same way (`minimal -e extensions/compact.py`) or copied to `~/.minimal/extensions/` (tools in its `tools/` subfolder):
209
+
210
+ | Extension | What it adds |
211
+ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
212
+ | `extensions/compact.py` | Summarises the conversation into a new session when the context reaches 100k tokens (setting `compact_at`), and on demand with `/compact`. The old session stays on disk: `/resume <id>` brings it back |
213
+ | `extensions/tools/subagent.py` | `subagent` tool: runs a task in a fresh agent loop with the same tools and an empty history; only its final message returns to the conversation |
214
+ | `extensions/tools/web_search.py` | `web_search` tool: results from the [Brave Search API](https://api-dashboard.search.brave.com) (needs `BRAVE_API_KEY`) |
215
+
216
+ ### Self-extension and hot reload
217
+
218
+ The model knows how to write extensions: the system prompt explains the API and where to save them. After writing or editing one, it calls the `reload_extensions` tool, which asks for confirmation like `write` (skipped with `-y`). Reloading rebuilds the registry from disk in place, so new tools are offered on the model's next request — even in the middle of a turn — and widgets are remounted without restarting. Load errors are returned to the model so it can fix them. Reload manually with `/reload`.
219
+
220
+ ---
221
+
222
+ ## Marketplace
223
+
224
+ Extensions are shared through a git repository with one folder per category: `extensions/` (general), `themes/` (colour themes), `tools/` (tools for the model) and `widgets/` (terminal UI components). Each holds `<name>.py` files or `<name>/` package folders whose docstring's first line is the description, with a required `__version__ = "1.0"` and an optional `__requires__ = ">=0.2"`; items that need a newer minimal are shown but not installed. Installing mirrors the category, so `themes/amber.py` goes to `~/.minimal/extensions/themes/amber.py`. Before publishing, `minimal ext check <path>` (or `minimal ext check .` in a marketplace checkout) loads each item without a terminal and prints `ok`, `WARN` (a missing optional module) or `FAIL`; it exits 1 on any failure.
225
+
226
+ The default marketplace is [`RafaSG13/minimal-marketplace`](https://github.com/RafaSG13/minimal-marketplace). `MINIMAL_MARKETPLACE` adds another URL or local path (for example your fork) and makes it the one the model's tools use.
227
+
228
+ ```
229
+ /ext pick a marketplace and a category, then tick items to install, update or remove
230
+ /ext list list installed items by scope and category
231
+ /ext publish <path> [name] push a publish/<name>-<timestamp> branch and open a pull request (with `gh`)
232
+ ```
233
+
234
+ `/ext` lists the default marketplace, `$MINIMAL_MARKETPLACE` and the ones you added. "Add a marketplace…" takes a git URL or a local path, checks that it can be cloned and saves it under `marketplaces` in `~/.minimal/settings.json`; "Remove a saved marketplace…" forgets one. Picking a marketplace shows its categories with item and install counts; picking a category shows a checklist with installed items ticked. Tick to install (then choose the global or the project folder), untick to uninstall; items whose files differ from the marketplace's are offered for update. `Space` toggles, `Enter` applies, `Esc` cancels; extensions are reloaded afterwards. `/ext publish` files an extension under the category of the folder it comes from (`~/.minimal/extensions/themes/x.py` → `themes/`), else under `extensions/`.
235
+
236
+ The model has the `ext_search`, `ext_install` and `ext_publish` tools (each takes an optional `category`). `ext_search` searches every configured marketplace; `ext_install` and `ext_publish` take an optional `marketplace` (one of those URLs), and otherwise install from the marketplace that has the item and publish to `$MINIMAL_MARKETPLACE` or the default. The model must call `reload_extensions` after installing. Publishing never pushes to the default branch: without push access, fork the marketplace and point `MINIMAL_MARKETPLACE` at the fork.
237
+
238
+ ---
239
+
240
+ ## Architecture
241
+
242
+ ```mermaid
243
+ flowchart LR
244
+ cli[cli.main] --> app[App]
245
+ cli --> tui[MinimalTui] --> commands[commands.run_command]
246
+ tui --> turn[App.run_turn]
247
+ turn --> agent[agent.run_agent]
248
+ agent --> provider{Provider}
249
+ provider --> anthropic[(Anthropic API)]
250
+ provider --> gemini[(Gemini API)]
251
+ provider --> openai[(OpenAI-compatible API)]
252
+ agent --> exec[execute_tool_call] --> guard["tool_call handlers: veto, fail closed"] --> perms["permission rules: deny, allow"] --> confirm[user confirmation] --> registry[Registry]
253
+ registry --> tools[built-in tools]
254
+ registry --> ext[extensions]
255
+ app --> reload[App.reload_extensions] --> trust["settings layers; project extensions and settings trusted?<br/>(~/.minimal/trusted.json, /trust)"] --> registry
256
+ turn --> session[Session.save]
257
+ ```
258
+
259
+ **One user turn.** `App.run_turn` appends the user message and calls `run_agent`. The loop asks the current provider for a reply. If the reply calls tools, each call goes through `tool_call` handlers (which can veto it; one that raises blocks it), permission rules (a deny fails the call, an allow skips the question), user confirmation, the tool itself, and `tool_result` handlers (which can rewrite the output). The results go back to the model, and this repeats until it answers without tools. If anything fails mid-turn, or the user interrupts it, the turn is dropped from the history. The session file is appended to after every turn.
260
+
261
+ **Frontends.** What the user sees goes through a `Frontend` (`core/app.py`): the terminal UI, or `ConsoleFrontend` (plain stdout and `input()`) for commands that need the real terminal. In the terminal UI, turns and slash commands run in a worker thread; streamed text, tool calls, tool results and confirmation dialogs are handed to the UI thread, and anything printed appears in the transcript.
262
+
263
+ **History.** Stored in one provider-neutral format shaped like the Anthropic Messages API (see `providers/base.py`). Each provider converts it to its own API on every request. When a provider must replay its own replies exactly (Gemini's thought signatures), it keeps that data in the message's `provider_data`; other providers ignore it. Assistant messages also record the request's token `usage` when the provider reports it, which is where the status line's context size comes from. Requests to Anthropic use prompt caching, so the unchanged start of the history is not paid for again.
264
+
265
+ <details>
266
+ <summary><b>Project layout</b></summary>
267
+
268
+ ```
269
+ src/minimal/ packages from the bottom up; each imports only from the ones above it
270
+ config.py layered settings (settings.json global/project/local), migration from 0.1 files, API keys, trust records
271
+ providers/
272
+ base.py Provider interface, Reply, Usage, canonical message format
273
+ anthropic.py Claude via the Anthropic Messages API (API key)
274
+ claude.py Claude via a Pro/Max subscription (browser login)
275
+ claude_auth.py browser login (OAuth PKCE) and token storage (~/.minimal/claude-auth.json)
276
+ gemini.py Gemini via the Google Interactions API
277
+ openai.py OpenAI-compatible servers via the Chat Completions API
278
+ extensions/
279
+ registry.py internal Registry, Tool, Command, WidgetSpec: tools, commands, providers, hooks, widgets, themes, logo
280
+ facade.py Extension: what an extension's register(minimal) receives (decorators, declare_settings)
281
+ metadata.py reads docstring, __version__ and __requires__ without running the code; version checks
282
+ loader.py finds extension files and package folders, imports them and calls their register(minimal)
283
+ audit.py static scan of extension source for /trust: what a file can do
284
+ marketplace.py git-based marketplaces: list, search, install, publish, `minimal ext check` (~/.minimal/marketplace cache)
285
+ logo.py python -m minimal.extensions.logo: turns an image into a header-logo extension (needs Pillow)
286
+ tools/
287
+ common.py output clipping, which paths are read without asking
288
+ files.py read, write, edit
289
+ shell.py bash
290
+ search.py grep, glob
291
+ reload.py reload_extensions
292
+ builtin.py the list of built-in tools
293
+ core/
294
+ agent.py agent loop (model → tools → model …)
295
+ app.py App: runtime state, settings, provider switching, system prompt, one user turn, extension reload, Frontend
296
+ context.py Context and UI: the ctx extensions receive
297
+ permissions.py permission rules: allow/deny matching for tool calls
298
+ session.py JSONL sessions (~/.minimal/sessions)
299
+ prompt.md packaged base prompt
300
+ ui/
301
+ commands.py slash commands (/help, /provider, /reload, /settings, /trust, /ext, …)
302
+ setup.py onboarding wizard: provider, login or API key, model
303
+ tui/
304
+ app.py MinimalTui: transcript, prompt, status line, widget areas; run_tui
305
+ widgets.py tool-call blocks and the welcome header
306
+ screens.py dialogs: confirmation and pickers
307
+ frontend.py TuiFrontend: how turns in worker threads reach the UI
308
+ theme.py built-in theme, extension themes for Textual, UI glyphs
309
+ cli.py option parsing, entry point, `minimal ext check`
310
+ api.py the only module extensions import from: Extension, Context, event types, theme, …
311
+ extensions/
312
+ example.py example extension (tool, command, guardrail with a setting, prompt note, sidebar widget)
313
+ compact.py history compaction: automatic on turn_end, and /compact
314
+ tools/
315
+ subagent.py subagent tool
316
+ web_search.py web_search tool (Brave Search API)
317
+ tests/ unittest suite (see Development)
318
+ ```
319
+
320
+ </details>
321
+
322
+ ### Adding a provider
323
+
324
+ A provider is a class with a `name` and a `stream(*, model, system, messages, tools, on_text) -> Reply` method (see `providers/base.py`):
325
+
326
+ 1. Convert the canonical `messages` and `tools` to the API's request format.
327
+ 2. Stream the response, calling `on_text(chunk)` for text as it arrives.
328
+ 3. Return `Reply(content=[text/tool_use blocks], stop_reason="tool_use" | "end_turn" | ..., provider_data=...)`.
329
+
330
+ Register it as a built-in in `providers/__init__.py` (`BUILTIN_PROVIDERS`), or from an extension with `minimal.register_provider(...)`. `providers/gemini.py` is a complete example, including the history conversion.
331
+
332
+ ---
333
+
334
+ ## Development
335
+
336
+ ```bash
337
+ uv sync
338
+ uv run ruff check
339
+ uv run python -m unittest discover -s tests
340
+ ```
341
+
342
+ The suite needs no network or API key. It covers:
343
+
344
+ | Test file | What's covered |
345
+ | --------------------- | ------------------------------------------------------------------------------------------------ |
346
+ | `test_smoke.py` | Tools, extensions, hot reload, the agent loop with a fake provider, interruption, sessions, logo validation |
347
+ | `test_providers.py` | Provider conversions (Gemini and OpenAI history/streaming, Anthropic/Claude requests, token usage) |
348
+ | `test_claude_auth.py` | Browser login round trip, token refresh |
349
+ | `test_config.py` | Saved keys, how the startup provider is chosen |
350
+ | `test_settings.py` | Settings layers and precedence, validation errors, trust-gated keys, prompt files, migration from 0.1 files |
351
+ | `test_permissions.py` | Allow/deny rules for bash, paths and extension tools; deny with `-y`; shell operators |
352
+ | `test_extension_api.py` | The `Extension` and `Context` facades: decorators, events, settings, sessions, nested agents, package folders, `__requires__`, disabling |
353
+ | `test_security.py` | Trust digests and the `/trust` scan |
354
+ | `test_marketplace.py` | Search, install, publish, package folders, versions, the `/ext` picker, saved marketplaces and `minimal ext check`, against local bare git repos |
355
+ | `test_tui.py` | The headless terminal UI: streaming, interruption, session replay, dialogs, tool-call states, command output, widgets, header |
356
+ | `test_logo.py` | The image-to-logo converter (skipped without Pillow) |
357
+ | `test_bundled_extensions.py` | The extensions in `extensions/`: web search, subagent, compaction |
358
+
359
+ ---
360
+
361
+ ## Contributing
362
+
363
+ Issues and pull requests are welcome at [RafaSG13/Minimal](https://github.com/RafaSG13/Minimal/issues). Extensions belong in the [marketplace](https://github.com/RafaSG13/minimal-marketplace) — publish one straight from the app with `/ext publish <path>`.
364
+
365
+ Licensed under the [MIT License](LICENSE).
366
+
367
+ Built by [RafaSG13](https://github.com/RafaSG13).