minu 0.1.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.
- minu-0.1.0/LICENSE +21 -0
- minu-0.1.0/PKG-INFO +134 -0
- minu-0.1.0/README.md +105 -0
- minu-0.1.0/pyproject.toml +53 -0
- minu-0.1.0/pyproject.toml.orig +37 -0
- minu-0.1.0/src/minu/__init__.py +5 -0
- minu-0.1.0/src/minu/__main__.py +3 -0
- minu-0.1.0/src/minu/agent.py +119 -0
- minu-0.1.0/src/minu/app.py +207 -0
- minu-0.1.0/src/minu/cli.py +49 -0
- minu-0.1.0/src/minu/config.py +49 -0
- minu-0.1.0/src/minu/images.py +35 -0
- minu-0.1.0/src/minu/installs.py +21 -0
- minu-0.1.0/src/minu/model_flows.py +156 -0
- minu-0.1.0/src/minu/models.py +104 -0
- minu-0.1.0/src/minu/server.py +85 -0
- minu-0.1.0/src/minu/sessions.py +75 -0
- minu-0.1.0/src/minu/tools.py +100 -0
- minu-0.1.0/src/minu/widgets.py +43 -0
minu-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mr-jones123
|
|
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.
|
minu-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: minu
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tiny agent harness for local models (llama.cpp, MLX, Ollama)
|
|
5
|
+
Keywords: agent,llm,local-llm,llama.cpp,mlx,ollama,tui,coding-agent
|
|
6
|
+
Author: mr-jones123
|
|
7
|
+
Author-email: mr-jones123 <lacapxyniljhed@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Requires-Dist: textual>=1.0
|
|
20
|
+
Requires-Dist: textual-image>=0.8
|
|
21
|
+
Requires-Dist: pillow>=10.3
|
|
22
|
+
Requires-Dist: mlx-lm ; platform_machine == 'arm64' and sys_platform == 'darwin' and extra == 'mlx'
|
|
23
|
+
Requires-Python: >=3.13
|
|
24
|
+
Project-URL: Homepage, https://github.com/mr-jones123/minu
|
|
25
|
+
Project-URL: Repository, https://github.com/mr-jones123/minu
|
|
26
|
+
Project-URL: Issues, https://github.com/mr-jones123/minu/issues
|
|
27
|
+
Provides-Extra: mlx
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# minu
|
|
31
|
+
|
|
32
|
+
A very small agent harness for local models. One tool (`sh`) and a system prompt of about 86 tokens.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
uv tool install minu # or: pipx install minu (needs Python 3.13; uv fetches it for you)
|
|
36
|
+
minu # run it in any project folder
|
|
37
|
+
```
|
|
38
|
+
Upgrade with `uv tool upgrade minu`, remove with `uv tool uninstall minu`. Sessions are saved in `~/.minu/`.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
minu # opens the model picker
|
|
42
|
+
minu --hf LiquidAI/LFM3-1.7B-GGUF
|
|
43
|
+
minu --mlx mlx-community/Qwen3-1.7B-4bit
|
|
44
|
+
minu --ollama gemma3:4b
|
|
45
|
+
minu --url http://localhost:8080 # server that's already running
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`-y` runs commands without asking. `--native` uses the server's tool-call API instead of ```sh blocks.
|
|
49
|
+
|
|
50
|
+
## Models
|
|
51
|
+
`ctrl+o` (or `/model`) opens the picker. It has:
|
|
52
|
+
- the GGUF and MLX models in your Hugging Face cache, plus your Ollama models
|
|
53
|
+
- **⬇ download from Hugging Face**: search, or paste `org/repo`, then pick a size. For vision repos the `mmproj` file is downloaded too.
|
|
54
|
+
- **⬇ pull from Ollama**: any name from ollama.com/library, with progress shown in the status bar.
|
|
55
|
+
|
|
56
|
+
Switching models stops the old server and starts the new one. The chat stays.
|
|
57
|
+
|
|
58
|
+
**✕ delete a model** (in the picker, or `/delete`) lists your downloaded models with their size on disk. It asks before
|
|
59
|
+
deleting (the default is "keep it"). For GGUF it removes that quant, plus the whole repo (mmproj, cache entry) once
|
|
60
|
+
no weights are left in it; MLX repos are removed whole; Ollama models go through `ollama`'s API. A loaded model is stopped first.
|
|
61
|
+
`esc` cancels a download.
|
|
62
|
+
|
|
63
|
+
If something is missing, minu offers to install it (it always asks first):
|
|
64
|
+
| needed for | tool | install |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| GGUF | llama-server | `brew install llama.cpp` |
|
|
67
|
+
| MLX (Apple Silicon only) | mlx_lm | `uv tool install mlx-lm` |
|
|
68
|
+
| HF downloads | hf | `uv tool install huggingface_hub` |
|
|
69
|
+
| Ollama | ollama | `brew install ollama` / ollama.com install script |
|
|
70
|
+
|
|
71
|
+
If Ollama is installed but not running, minu starts `ollama serve` itself and stops it when you quit.
|
|
72
|
+
|
|
73
|
+
## Images
|
|
74
|
+
Put an image path in your message (dragging a file into the terminal pastes its path):
|
|
75
|
+
`what's in ~/Desktop/shot.png?`
|
|
76
|
+
The image is shown in the chat and sent to the model. That needs a vision model: GGUF repos with an
|
|
77
|
+
mmproj (marked `[vision]` in the picker), or an Ollama vision model such as `gemma3:4b`. MLX models are text only.
|
|
78
|
+
Image files the model mentions in its answer are shown too, and `/view path.png` shows any image.
|
|
79
|
+
Images render as real pixels in Kitty, Ghostty, WezTerm and iTerm2, and as colored blocks in other terminals.
|
|
80
|
+
|
|
81
|
+
## Sessions
|
|
82
|
+
Each chat is saved to `~/.minu/sessions/<project>/<timestamp>.jsonl` (one folder per project path, e.g.
|
|
83
|
+
`-Users-me-code-app`): a `{"meta": …}` line, then one message per line. Set `MINU_HOME` to move `~/.minu`.
|
|
84
|
+
Sessions from older versions in `./.minu/sessions` still show up in `/resume`.
|
|
85
|
+
`/resume` opens a session picker · `minu -c` continues the latest one · `minu -r FILE` opens a specific file · `/new` (or `ctrl+n`) starts a new session.
|
|
86
|
+
|
|
87
|
+
## Keys
|
|
88
|
+
`enter` send · `ctrl+j` newline · `ctrl+o` model · `ctrl+y` auto-run · `esc` stop · `ctrl+n` new session · `ctrl+c` quit
|
|
89
|
+
Type `/` for a menu of commands (↑/↓ to move, tab to complete, enter to run, esc to close):
|
|
90
|
+
`/new` `/model` `/delete` `/resume` `/search <query>` `/fetch <url>` `/view <img>` `/auto` `/clear` `/exit`
|
|
91
|
+
|
|
92
|
+
## Web
|
|
93
|
+
The model can search on its own (`minu-search "q"`, `minu-fetch URL` in its shell). You can also do it yourself:
|
|
94
|
+
`/search <query>` gets results and the model answers from them; `/fetch <url>` reads a page and the model summarizes it.
|
|
95
|
+
Search tries DuckDuckGo first, then Wikipedia's API. DuckDuckGo sometimes rate-limits; for search that never gets blocked,
|
|
96
|
+
point minu at your own SearXNG instance: `MINU_SEARXNG=http://localhost:8888`.
|
|
97
|
+
Env: `MINU_CTX` (llama.cpp context, default 16384), `MINU_HOME` (default `~/.minu`), `OLLAMA_HOST`, `HF_HOME`. Server log: `~/.cache/minu/server.log`.
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
```
|
|
101
|
+
git clone https://github.com/mr-jones123/minu && cd minu
|
|
102
|
+
uv tool install -e . # global `minu` that runs your checkout (changes to src/ apply right away)
|
|
103
|
+
uv run minu # or run it from the repo
|
|
104
|
+
uv build # sdist + wheel into dist/
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Releasing to PyPI
|
|
108
|
+
Publishing runs in GitHub Actions (`.github/workflows/publish.yml`) with PyPI Trusted Publishing, so no token is stored.
|
|
109
|
+
One-time setup: on pypi.org → *Your account → Publishing → Add a pending publisher*: project `minu`,
|
|
110
|
+
owner `mr-jones123`, repo `minu`, workflow `publish.yml`, environment `pypi`. Then for each release:
|
|
111
|
+
```
|
|
112
|
+
uv version --bump patch # or minor / major; edits pyproject.toml
|
|
113
|
+
git commit -am "release v$(uv version --short)" && git tag v$(uv version --short) && git push --follow-tags
|
|
114
|
+
```
|
|
115
|
+
CI builds, smoke-tests the wheel, checks the tag matches the version, and uploads.
|
|
116
|
+
|
|
117
|
+
## Code layout (`src/minu/`, the `minu` package)
|
|
118
|
+
| file | what it does |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `cli.py` | command-line flags, plus `minu search` / `minu fetch` for the agent's shell |
|
|
121
|
+
| `config.py` | paths, limits, system prompts, banner |
|
|
122
|
+
| `app.py` | the TUI: layout, status bar, input handling |
|
|
123
|
+
| `agent.py` | agent loop: stream a reply, run its command, send the output back |
|
|
124
|
+
| `model_flows.py` | model picker, HF/Ollama downloads, install offers, loading |
|
|
125
|
+
| `sessions.py` | saving and resuming `~/.minu/sessions/<project>/*.jsonl` |
|
|
126
|
+
| `server.py` | starting llama-server / mlx_lm / Ollama and streaming chat |
|
|
127
|
+
| `models.py` | finding local models and searching Hugging Face |
|
|
128
|
+
| `tools.py` | the `sh` tool, web search/fetch, parsing commands from replies |
|
|
129
|
+
| `images.py` | attaching images and showing them |
|
|
130
|
+
| `installs.py` | finding tools and their install commands |
|
|
131
|
+
| `widgets.py` | input box, list picker, text prompt |
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
MIT, see [LICENSE](LICENSE).
|
minu-0.1.0/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# minu
|
|
2
|
+
|
|
3
|
+
A very small agent harness for local models. One tool (`sh`) and a system prompt of about 86 tokens.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
uv tool install minu # or: pipx install minu (needs Python 3.13; uv fetches it for you)
|
|
7
|
+
minu # run it in any project folder
|
|
8
|
+
```
|
|
9
|
+
Upgrade with `uv tool upgrade minu`, remove with `uv tool uninstall minu`. Sessions are saved in `~/.minu/`.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
minu # opens the model picker
|
|
13
|
+
minu --hf LiquidAI/LFM3-1.7B-GGUF
|
|
14
|
+
minu --mlx mlx-community/Qwen3-1.7B-4bit
|
|
15
|
+
minu --ollama gemma3:4b
|
|
16
|
+
minu --url http://localhost:8080 # server that's already running
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`-y` runs commands without asking. `--native` uses the server's tool-call API instead of ```sh blocks.
|
|
20
|
+
|
|
21
|
+
## Models
|
|
22
|
+
`ctrl+o` (or `/model`) opens the picker. It has:
|
|
23
|
+
- the GGUF and MLX models in your Hugging Face cache, plus your Ollama models
|
|
24
|
+
- **⬇ download from Hugging Face**: search, or paste `org/repo`, then pick a size. For vision repos the `mmproj` file is downloaded too.
|
|
25
|
+
- **⬇ pull from Ollama**: any name from ollama.com/library, with progress shown in the status bar.
|
|
26
|
+
|
|
27
|
+
Switching models stops the old server and starts the new one. The chat stays.
|
|
28
|
+
|
|
29
|
+
**✕ delete a model** (in the picker, or `/delete`) lists your downloaded models with their size on disk. It asks before
|
|
30
|
+
deleting (the default is "keep it"). For GGUF it removes that quant, plus the whole repo (mmproj, cache entry) once
|
|
31
|
+
no weights are left in it; MLX repos are removed whole; Ollama models go through `ollama`'s API. A loaded model is stopped first.
|
|
32
|
+
`esc` cancels a download.
|
|
33
|
+
|
|
34
|
+
If something is missing, minu offers to install it (it always asks first):
|
|
35
|
+
| needed for | tool | install |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| GGUF | llama-server | `brew install llama.cpp` |
|
|
38
|
+
| MLX (Apple Silicon only) | mlx_lm | `uv tool install mlx-lm` |
|
|
39
|
+
| HF downloads | hf | `uv tool install huggingface_hub` |
|
|
40
|
+
| Ollama | ollama | `brew install ollama` / ollama.com install script |
|
|
41
|
+
|
|
42
|
+
If Ollama is installed but not running, minu starts `ollama serve` itself and stops it when you quit.
|
|
43
|
+
|
|
44
|
+
## Images
|
|
45
|
+
Put an image path in your message (dragging a file into the terminal pastes its path):
|
|
46
|
+
`what's in ~/Desktop/shot.png?`
|
|
47
|
+
The image is shown in the chat and sent to the model. That needs a vision model: GGUF repos with an
|
|
48
|
+
mmproj (marked `[vision]` in the picker), or an Ollama vision model such as `gemma3:4b`. MLX models are text only.
|
|
49
|
+
Image files the model mentions in its answer are shown too, and `/view path.png` shows any image.
|
|
50
|
+
Images render as real pixels in Kitty, Ghostty, WezTerm and iTerm2, and as colored blocks in other terminals.
|
|
51
|
+
|
|
52
|
+
## Sessions
|
|
53
|
+
Each chat is saved to `~/.minu/sessions/<project>/<timestamp>.jsonl` (one folder per project path, e.g.
|
|
54
|
+
`-Users-me-code-app`): a `{"meta": …}` line, then one message per line. Set `MINU_HOME` to move `~/.minu`.
|
|
55
|
+
Sessions from older versions in `./.minu/sessions` still show up in `/resume`.
|
|
56
|
+
`/resume` opens a session picker · `minu -c` continues the latest one · `minu -r FILE` opens a specific file · `/new` (or `ctrl+n`) starts a new session.
|
|
57
|
+
|
|
58
|
+
## Keys
|
|
59
|
+
`enter` send · `ctrl+j` newline · `ctrl+o` model · `ctrl+y` auto-run · `esc` stop · `ctrl+n` new session · `ctrl+c` quit
|
|
60
|
+
Type `/` for a menu of commands (↑/↓ to move, tab to complete, enter to run, esc to close):
|
|
61
|
+
`/new` `/model` `/delete` `/resume` `/search <query>` `/fetch <url>` `/view <img>` `/auto` `/clear` `/exit`
|
|
62
|
+
|
|
63
|
+
## Web
|
|
64
|
+
The model can search on its own (`minu-search "q"`, `minu-fetch URL` in its shell). You can also do it yourself:
|
|
65
|
+
`/search <query>` gets results and the model answers from them; `/fetch <url>` reads a page and the model summarizes it.
|
|
66
|
+
Search tries DuckDuckGo first, then Wikipedia's API. DuckDuckGo sometimes rate-limits; for search that never gets blocked,
|
|
67
|
+
point minu at your own SearXNG instance: `MINU_SEARXNG=http://localhost:8888`.
|
|
68
|
+
Env: `MINU_CTX` (llama.cpp context, default 16384), `MINU_HOME` (default `~/.minu`), `OLLAMA_HOST`, `HF_HOME`. Server log: `~/.cache/minu/server.log`.
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
```
|
|
72
|
+
git clone https://github.com/mr-jones123/minu && cd minu
|
|
73
|
+
uv tool install -e . # global `minu` that runs your checkout (changes to src/ apply right away)
|
|
74
|
+
uv run minu # or run it from the repo
|
|
75
|
+
uv build # sdist + wheel into dist/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Releasing to PyPI
|
|
79
|
+
Publishing runs in GitHub Actions (`.github/workflows/publish.yml`) with PyPI Trusted Publishing, so no token is stored.
|
|
80
|
+
One-time setup: on pypi.org → *Your account → Publishing → Add a pending publisher*: project `minu`,
|
|
81
|
+
owner `mr-jones123`, repo `minu`, workflow `publish.yml`, environment `pypi`. Then for each release:
|
|
82
|
+
```
|
|
83
|
+
uv version --bump patch # or minor / major; edits pyproject.toml
|
|
84
|
+
git commit -am "release v$(uv version --short)" && git tag v$(uv version --short) && git push --follow-tags
|
|
85
|
+
```
|
|
86
|
+
CI builds, smoke-tests the wheel, checks the tag matches the version, and uploads.
|
|
87
|
+
|
|
88
|
+
## Code layout (`src/minu/`, the `minu` package)
|
|
89
|
+
| file | what it does |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `cli.py` | command-line flags, plus `minu search` / `minu fetch` for the agent's shell |
|
|
92
|
+
| `config.py` | paths, limits, system prompts, banner |
|
|
93
|
+
| `app.py` | the TUI: layout, status bar, input handling |
|
|
94
|
+
| `agent.py` | agent loop: stream a reply, run its command, send the output back |
|
|
95
|
+
| `model_flows.py` | model picker, HF/Ollama downloads, install offers, loading |
|
|
96
|
+
| `sessions.py` | saving and resuming `~/.minu/sessions/<project>/*.jsonl` |
|
|
97
|
+
| `server.py` | starting llama-server / mlx_lm / Ollama and streaming chat |
|
|
98
|
+
| `models.py` | finding local models and searching Hugging Face |
|
|
99
|
+
| `tools.py` | the `sh` tool, web search/fetch, parsing commands from replies |
|
|
100
|
+
| `images.py` | attaching images and showing them |
|
|
101
|
+
| `installs.py` | finding tools and their install commands |
|
|
102
|
+
| `widgets.py` | input box, list picker, text prompt |
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "minu"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Tiny agent harness for local models (llama.cpp, MLX, Ollama)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"agent",
|
|
11
|
+
"llm",
|
|
12
|
+
"local-llm",
|
|
13
|
+
"llama.cpp",
|
|
14
|
+
"mlx",
|
|
15
|
+
"ollama",
|
|
16
|
+
"tui",
|
|
17
|
+
"coding-agent",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Environment :: Console",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Operating System :: MacOS",
|
|
24
|
+
"Operating System :: POSIX :: Linux",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
|
+
"Topic :: Software Development",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"textual>=1.0",
|
|
32
|
+
"textual-image>=0.8",
|
|
33
|
+
"pillow>=10.3",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[[project.authors]]
|
|
37
|
+
name = "mr-jones123"
|
|
38
|
+
email = "lacapxyniljhed@gmail.com"
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
mlx = ["mlx-lm; sys_platform == 'darwin' and platform_machine == 'arm64'"]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/mr-jones123/minu"
|
|
45
|
+
Repository = "https://github.com/mr-jones123/minu"
|
|
46
|
+
Issues = "https://github.com/mr-jones123/minu/issues"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
minu = "minu.cli:main"
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["uv_build>=0.9,<0.10"]
|
|
53
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "minu"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Tiny agent harness for local models (llama.cpp, MLX, Ollama)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "mr-jones123", email = "lacapxyniljhed@gmail.com" }]
|
|
10
|
+
keywords = ["agent", "llm", "local-llm", "llama.cpp", "mlx", "ollama", "tui", "coding-agent"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Operating System :: MacOS",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
"Topic :: Software Development",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["textual>=1.0", "textual-image>=0.8", "pillow>=10.3"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
mlx = ["mlx-lm; sys_platform == 'darwin' and platform_machine == 'arm64'"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/mr-jones123/minu"
|
|
29
|
+
Repository = "https://github.com/mr-jones123/minu"
|
|
30
|
+
Issues = "https://github.com/mr-jones123/minu/issues"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
minu = "minu.cli:main"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.9,<0.10"]
|
|
37
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""The agent loop: stream a reply, run its command, feed the output back."""
|
|
2
|
+
import json, re, time
|
|
3
|
+
|
|
4
|
+
from textual import work
|
|
5
|
+
from textual.widgets import Markdown
|
|
6
|
+
|
|
7
|
+
from .config import BLOCK, STEPS
|
|
8
|
+
from .images import mentioned_images
|
|
9
|
+
from .tools import fetch, search, sh, split_think, to_cmd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentMixin:
|
|
13
|
+
def approve(s, cmd):
|
|
14
|
+
if s.auto: s.ui(s.note, f"$ {cmd}", "cmd"); return True
|
|
15
|
+
s.ui(s.note, f"$ {cmd}", "cmd")
|
|
16
|
+
return s.ask("run it?")
|
|
17
|
+
|
|
18
|
+
def exec_cmd(s, cmd):
|
|
19
|
+
if not s.approve(cmd): return "[user declined to run this]"
|
|
20
|
+
s.set_state("running…")
|
|
21
|
+
out = sh(cmd); s.state = ""
|
|
22
|
+
lines = out.splitlines()
|
|
23
|
+
s.ui(s.note, "\n".join(lines[:15]) + (f"\n… ({len(lines) - 15} more lines)" if len(lines) > 15 else ""), "out")
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
def web(s, kind, arg): # /search and /fetch: look it up, show it, hand it to the model
|
|
27
|
+
s.set_state(f"{'searching' if kind == 'search' else 'fetching'}…")
|
|
28
|
+
try: res = search(arg) if kind == "search" else fetch(arg)
|
|
29
|
+
except Exception as e: res = f"{kind} failed: {e}"
|
|
30
|
+
lines = res.splitlines()
|
|
31
|
+
s.ui(s.note, "\n".join(lines[:20]) + (f"\n… ({len(lines) - 20} more lines)" if len(lines) > 20 else ""), "out")
|
|
32
|
+
if not s.srv.url: return s.say("▸ no model loaded, so just showing results (ctrl+o to pick one)")
|
|
33
|
+
ask = (f'Web search results for "{arg}":\n\n{res}\n\nAnswer "{arg}" now from these results, citing URLs. Do not run any commands.' if kind == "search"
|
|
34
|
+
else f"Contents of {arg}:\n\n{res}\n\nSummarize this page.")
|
|
35
|
+
s.push({"role": "user", "content": ask}); return True
|
|
36
|
+
|
|
37
|
+
@work(thread=True, exclusive=True, group="turn")
|
|
38
|
+
def turn(s, pre=None):
|
|
39
|
+
s.busy = True; s.stop_ev.clear()
|
|
40
|
+
try:
|
|
41
|
+
if not pre or s.web(*pre): # web() returns None when there's no model to answer
|
|
42
|
+
for _ in range(STEPS):
|
|
43
|
+
if s.step(): break
|
|
44
|
+
except Exception as e:
|
|
45
|
+
msg = str(e)
|
|
46
|
+
if re.search(r"image|mmproj|multimodal|vision", msg, re.I) and any(isinstance(m["content"], list) for m in s.msgs):
|
|
47
|
+
for m in s.msgs: # drop images so the chat can continue
|
|
48
|
+
if isinstance(m["content"], list):
|
|
49
|
+
m["content"] = m["content"][0]["text"] + "\n[image removed: model can't see images]"
|
|
50
|
+
msg = "this model can't read images. Pick one marked [vision] (ctrl+o), or e.g. gemma3:4b on Ollama"
|
|
51
|
+
s.say(f"▸ error: {msg}")
|
|
52
|
+
s.busy = False; s.set_state("")
|
|
53
|
+
|
|
54
|
+
def step(s):
|
|
55
|
+
md = think_box = think_w = None
|
|
56
|
+
raw, reasoning, calls, block = "", "", {}, None
|
|
57
|
+
last, first, think_t, n, final_tps = 0.0, None, None, 0, None
|
|
58
|
+
s.set_state("waiting…")
|
|
59
|
+
for ch in s.srv.stream(s.msgs, s.native, s.stop_ev):
|
|
60
|
+
if u := ch.get("usage"):
|
|
61
|
+
s.used = u.get("total_tokens") or u.get("prompt_tokens", 0) + u.get("completion_tokens", 0)
|
|
62
|
+
if u.get("completion_tokens") and first: final_tps = u["completion_tokens"] / max(time.time() - first, 1e-3)
|
|
63
|
+
if (tm := ch.get("timings")) and tm.get("predicted_per_second"): final_tps = tm["predicted_per_second"] # llama.cpp
|
|
64
|
+
if not ch.get("choices"): continue
|
|
65
|
+
d = ch["choices"][0].get("delta") or {}
|
|
66
|
+
piece, rpiece = d.get("content") or "", d.get("reasoning_content") or d.get("reasoning") or ""
|
|
67
|
+
raw += piece; reasoning += rpiece
|
|
68
|
+
for tc in d.get("tool_calls") or []:
|
|
69
|
+
c = calls.setdefault(tc.get("index", 0), {"id": "", "name": "", "args": ""})
|
|
70
|
+
f = tc.get("function") or {}
|
|
71
|
+
c["id"] = tc.get("id") or c["id"]; c["name"] += f.get("name") or ""; c["args"] += f.get("arguments") or ""
|
|
72
|
+
if piece or rpiece or d.get("tool_calls"):
|
|
73
|
+
n += 1; first = first or time.time()
|
|
74
|
+
think, text = s.split(reasoning, raw)
|
|
75
|
+
if not s.native and (block := BLOCK.search(text)):
|
|
76
|
+
break
|
|
77
|
+
if time.time() - last > 0.08:
|
|
78
|
+
last = time.time()
|
|
79
|
+
if n > 1: s.tps = (n - 1) / max(last - first, 1e-3) # ~1 token per streamed chunk
|
|
80
|
+
if think:
|
|
81
|
+
think_t = think_t or time.time()
|
|
82
|
+
if not think_box: think_box, think_w = s.ui(s.add_think)
|
|
83
|
+
s.ui(s.render, think_w, think)
|
|
84
|
+
if text:
|
|
85
|
+
if not md: md = s.ui(s.add, Markdown(""))
|
|
86
|
+
if think_box and not think_box.collapsed: s.ui(s.end_think, think_box, time.time() - think_t)
|
|
87
|
+
s.ui(s.render, md, text)
|
|
88
|
+
s.set_state("writing…" if text else "thinking…" if think else "waiting…")
|
|
89
|
+
think, text = s.split(reasoning, raw)
|
|
90
|
+
if block: text = text[:block.end()]
|
|
91
|
+
if final_tps: s.tps = final_tps
|
|
92
|
+
if think:
|
|
93
|
+
if not think_box: think_box, think_w = s.ui(s.add_think)
|
|
94
|
+
s.ui(s.render, think_w, think); s.ui(s.end_think, think_box, time.time() - (think_t or first or time.time()))
|
|
95
|
+
if text or not calls:
|
|
96
|
+
if not md: md = s.ui(s.add, Markdown(""))
|
|
97
|
+
s.ui(s.render, md, text or "_(empty)_")
|
|
98
|
+
if s.stop_ev.is_set():
|
|
99
|
+
s.push({"role": "assistant", "content": text}); s.say("▸ stopped"); return True
|
|
100
|
+
if calls:
|
|
101
|
+
s.push({"role": "assistant", "content": text, "tool_calls": [
|
|
102
|
+
{"id": c["id"] or f"call{i}", "type": "function", "function": {"name": c["name"], "arguments": c["args"]}}
|
|
103
|
+
for i, c in calls.items()]})
|
|
104
|
+
for i, c in calls.items():
|
|
105
|
+
try: cmd = json.loads(c["args"] or "{}").get("command", "")
|
|
106
|
+
except json.JSONDecodeError: cmd = c["args"]
|
|
107
|
+
s.push({"role": "tool", "tool_call_id": c["id"] or f"call{i}", "content": s.exec_cmd(cmd)})
|
|
108
|
+
return s.stop_ev.is_set()
|
|
109
|
+
s.push({"role": "assistant", "content": text})
|
|
110
|
+
if block:
|
|
111
|
+
s.push({"role": "user", "content": "Output:\n" + s.exec_cmd(to_cmd(block))})
|
|
112
|
+
return s.stop_ev.is_set()
|
|
113
|
+
for p in mentioned_images(text): s.ui(s.show_image, p)
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def split(reasoning, raw): # reasoning field (llama.cpp/Ollama) + inline <think> tags -> (thinking, answer)
|
|
118
|
+
think, text = split_think(raw)
|
|
119
|
+
return "\n".join(x for x in (reasoning.strip(), think) if x), text
|