Skip to main content

Template-driven CLI applications for free-threaded Python

Project description

ᗣᗣ Milo

PyPI version Build Status Python 3.14+ License: MIT

Template-driven CLI applications for free-threaded Python

from milo import App, Action

def reducer(state, action):
    if state is None:
        return {"count": 0}
    if action.type == "@@KEY" and action.payload.char == " ":
        return {**state, "count": state["count"] + 1}
    return state

app = App(template="counter.kida", reducer=reducer, initial_state=None)
final_state = app.run()

What is Milo?

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.

Why people pick it:

  • Elm Architecture — Immutable state, pure reducers, declarative views. Every state transition is explicit and testable.
  • Template-driven UI — Render terminal output with Kida templates. Same syntax you use for HTML, now for CLI.
  • Free-threading ready — Built for Python 3.14t (PEP 703). Sagas run on ThreadPoolExecutor with no GIL contention.
  • Declarative flows — Chain multi-screen state machines with the >> operator. No manual navigation plumbing.
  • Built-in forms — Text, select, confirm, and password fields with validation, keyboard navigation, and TTY fallback.
  • One runtime dependency — Just kida-templates. No click, no rich, no curses.

Use Milo For

  • Interactive CLI tools — Wizards, installers, configuration prompts, and guided workflows
  • Multi-screen terminal apps — Declarative flows with >> operator for screen-to-screen navigation
  • Forms and data collection — Text, select, confirm, and password fields with validation
  • Dev tools with hot reloadmilo dev watches templates and live-reloads on change
  • Session recording and replay — Record user sessions to JSONL, replay for debugging or CI regression tests
  • Styled terminal output — Kida terminal templates with ANSI colors, progress bars, and live rendering
  • AI agent integration — Every CLI is an MCP server; register multiple CLIs behind a single gateway

Installation

pip install milo

Requires Python 3.14+


Quick Start

Function Description
App(template, reducer, initial_state) Create a single-screen app
App.from_flow(flow) Create a multi-screen app from a Flow
app.run() Run the event loop, return final state
Store(reducer, initial_state) Standalone state container
combine_reducers(**reducers) Compose slice-based reducers
form(*specs) Run an interactive form, return {field: value}
FlowScreen(name, template, reducer) Define a named screen
flow = screen_a >> screen_b Chain screens into a flow
render_html(state, template) One-shot static HTML render
DevServer(app, watch_dirs) Hot-reload dev server

Features

Feature Description Docs
State Management Redux-style Store with dispatch, listeners, middleware, and saga scheduling State →
Sagas Generator-based side effects: Call, Put, Select, Fork, Delay Sagas →
Flows Multi-screen state machines with >> operator and custom transitions Flows →
Forms Text, select, confirm, password fields with validation and TTY fallback Forms →
Input Handling Cross-platform key reader with full escape sequence support (arrows, F-keys, modifiers) Input →
Templates Kida-powered terminal rendering with built-in form, field, help, and progress templates Templates →
Dev Server milo dev with filesystem polling and @@HOT_RELOAD dispatch Dev →
Session Recording JSONL action log with state hashes for debugging and regression testing Testing →
Replay Time-travel debugging, speed control, step-by-step mode, CI hash assertions Testing →
Snapshot Testing assert_renders, assert_state, assert_saga for deterministic test coverage Testing →
Help Rendering HelpRenderer — drop-in argparse.HelpFormatter using Kida templates Help →
MCP Server Every CLI doubles as an MCP server — AI agents discover and call commands via JSON-RPC MCP →
MCP Gateway Single gateway aggregates all registered Milo CLIs for unified AI agent access MCP →
llms.txt Generate AI-readable discovery documents from CLI command definitions llms.txt →
Error System Structured error hierarchy with namespaced codes (M-INP-001, M-STA-003) Errors →

Usage

Single-Screen App — Counter with keyboard input
from milo import App, Action

def reducer(state, action):
    if state is None:
        return {"count": 0}
    if action.type == "@@KEY" and action.payload.char == " ":
        return {**state, "count": state["count"] + 1}
    return state

app = App(template="counter.kida", reducer=reducer, initial_state=None)
final_state = app.run()

counter.kida:

Count: {{ count }}

