milo-cli 0.1.0__py3-none-any.whl
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/__init__.py +183 -0
- milo/_child.py +141 -0
- milo/_errors.py +189 -0
- milo/_protocols.py +37 -0
- milo/_types.py +234 -0
- milo/app.py +353 -0
- milo/cli.py +134 -0
- milo/commands.py +951 -0
- milo/config.py +250 -0
- milo/context.py +81 -0
- milo/dev.py +238 -0
- milo/flow.py +146 -0
- milo/form.py +277 -0
- milo/gateway.py +393 -0
- milo/groups.py +194 -0
- milo/help.py +84 -0
- milo/input/__init__.py +6 -0
- milo/input/_platform.py +81 -0
- milo/input/_reader.py +93 -0
- milo/input/_sequences.py +63 -0
- milo/llms.py +172 -0
- milo/mcp.py +299 -0
- milo/middleware.py +67 -0
- milo/observability.py +111 -0
- milo/output.py +106 -0
- milo/pipeline.py +276 -0
- milo/plugins.py +168 -0
- milo/py.typed +0 -0
- milo/registry.py +213 -0
- milo/schema.py +214 -0
- milo/state.py +229 -0
- milo/streaming.py +41 -0
- milo/templates/__init__.py +38 -0
- milo/templates/error.kida +5 -0
- milo/templates/field_confirm.kida +1 -0
- milo/templates/field_select.kida +3 -0
- milo/templates/field_text.kida +1 -0
- milo/templates/form.kida +8 -0
- milo/templates/help.kida +7 -0
- milo/templates/progress.kida +1 -0
- milo/testing/__init__.py +27 -0
- milo/testing/_mcp.py +87 -0
- milo/testing/_record.py +125 -0
- milo/testing/_replay.py +68 -0
- milo/testing/_snapshot.py +96 -0
- milo_cli-0.1.0.dist-info/METADATA +441 -0
- milo_cli-0.1.0.dist-info/RECORD +50 -0
- milo_cli-0.1.0.dist-info/WHEEL +5 -0
- milo_cli-0.1.0.dist-info/entry_points.txt +2 -0
- milo_cli-0.1.0.dist-info/top_level.txt +1 -0
milo/cli.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""CLI entry point — milo dev, milo replay."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _load_app(app_path: str):
|
|
12
|
+
"""Load an App instance from a module:attribute path like 'myapp:app'."""
|
|
13
|
+
if ":" not in app_path:
|
|
14
|
+
print(f"Error: expected format 'module:attribute', got '{app_path}'", file=sys.stderr)
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
module_path, attr_name = app_path.rsplit(":", 1)
|
|
18
|
+
|
|
19
|
+
# Add cwd to sys.path so local modules can be found
|
|
20
|
+
cwd = str(Path.cwd())
|
|
21
|
+
if cwd not in sys.path:
|
|
22
|
+
sys.path.insert(0, cwd)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
module = importlib.import_module(module_path)
|
|
26
|
+
except ModuleNotFoundError as e:
|
|
27
|
+
print(f"Error: could not import '{module_path}': {e}", file=sys.stderr)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
app = getattr(module, attr_name)
|
|
32
|
+
except AttributeError:
|
|
33
|
+
print(f"Error: '{module_path}' has no attribute '{attr_name}'", file=sys.stderr)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
return app
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _cmd_dev(args: argparse.Namespace) -> None:
|
|
40
|
+
"""Run the dev server with hot-reload."""
|
|
41
|
+
from milo.dev import DevServer
|
|
42
|
+
|
|
43
|
+
app = _load_app(args.app)
|
|
44
|
+
watch_dirs = tuple(args.watch) if args.watch else ()
|
|
45
|
+
server = DevServer(app, watch_dirs=watch_dirs, poll_interval=args.poll)
|
|
46
|
+
server.run()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cmd_replay(args: argparse.Namespace) -> None:
|
|
50
|
+
"""Replay a recorded session."""
|
|
51
|
+
from milo.testing._record import load_recording
|
|
52
|
+
from milo.testing._replay import replay
|
|
53
|
+
|
|
54
|
+
recording = load_recording(args.session)
|
|
55
|
+
|
|
56
|
+
def default_reducer(state, action):
|
|
57
|
+
return state
|
|
58
|
+
|
|
59
|
+
reducer = default_reducer
|
|
60
|
+
|
|
61
|
+
# Try to load a reducer if specified
|
|
62
|
+
if args.reducer:
|
|
63
|
+
reducer = _load_app(args.reducer)
|
|
64
|
+
|
|
65
|
+
def on_state(state, action):
|
|
66
|
+
if args.diff:
|
|
67
|
+
print(f"[{action.type}] -> {state!r}")
|
|
68
|
+
|
|
69
|
+
final = replay(
|
|
70
|
+
recording,
|
|
71
|
+
reducer,
|
|
72
|
+
speed=args.speed,
|
|
73
|
+
step=args.step,
|
|
74
|
+
on_state=on_state if args.diff else None,
|
|
75
|
+
assert_hashes=args.assert_hashes,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if args.assert_hashes:
|
|
79
|
+
print("All state hashes match.", file=sys.stderr)
|
|
80
|
+
else:
|
|
81
|
+
print(f"Replay complete. Final state: {final!r}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main(argv: list[str] | None = None) -> None:
|
|
85
|
+
"""Main CLI entry point."""
|
|
86
|
+
parser = argparse.ArgumentParser(
|
|
87
|
+
prog="milo",
|
|
88
|
+
description="Template-driven CLI applications for free-threaded Python",
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")
|
|
91
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
92
|
+
|
|
93
|
+
# milo dev
|
|
94
|
+
dev_parser = subparsers.add_parser("dev", help="Run app with hot-reload")
|
|
95
|
+
dev_parser.add_argument("app", help="App path as 'module:attribute'")
|
|
96
|
+
dev_parser.add_argument(
|
|
97
|
+
"--watch", "-w", action="append", default=[], help="Directories to watch for changes"
|
|
98
|
+
)
|
|
99
|
+
dev_parser.add_argument(
|
|
100
|
+
"--poll", type=float, default=0.5, help="Poll interval in seconds (default: 0.5)"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# milo replay
|
|
104
|
+
replay_parser = subparsers.add_parser("replay", help="Replay a recorded session")
|
|
105
|
+
replay_parser.add_argument("session", help="Path to session JSONL file")
|
|
106
|
+
replay_parser.add_argument("--reducer", "-r", help="Reducer path as 'module:attribute'")
|
|
107
|
+
replay_parser.add_argument(
|
|
108
|
+
"--speed", "-s", type=float, default=1.0, help="Playback speed multiplier (default: 1.0)"
|
|
109
|
+
)
|
|
110
|
+
replay_parser.add_argument(
|
|
111
|
+
"--step", action="store_true", help="Step-by-step mode (press Enter to advance)"
|
|
112
|
+
)
|
|
113
|
+
replay_parser.add_argument(
|
|
114
|
+
"--diff", "-d", action="store_true", help="Show state diff between transitions"
|
|
115
|
+
)
|
|
116
|
+
replay_parser.add_argument(
|
|
117
|
+
"--assert",
|
|
118
|
+
dest="assert_hashes",
|
|
119
|
+
action="store_true",
|
|
120
|
+
help="Exit non-zero if state hashes don't match (CI use)",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
args = parser.parse_args(argv)
|
|
124
|
+
|
|
125
|
+
if args.command == "dev":
|
|
126
|
+
_cmd_dev(args)
|
|
127
|
+
elif args.command == "replay":
|
|
128
|
+
_cmd_replay(args)
|
|
129
|
+
else:
|
|
130
|
+
parser.print_help()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
main()
|