milo-cli 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.
- milo_cli-0.1.0/PKG-INFO +441 -0
- milo_cli-0.1.0/README.md +414 -0
- milo_cli-0.1.0/pyproject.toml +116 -0
- milo_cli-0.1.0/setup.cfg +4 -0
- milo_cli-0.1.0/src/milo/__init__.py +183 -0
- milo_cli-0.1.0/src/milo/_child.py +141 -0
- milo_cli-0.1.0/src/milo/_errors.py +189 -0
- milo_cli-0.1.0/src/milo/_protocols.py +37 -0
- milo_cli-0.1.0/src/milo/_types.py +234 -0
- milo_cli-0.1.0/src/milo/app.py +353 -0
- milo_cli-0.1.0/src/milo/cli.py +134 -0
- milo_cli-0.1.0/src/milo/commands.py +951 -0
- milo_cli-0.1.0/src/milo/config.py +250 -0
- milo_cli-0.1.0/src/milo/context.py +81 -0
- milo_cli-0.1.0/src/milo/dev.py +238 -0
- milo_cli-0.1.0/src/milo/flow.py +146 -0
- milo_cli-0.1.0/src/milo/form.py +277 -0
- milo_cli-0.1.0/src/milo/gateway.py +393 -0
- milo_cli-0.1.0/src/milo/groups.py +194 -0
- milo_cli-0.1.0/src/milo/help.py +84 -0
- milo_cli-0.1.0/src/milo/input/__init__.py +6 -0
- milo_cli-0.1.0/src/milo/input/_platform.py +81 -0
- milo_cli-0.1.0/src/milo/input/_reader.py +93 -0
- milo_cli-0.1.0/src/milo/input/_sequences.py +63 -0
- milo_cli-0.1.0/src/milo/llms.py +172 -0
- milo_cli-0.1.0/src/milo/mcp.py +299 -0
- milo_cli-0.1.0/src/milo/middleware.py +67 -0
- milo_cli-0.1.0/src/milo/observability.py +111 -0
- milo_cli-0.1.0/src/milo/output.py +106 -0
- milo_cli-0.1.0/src/milo/pipeline.py +276 -0
- milo_cli-0.1.0/src/milo/plugins.py +168 -0
- milo_cli-0.1.0/src/milo/py.typed +0 -0
- milo_cli-0.1.0/src/milo/registry.py +213 -0
- milo_cli-0.1.0/src/milo/schema.py +214 -0
- milo_cli-0.1.0/src/milo/state.py +229 -0
- milo_cli-0.1.0/src/milo/streaming.py +41 -0
- milo_cli-0.1.0/src/milo/templates/__init__.py +38 -0
- milo_cli-0.1.0/src/milo/templates/error.kida +5 -0
- milo_cli-0.1.0/src/milo/templates/field_confirm.kida +1 -0
- milo_cli-0.1.0/src/milo/templates/field_select.kida +3 -0
- milo_cli-0.1.0/src/milo/templates/field_text.kida +1 -0
- milo_cli-0.1.0/src/milo/templates/form.kida +8 -0
- milo_cli-0.1.0/src/milo/templates/help.kida +7 -0
- milo_cli-0.1.0/src/milo/templates/progress.kida +1 -0
- milo_cli-0.1.0/src/milo/testing/__init__.py +27 -0
- milo_cli-0.1.0/src/milo/testing/_mcp.py +87 -0
- milo_cli-0.1.0/src/milo/testing/_record.py +125 -0
- milo_cli-0.1.0/src/milo/testing/_replay.py +68 -0
- milo_cli-0.1.0/src/milo/testing/_snapshot.py +96 -0
- milo_cli-0.1.0/src/milo_cli.egg-info/PKG-INFO +441 -0
- milo_cli-0.1.0/src/milo_cli.egg-info/SOURCES.txt +86 -0
- milo_cli-0.1.0/src/milo_cli.egg-info/dependency_links.txt +1 -0
- milo_cli-0.1.0/src/milo_cli.egg-info/entry_points.txt +2 -0
- milo_cli-0.1.0/src/milo_cli.egg-info/requires.txt +10 -0
- milo_cli-0.1.0/src/milo_cli.egg-info/top_level.txt +1 -0
- milo_cli-0.1.0/tests/test_ai_native.py +664 -0
- milo_cli-0.1.0/tests/test_app.py +408 -0
- milo_cli-0.1.0/tests/test_child.py +108 -0
- milo_cli-0.1.0/tests/test_cli.py +169 -0
- milo_cli-0.1.0/tests/test_components.py +206 -0
- milo_cli-0.1.0/tests/test_config.py +285 -0
- milo_cli-0.1.0/tests/test_context.py +314 -0
- milo_cli-0.1.0/tests/test_dev.py +297 -0
- milo_cli-0.1.0/tests/test_effects.py +106 -0
- milo_cli-0.1.0/tests/test_errors.py +106 -0
- milo_cli-0.1.0/tests/test_flow.py +170 -0
- milo_cli-0.1.0/tests/test_form.py +438 -0
- milo_cli-0.1.0/tests/test_groups.py +329 -0
- milo_cli-0.1.0/tests/test_help.py +99 -0
- milo_cli-0.1.0/tests/test_input.py +369 -0
- milo_cli-0.1.0/tests/test_lazy.py +332 -0
- milo_cli-0.1.0/tests/test_mcp_prompts.py +86 -0
- milo_cli-0.1.0/tests/test_mcp_resources.py +84 -0
- milo_cli-0.1.0/tests/test_middleware.py +122 -0
- milo_cli-0.1.0/tests/test_milo_init.py +273 -0
- milo_cli-0.1.0/tests/test_mount.py +97 -0
- milo_cli-0.1.0/tests/test_observability.py +134 -0
- milo_cli-0.1.0/tests/test_pipeline.py +299 -0
- milo_cli-0.1.0/tests/test_plugins.py +214 -0
- milo_cli-0.1.0/tests/test_protocols.py +106 -0
- milo_cli-0.1.0/tests/test_registry_v2.py +126 -0
- milo_cli-0.1.0/tests/test_schema_v2.py +207 -0
- milo_cli-0.1.0/tests/test_state.py +352 -0
- milo_cli-0.1.0/tests/test_streaming.py +84 -0
- milo_cli-0.1.0/tests/test_templates.py +92 -0
- milo_cli-0.1.0/tests/test_testing.py +389 -0
- milo_cli-0.1.0/tests/test_testing_mcp.py +118 -0
- milo_cli-0.1.0/tests/test_types.py +222 -0
milo_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: milo-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Template-driven CLI applications for free-threaded Python
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://lbliii.github.io/milo/
|
|
7
|
+
Project-URL: Documentation, https://lbliii.github.io/milo/
|
|
8
|
+
Project-URL: Repository, https://github.com/lbliii/milo
|
|
9
|
+
Project-URL: Changelog, https://github.com/lbliii/milo/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: cli,terminal,forms,free-threading,template,elm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Terminals
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.14
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: kida-templates>=0.3.0
|
|
21
|
+
Provides-Extra: docs
|
|
22
|
+
Requires-Dist: bengal>=0.2.6; extra == "docs"
|
|
23
|
+
Provides-Extra: yaml
|
|
24
|
+
Requires-Dist: pyyaml>=6.0; extra == "yaml"
|
|
25
|
+
Provides-Extra: watch
|
|
26
|
+
Requires-Dist: watchfiles>=1.0; extra == "watch"
|
|
27
|
+
|
|
28
|
+
# ᗣᗣ Milo
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/milo/)
|
|
31
|
+
[](https://github.com/lbliii/milo/actions/workflows/tests.yml)
|
|
32
|
+
[](https://pypi.org/project/milo/)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
|
|
35
|
+
**Template-driven CLI applications for free-threaded Python**
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from milo import App, Action
|
|
39
|
+
|
|
40
|
+
def reducer(state, action):
|
|
41
|
+
if state is None:
|
|
42
|
+
return {"count": 0}
|
|
43
|
+
if action.type == "@@KEY" and action.payload.char == " ":
|
|
44
|
+
return {**state, "count": state["count"] + 1}
|
|
45
|
+
return state
|
|
46
|
+
|
|
47
|
+
app = App(template="counter.kida", reducer=reducer, initial_state=None)
|
|
48
|
+
final_state = app.run()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## What is Milo?
|
|
54
|
+
|
|
55
|
+
Milo is a framework for building interactive terminal applications in Python 3.14t. It uses the Elm Architecture (Model-View-Update) — an immutable state tree managed by pure reducer functions, a view layer driven by Kida templates, and generator-based sagas for side effects. The result is CLI apps that are predictable, testable, and free-threading ready.
|
|
56
|
+
|
|
57
|
+
**Why people pick it:**
|
|
58
|
+
|
|
59
|
+
- **Elm Architecture** — Immutable state, pure reducers, declarative views. Every state transition is explicit and testable.
|
|
60
|
+
- **Template-driven UI** — Render terminal output with Kida templates. Same syntax you use for HTML, now for CLI.
|
|
61
|
+
- **Free-threading ready** — Built for Python 3.14t (PEP 703). Sagas run on `ThreadPoolExecutor` with no GIL contention.
|
|
62
|
+
- **Declarative flows** — Chain multi-screen state machines with the `>>` operator. No manual navigation plumbing.
|
|
63
|
+
- **Built-in forms** — Text, select, confirm, and password fields with validation, keyboard navigation, and TTY fallback.
|
|
64
|
+
- **One runtime dependency** — Just `kida-templates`. No click, no rich, no curses.
|
|
65
|
+
|
|
66
|
+
## Use Milo For
|
|
67
|
+
|
|
68
|
+
- **Interactive CLI tools** — Wizards, installers, configuration prompts, and guided workflows
|
|
69
|
+
- **Multi-screen terminal apps** — Declarative flows with `>>` operator for screen-to-screen navigation
|
|
70
|
+
- **Forms and data collection** — Text, select, confirm, and password fields with validation
|
|
71
|
+
- **Dev tools with hot reload** — `milo dev` watches templates and live-reloads on change
|
|
72
|
+
- **Session recording and replay** — Record user sessions to JSONL, replay for debugging or CI regression tests
|
|
73
|
+
- **Styled terminal output** — Kida terminal templates with ANSI colors, progress bars, and live rendering
|
|
74
|
+
- **AI agent integration** — Every CLI is an MCP server; register multiple CLIs behind a single gateway
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install milo
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Requires Python 3.14+
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Quick Start
|
|
89
|
+
|
|
90
|
+
| Function | Description |
|
|
91
|
+
|----------|-------------|
|
|
92
|
+
| `App(template, reducer, initial_state)` | Create a single-screen app |
|
|
93
|
+
| `App.from_flow(flow)` | Create a multi-screen app from a `Flow` |
|
|
94
|
+
| `app.run()` | Run the event loop, return final state |
|
|
95
|
+
| `Store(reducer, initial_state)` | Standalone state container |
|
|
96
|
+
| `combine_reducers(**reducers)` | Compose slice-based reducers |
|
|
97
|
+
| `form(*specs)` | Run an interactive form, return `{field: value}` |
|
|
98
|
+
| `FlowScreen(name, template, reducer)` | Define a named screen |
|
|
99
|
+
| `flow = screen_a >> screen_b` | Chain screens into a flow |
|
|
100
|
+
| `render_html(state, template)` | One-shot static HTML render |
|
|
101
|
+
| `DevServer(app, watch_dirs)` | Hot-reload dev server |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Features
|
|
106
|
+
|
|
107
|
+
| Feature | Description | Docs |
|
|
108
|
+
|---------|-------------|------|
|
|
109
|
+
| **State Management** | Redux-style `Store` with dispatch, listeners, middleware, and saga scheduling | [State →](https://lbliii.github.io/milo/docs/usage/state/) |
|
|
110
|
+
| **Sagas** | Generator-based side effects: `Call`, `Put`, `Select`, `Fork`, `Delay` | [Sagas →](https://lbliii.github.io/milo/docs/usage/sagas/) |
|
|
111
|
+
| **Flows** | Multi-screen state machines with `>>` operator and custom transitions | [Flows →](https://lbliii.github.io/milo/docs/usage/flows/) |
|
|
112
|
+
| **Forms** | Text, select, confirm, password fields with validation and TTY fallback | [Forms →](https://lbliii.github.io/milo/docs/usage/forms/) |
|
|
113
|
+
| **Input Handling** | Cross-platform key reader with full escape sequence support (arrows, F-keys, modifiers) | [Input →](https://lbliii.github.io/milo/docs/usage/input/) |
|
|
114
|
+
| **Templates** | Kida-powered terminal rendering with built-in form, field, help, and progress templates | [Templates →](https://lbliii.github.io/milo/docs/usage/templates/) |
|
|
115
|
+
| **Dev Server** | `milo dev` with filesystem polling and `@@HOT_RELOAD` dispatch | [Dev →](https://lbliii.github.io/milo/docs/usage/dev/) |
|
|
116
|
+
| **Session Recording** | JSONL action log with state hashes for debugging and regression testing | [Testing →](https://lbliii.github.io/milo/docs/usage/testing/) |
|
|
117
|
+
| **Replay** | Time-travel debugging, speed control, step-by-step mode, CI hash assertions | [Testing →](https://lbliii.github.io/milo/docs/usage/testing/) |
|
|
118
|
+
| **Snapshot Testing** | `assert_renders`, `assert_state`, `assert_saga` for deterministic test coverage | [Testing →](https://lbliii.github.io/milo/docs/usage/testing/) |
|
|
119
|
+
| **Help Rendering** | `HelpRenderer` — drop-in `argparse.HelpFormatter` using Kida templates | [Help →](https://lbliii.github.io/milo/docs/usage/help/) |
|
|
120
|
+
| **MCP Server** | Every CLI doubles as an MCP server — AI agents discover and call commands via JSON-RPC | [MCP →](https://lbliii.github.io/milo/docs/usage/mcp/) |
|
|
121
|
+
| **MCP Gateway** | Single gateway aggregates all registered Milo CLIs for unified AI agent access | [MCP →](https://lbliii.github.io/milo/docs/usage/mcp/) |
|
|
122
|
+
| **llms.txt** | Generate AI-readable discovery documents from CLI command definitions | [llms.txt →](https://lbliii.github.io/milo/docs/usage/llms/) |
|
|
123
|
+
| **Error System** | Structured error hierarchy with namespaced codes (`M-INP-001`, `M-STA-003`) | [Errors →](https://lbliii.github.io/milo/docs/reference/errors/) |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Usage
|
|
128
|
+
|
|
129
|
+
<details>
|
|
130
|
+
<summary><strong>Single-Screen App</strong> — Counter with keyboard input</summary>
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from milo import App, Action
|
|
134
|
+
|
|
135
|
+
def reducer(state, action):
|
|
136
|
+
if state is None:
|
|
137
|
+
return {"count": 0}
|
|
138
|
+
if action.type == "@@KEY" and action.payload.char == " ":
|
|
139
|
+
return {**state, "count": state["count"] + 1}
|
|
140
|
+
return state
|
|
141
|
+
|
|
142
|
+
app = App(template="counter.kida", reducer=reducer, initial_state=None)
|
|
143
|
+
final_state = app.run()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**counter.kida:**
|
|
147
|
+
```
|
|
148
|
+
Count: {{ count }}
|
|
149
|
+
|
|
150
|
+
Press SPACE to increment, Ctrl+C to quit.
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
<details>
|
|
156
|
+
<summary><strong>Multi-Screen Flow</strong> — Chain screens with <code>>></code></summary>
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from milo import App
|
|
160
|
+
from milo.flow import FlowScreen
|
|
161
|
+
|
|
162
|
+
welcome = FlowScreen("welcome", "welcome.kida", welcome_reducer)
|
|
163
|
+
config = FlowScreen("config", "config.kida", config_reducer)
|
|
164
|
+
confirm = FlowScreen("confirm", "confirm.kida", confirm_reducer)
|
|
165
|
+
|
|
166
|
+
flow = welcome >> config >> confirm
|
|
167
|
+
app = App.from_flow(flow)
|
|
168
|
+
app.run()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Navigate between screens by dispatching `@@NAVIGATE` actions from your reducers. Add custom transitions with `flow.with_transition("welcome", "confirm", on="@@SKIP")`.
|
|
172
|
+
|
|
173
|
+
</details>
|
|
174
|
+
|
|
175
|
+
<details>
|
|
176
|
+
<summary><strong>Interactive Forms</strong> — Collect structured input</summary>
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from milo import form, FieldSpec, FieldType
|
|
180
|
+
|
|
181
|
+
result = form(
|
|
182
|
+
FieldSpec("name", "Your name"),
|
|
183
|
+
FieldSpec("env", "Environment", field_type=FieldType.SELECT,
|
|
184
|
+
choices=("dev", "staging", "prod")),
|
|
185
|
+
FieldSpec("confirm", "Deploy?", field_type=FieldType.CONFIRM),
|
|
186
|
+
)
|
|
187
|
+
# result = {"name": "Alice", "env": "prod", "confirm": True}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Tab/Shift+Tab navigates fields. Arrow keys cycle select options. Falls back to plain `input()` prompts when stdin is not a TTY.
|
|
191
|
+
|
|
192
|
+
</details>
|
|
193
|
+
|
|
194
|
+
<details>
|
|
195
|
+
<summary><strong>Sagas</strong> — Generator-based side effects</summary>
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from milo import Call, Put, Select, ReducerResult
|
|
199
|
+
|
|
200
|
+
def fetch_saga():
|
|
201
|
+
url = yield Select(lambda s: s["url"])
|
|
202
|
+
data = yield Call(fetch_json, (url,))
|
|
203
|
+
yield Put(Action("FETCH_DONE", payload=data))
|
|
204
|
+
|
|
205
|
+
def reducer(state, action):
|
|
206
|
+
if action.type == "@@KEY" and action.payload.char == "f":
|
|
207
|
+
return ReducerResult({**state, "loading": True}, sagas=(fetch_saga,))
|
|
208
|
+
if action.type == "FETCH_DONE":
|
|
209
|
+
return {**state, "loading": False, "data": action.payload}
|
|
210
|
+
return state
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Effects: `Call(fn, args)`, `Put(action)`, `Select(selector)`, `Fork(saga)`, `Delay(seconds)`.
|
|
214
|
+
|
|
215
|
+
</details>
|
|
216
|
+
|
|
217
|
+
<details>
|
|
218
|
+
<summary><strong>Middleware</strong> — Intercept and transform dispatches</summary>
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
def logging_middleware(dispatch, get_state):
|
|
222
|
+
def wrapper(action):
|
|
223
|
+
print(f"Action: {action.type}")
|
|
224
|
+
return dispatch(action)
|
|
225
|
+
return wrapper
|
|
226
|
+
|
|
227
|
+
app = App(
|
|
228
|
+
template="app.kida",
|
|
229
|
+
reducer=reducer,
|
|
230
|
+
initial_state=None,
|
|
231
|
+
middleware=[logging_middleware],
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
</details>
|
|
236
|
+
|
|
237
|
+
<details>
|
|
238
|
+
<summary><strong>Dev Server</strong> — Hot reload templates</summary>
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# Watch templates and reload on change
|
|
242
|
+
milo dev myapp:app --watch ./templates --poll 0.25
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from milo import App, DevServer
|
|
247
|
+
|
|
248
|
+
app = App(template="dashboard.kida", reducer=reducer, initial_state=None)
|
|
249
|
+
server = DevServer(app, watch_dirs=("./templates",), poll_interval=0.5)
|
|
250
|
+
server.run()
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
</details>
|
|
254
|
+
|
|
255
|
+
<details>
|
|
256
|
+
<summary><strong>Session Recording & Replay</strong> — Debug and regression testing</summary>
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
# Record a session
|
|
260
|
+
app = App(template="app.kida", reducer=reducer, initial_state=None, record=True)
|
|
261
|
+
app.run() # Writes to session.jsonl
|
|
262
|
+
|
|
263
|
+
# Replay for debugging
|
|
264
|
+
milo replay session.jsonl --speed 2.0 --diff
|
|
265
|
+
|
|
266
|
+
# CI regression: assert state hashes match
|
|
267
|
+
milo replay session.jsonl --assert --reducer myapp:reducer
|
|
268
|
+
|
|
269
|
+
# Step-by-step interactive replay
|
|
270
|
+
milo replay session.jsonl --step
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
</details>
|
|
274
|
+
|
|
275
|
+
<details>
|
|
276
|
+
<summary><strong>Testing Utilities</strong> — Snapshot, state, and saga assertions</summary>
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from milo.testing import assert_renders, assert_state, assert_saga
|
|
280
|
+
from milo import Action, Call
|
|
281
|
+
|
|
282
|
+
# Snapshot test: render state through template, compare to file
|
|
283
|
+
assert_renders({"count": 5}, "counter.kida", snapshot="tests/snapshots/count_5.txt")
|
|
284
|
+
|
|
285
|
+
# Reducer test: feed actions, assert final state
|
|
286
|
+
assert_state(reducer, None, [Action("@@INIT"), Action("INCREMENT")], {"count": 1})
|
|
287
|
+
|
|
288
|
+
# Saga test: step through generator, assert each yielded effect
|
|
289
|
+
assert_saga(my_saga(), [(Call(fetch, ("url",), {}), {"data": 42})])
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Set `MILO_UPDATE_SNAPSHOTS=1` to regenerate snapshot files.
|
|
293
|
+
|
|
294
|
+
</details>
|
|
295
|
+
|
|
296
|
+
<details>
|
|
297
|
+
<summary><strong>MCP Server & Gateway</strong> — AI agent integration</summary>
|
|
298
|
+
|
|
299
|
+
Every Milo CLI is automatically an MCP server:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Run as MCP server (stdin/stdout JSON-RPC)
|
|
303
|
+
myapp --mcp
|
|
304
|
+
|
|
305
|
+
# Register with an AI host directly
|
|
306
|
+
claude mcp add myapp -- uv run python examples/taskman/app.py --mcp
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
For multiple CLIs, register them and run a single gateway:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# Register CLIs
|
|
313
|
+
taskman --mcp-install
|
|
314
|
+
ghub --mcp-install
|
|
315
|
+
|
|
316
|
+
# Run the unified gateway
|
|
317
|
+
uv run python -m milo.gateway --mcp
|
|
318
|
+
|
|
319
|
+
# Or register the gateway with your AI host
|
|
320
|
+
claude mcp add milo -- uv run python -m milo.gateway --mcp
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The gateway namespaces tools automatically: `taskman.add`, `ghub.repo.list`, etc. Implements MCP 2025-11-25 with `outputSchema`, `structuredContent`, and tool `title` fields.
|
|
324
|
+
|
|
325
|
+
</details>
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Architecture
|
|
330
|
+
|
|
331
|
+
<details>
|
|
332
|
+
<summary><strong>Elm Architecture</strong> — Model-View-Update loop</summary>
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
┌──────────────┐
|
|
336
|
+
│ Terminal │
|
|
337
|
+
│ (View) │
|
|
338
|
+
└──────┬───────┘
|
|
339
|
+
│ Key events
|
|
340
|
+
▼
|
|
341
|
+
┌──────────┐ ┌──────────────────┐ ┌──────────────┐
|
|
342
|
+
│ Kida │◄───│ Store │◄───│ Reducer │
|
|
343
|
+
│ Template │ │ (State Tree) │ │ (Pure fn) │
|
|
344
|
+
└──────────┘ └──────────┬───────┘ └──────────────┘
|
|
345
|
+
│
|
|
346
|
+
▼
|
|
347
|
+
┌──────────────┐
|
|
348
|
+
│ Sagas │
|
|
349
|
+
│ (Side Effects│
|
|
350
|
+
│ on ThreadPool)
|
|
351
|
+
└──────────────┘
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
1. **Model** — Immutable state (plain dicts or frozen dataclasses)
|
|
355
|
+
2. **View** — Kida templates render state to terminal output
|
|
356
|
+
3. **Update** — Pure `reducer(state, action) -> state` functions
|
|
357
|
+
4. **Effects** — Generator-based sagas scheduled on `ThreadPoolExecutor`
|
|
358
|
+
|
|
359
|
+
</details>
|
|
360
|
+
|
|
361
|
+
<details>
|
|
362
|
+
<summary><strong>Event Loop</strong> — App lifecycle</summary>
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
App.run()
|
|
366
|
+
├── Store(reducer, initial_state)
|
|
367
|
+
├── KeyReader (raw mode, escape sequences → Key objects)
|
|
368
|
+
├── TerminalRenderer (alternate screen buffer, flicker-free updates)
|
|
369
|
+
├── Optional: tick thread (@@TICK at interval)
|
|
370
|
+
├── Optional: SIGWINCH handler (@@RESIZE)
|
|
371
|
+
└── Loop:
|
|
372
|
+
read key → dispatch @@KEY → reducer → re-render
|
|
373
|
+
until state.submitted or @@QUIT
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
</details>
|
|
377
|
+
|
|
378
|
+
<details>
|
|
379
|
+
<summary><strong>Builtin Actions</strong> — Event vocabulary</summary>
|
|
380
|
+
|
|
381
|
+
| Action | Trigger | Payload |
|
|
382
|
+
|--------|---------|---------|
|
|
383
|
+
| `@@INIT` | Store creation | — |
|
|
384
|
+
| `@@KEY` | Keyboard input | `Key(char, name, ctrl, alt, shift)` |
|
|
385
|
+
| `@@TICK` | Timer interval | — |
|
|
386
|
+
| `@@RESIZE` | Terminal resize | `(cols, rows)` |
|
|
387
|
+
| `@@NAVIGATE` | Screen transition | `screen_name` |
|
|
388
|
+
| `@@HOT_RELOAD` | Template file change | `file_path` |
|
|
389
|
+
| `@@EFFECT_RESULT` | Saga completion | `result` |
|
|
390
|
+
| `@@QUIT` | Ctrl+C | — |
|
|
391
|
+
|
|
392
|
+
</details>
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Documentation
|
|
397
|
+
|
|
398
|
+
| Section | Description |
|
|
399
|
+
|---------|-------------|
|
|
400
|
+
| [Get Started](https://lbliii.github.io/milo/docs/get-started/) | Installation and quickstart |
|
|
401
|
+
| [Usage](https://lbliii.github.io/milo/docs/usage/) | State, sagas, flows, forms, templates |
|
|
402
|
+
| [Testing](https://lbliii.github.io/milo/docs/usage/testing/) | Snapshots, recording, replay |
|
|
403
|
+
| [MCP & AI](https://lbliii.github.io/milo/docs/usage/mcp/) | MCP server, gateway, and llms.txt |
|
|
404
|
+
| [Reference](https://lbliii.github.io/milo/docs/reference/) | Complete API documentation |
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Development
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
git clone https://github.com/lbliii/milo.git
|
|
412
|
+
cd milo
|
|
413
|
+
# Uses Python 3.14t by default (.python-version)
|
|
414
|
+
uv sync --group dev --python 3.14t
|
|
415
|
+
PYTHON_GIL=0 uv run --python 3.14t pytest
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## The Bengal Ecosystem
|
|
421
|
+
|
|
422
|
+
A structured reactive stack — every layer written in pure Python for 3.14t free-threading.
|
|
423
|
+
|
|
424
|
+
| | | | |
|
|
425
|
+
|--:|---|---|---|
|
|
426
|
+
| **ᓚᘏᗢ** | [Bengal](https://github.com/lbliii/bengal) | Static site generator | [Docs](https://lbliii.github.io/bengal/) |
|
|
427
|
+
| **∿∿** | [Purr](https://github.com/lbliii/purr) | Content runtime | — |
|
|
428
|
+
| **⌁⌁** | [Chirp](https://github.com/lbliii/chirp) | Web framework | [Docs](https://lbliii.github.io/chirp/) |
|
|
429
|
+
| **=^..^=** | [Pounce](https://github.com/lbliii/pounce) | ASGI server | [Docs](https://lbliii.github.io/pounce/) |
|
|
430
|
+
| **)彡** | [Kida](https://github.com/lbliii/kida) | Template engine | [Docs](https://lbliii.github.io/kida/) |
|
|
431
|
+
| **ฅᨐฅ** | [Patitas](https://github.com/lbliii/patitas) | Markdown parser | [Docs](https://lbliii.github.io/patitas/) |
|
|
432
|
+
| **⌾⌾⌾** | [Rosettes](https://github.com/lbliii/rosettes) | Syntax highlighter | [Docs](https://lbliii.github.io/rosettes/) |
|
|
433
|
+
| **ᗣᗣ** | **Milo** | CLI framework ← You are here | [Docs](https://lbliii.github.io/milo/) |
|
|
434
|
+
|
|
435
|
+
Python-native. Free-threading ready. No npm required.
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## License
|
|
440
|
+
|
|
441
|
+
MIT License — see [LICENSE](LICENSE) for details.
|