Press SPACE to increment, Ctrl+C to quit.
Multi-Screen Flow — Chain screens with >>
from milo import App
from milo.flow import FlowScreen

welcome = FlowScreen("welcome", "welcome.kida", welcome_reducer)
config = FlowScreen("config", "config.kida", config_reducer)
confirm = FlowScreen("confirm", "confirm.kida", confirm_reducer)

flow = welcome >> config >> confirm
app = App.from_flow(flow)
app.run()

Navigate between screens by dispatching @@NAVIGATE actions from your reducers. Add custom transitions with flow.with_transition("welcome", "confirm", on="@@SKIP").

Interactive Forms — Collect structured input
from milo import form, FieldSpec, FieldType

result = form(
    FieldSpec("name", "Your name"),
    FieldSpec("env", "Environment", field_type=FieldType.SELECT,
              choices=("dev", "staging", "prod")),
    FieldSpec("confirm", "Deploy?", field_type=FieldType.CONFIRM),
)
# result = {"name": "Alice", "env": "prod", "confirm": True}

Tab/Shift+Tab navigates fields. Arrow keys cycle select options. Falls back to plain input() prompts when stdin is not a TTY.

Sagas — Generator-based side effects
from milo import Call, Put, Select, ReducerResult

def fetch_saga():
    url = yield Select(lambda s: s["url"])
    data = yield Call(fetch_json, (url,))
    yield Put(Action("FETCH_DONE", payload=data))

def reducer(state, action):
    if action.type == "@@KEY" and action.payload.char == "f":
        return ReducerResult({**state, "loading": True}, sagas=(fetch_saga,))
    if action.type == "FETCH_DONE":
        return {**state, "loading": False, "data": action.payload}
    return state

Effects: Call(fn, args), Put(action), Select(selector), Fork(saga), Delay(seconds).

Middleware — Intercept and transform dispatches
def logging_middleware(dispatch, get_state):
    def wrapper(action):
        print(f"Action: {action.type}")
        return dispatch(action)
    return wrapper

app = App(
    template="app.kida",
    reducer=reducer,
    initial_state=None,
    middleware=[logging_middleware],
)
Dev Server — Hot reload templates
# Watch templates and reload on change
milo dev myapp:app --watch ./templates --poll 0.25
from milo import App, DevServer

app = App(template="dashboard.kida", reducer=reducer, initial_state=None)
server = DevServer(app, watch_dirs=("./templates",), poll_interval=0.5)
server.run()
Session Recording & Replay — Debug and regression testing
# Record a session
app = App(template="app.kida", reducer=reducer, initial_state=None, record=True)
app.run()  # Writes to session.jsonl

# Replay for debugging
milo replay session.jsonl --speed 2.0 --diff

# CI regression: assert state hashes match
milo replay session.jsonl --assert --reducer myapp:reducer

# Step-by-step interactive replay
milo replay session.jsonl --step
Testing Utilities — Snapshot, state, and saga assertions
from milo.testing import assert_renders, assert_state, assert_saga
from milo import Action, Call

# Snapshot test: render state through template, compare to file
assert_renders({"count": 5}, "counter.kida", snapshot="tests/snapshots/count_5.txt")

# Reducer test: feed actions, assert final state
assert_state(reducer, None, [Action("@@INIT"), Action("INCREMENT")], {"count": 1})

# Saga test: step through generator, assert each yielded effect
assert_saga(my_saga(), [(Call(fetch, ("url",), {}), {"data": 42})])

Set MILO_UPDATE_SNAPSHOTS=1 to regenerate snapshot files.

MCP Server & Gateway — AI agent integration

Every Milo CLI is automatically an MCP server:

# Run as MCP server (stdin/stdout JSON-RPC)
myapp --mcp

# Register with an AI host directly
claude mcp add myapp -- uv run python examples/taskman/app.py --mcp

For multiple CLIs, register them and run a single gateway:

# Register CLIs
taskman --mcp-install
ghub --mcp-install

# Run the unified gateway
uv run python -m milo.gateway --mcp

# Or register the gateway with your AI host
claude mcp add milo -- uv run python -m milo.gateway --mcp

The gateway namespaces tools automatically: taskman.add, ghub.repo.list, etc. Implements MCP 2025-11-25 with outputSchema, structuredContent, and tool title fields.


Architecture

Elm Architecture — Model-View-Update loop
                    ┌──────────────┐
                    │   Terminal    │
                    │   (View)     │
                    └──────┬───────┘
                           │ Key events
                           ▼
┌──────────┐    ┌──────────────────┐    ┌──────────────┐
│  Kida    │◄───│      Store       │◄───│   Reducer    │
│ Template │    │  (State Tree)    │    │  (Pure fn)   │
└──────────┘    └──────────┬───────┘    └──────────────┘
                           │
                           ▼
                    ┌──────────────┐
                    │    Sagas     │
                    │ (Side Effects│
                    │  on ThreadPool)
                    └──────────────┘
  1. Model — Immutable state (plain dicts or frozen dataclasses)
  2. View — Kida templates render state to terminal output
  3. Update — Pure reducer(state, action) -> state functions
  4. Effects — Generator-based sagas scheduled on ThreadPoolExecutor
Event Loop — App lifecycle
App.run()
  ├── Store(reducer, initial_state)
  ├── KeyReader (raw mode, escape sequences → Key objects)
  ├── TerminalRenderer (alternate screen buffer, flicker-free updates)
  ├── Optional: tick thread (@@TICK at interval)
  ├── Optional: SIGWINCH handler (@@RESIZE)
  └── Loop:
        read key → dispatch @@KEY → reducer → re-render
        until state.submitted or @@QUIT
Builtin Actions — Event vocabulary
Action Trigger Payload
@@INIT Store creation
@@KEY Keyboard input Key(char, name, ctrl, alt, shift)
@@TICK Timer interval
@@RESIZE Terminal resize (cols, rows)
@@NAVIGATE Screen transition screen_name
@@HOT_RELOAD Template file change file_path
@@EFFECT_RESULT Saga completion result
@@QUIT Ctrl+C

Documentation

Section Description
Get Started Installation and quickstart
Usage State, sagas, flows, forms, templates
Testing Snapshots, recording, replay
MCP & AI MCP server, gateway, and llms.txt
Reference Complete API documentation

Development

git clone https://github.com/lbliii/milo.git
cd milo
# Uses Python 3.14t by default (.python-version)
uv sync --group dev --python 3.14t
PYTHON_GIL=0 uv run --python 3.14t pytest

The Bengal Ecosystem

A structured reactive stack — every layer written in pure Python for 3.14t free-threading.

ᓚᘏᗢ Bengal Static site generator Docs
∿∿ Purr Content runtime
⌁⌁ Chirp Web framework Docs
=^..^= Pounce ASGI server Docs
)彡 Kida Template engine Docs
ฅᨐฅ Patitas Markdown parser Docs
⌾⌾⌾ Rosettes Syntax highlighter Docs
ᗣᗣ Milo CLI framework ← You are here Docs

Python-native. Free-threading ready. No npm required.


License

MIT License — see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

milo_cli-0.1.0.tar.gz (107.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

milo_cli-0.1.0-py3-none-any.whl (73.1 kB view details)

Uploaded Python 3

File details

Details for the file milo_cli-0.1.0.tar.gz.

File metadata

  • Download URL: milo_cli-0.1.0.tar.gz
  • Upload date:
  • Size: 107.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for milo_cli-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6457edcfc32da973714182630cedf6e5ed40af8a31c37a9fb7c5c3498d4b0145
MD5 bb1eb63c3aae5e4dcf634374f1c27b70
BLAKE2b-256 c92b7c611080a0be29ed38bd18bed3c62722dad8193bd2dc0f5e17082c2c8050

See more details on using hashes here.

Provenance

The following attestation bundles were made for milo_cli-0.1.0.tar.gz:

Publisher: python-publish.yml on lbliii/milo-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file milo_cli-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: milo_cli-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 73.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for milo_cli-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2a66ebeb5ec6e361f9d662a9b09377199eadd6669a191653f3e73e7e8d2352b2
MD5 19bda970215c5925fe56fbe8c54c3615
BLAKE2b-256 97ae4522cc13b0ab165d819a1f9106346ca2c30b50c098d79d79a1f400734b0f

See more details on using hashes here.

Provenance

The following attestation bundles were made for milo_cli-0.1.0-py3-none-any.whl:

Publisher: python-publish.yml on lbliii/milo-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page