milo-cli 0.2.0__tar.gz → 0.2.2__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 (119) hide show
  1. {milo_cli-0.2.0 → milo_cli-0.2.2}/PKG-INFO +5 -4
  2. {milo_cli-0.2.0 → milo_cli-0.2.2}/README.md +3 -2
  3. {milo_cli-0.2.0 → milo_cli-0.2.2}/pyproject.toml +7 -4
  4. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/__init__.py +13 -1
  5. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/_command_defs.py +37 -6
  6. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/_jsonrpc.py +7 -2
  7. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/_types.py +10 -0
  8. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/app.py +16 -2
  9. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/commands.py +445 -201
  10. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/config.py +29 -4
  11. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/context.py +37 -3
  12. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/form.py +74 -22
  13. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/gateway.py +56 -4
  14. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/groups.py +48 -0
  15. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/help.py +20 -2
  16. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/mcp.py +85 -4
  17. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/output.py +11 -1
  18. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/pipeline.py +458 -11
  19. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/plugins.py +22 -5
  20. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/registry.py +15 -3
  21. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/schema.py +59 -12
  22. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/state.py +73 -15
  23. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/__init__.py +3 -2
  24. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/components/_defs.kida +85 -0
  25. milo_cli-0.2.2/src/milo/templates/help.kida +30 -0
  26. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/testing/_record.py +6 -1
  27. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/version_check.py +20 -13
  28. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo_cli.egg-info/PKG-INFO +5 -4
  29. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo_cli.egg-info/SOURCES.txt +4 -0
  30. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo_cli.egg-info/requires.txt +1 -1
  31. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_ai_native.py +68 -3
  32. milo_cli-0.2.2/tests/test_command_defs.py +294 -0
  33. milo_cli-0.2.2/tests/test_commands_core.py +457 -0
  34. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_components.py +139 -0
  35. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_config.py +82 -0
  36. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_context.py +32 -0
  37. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_effects.py +13 -12
  38. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_effects_stress.py +1 -1
  39. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_form.py +58 -0
  40. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_gateway.py +47 -0
  41. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_groups.py +139 -0
  42. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_help.py +17 -1
  43. milo_cli-0.2.2/tests/test_lazy.py +678 -0
  44. milo_cli-0.2.2/tests/test_mcp_handler.py +400 -0
  45. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_mcp_resources.py +62 -2
  46. milo_cli-0.2.2/tests/test_pipeline.py +1318 -0
  47. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_plugins.py +47 -0
  48. milo_cli-0.2.2/tests/test_registry_crud.py +122 -0
  49. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_schema_v2.py +55 -0
  50. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_state.py +52 -0
  51. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_testing.py +14 -0
  52. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_types.py +2 -1
  53. milo_cli-0.2.0/src/milo/templates/help.kida +0 -13
  54. milo_cli-0.2.0/tests/test_lazy.py +0 -332
  55. milo_cli-0.2.0/tests/test_pipeline.py +0 -614
  56. {milo_cli-0.2.0 → milo_cli-0.2.2}/setup.cfg +0 -0
  57. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/_child.py +0 -0
  58. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/_cli_help.py +0 -0
  59. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/_compat.py +0 -0
  60. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/_errors.py +0 -0
  61. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/_mcp_router.py +0 -0
  62. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/_protocols.py +0 -0
  63. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/cli.py +0 -0
  64. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/completions.py +0 -0
  65. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/dev.py +0 -0
  66. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/doctor.py +0 -0
  67. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/flow.py +0 -0
  68. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/input/__init__.py +0 -0
  69. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/input/_platform.py +0 -0
  70. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/input/_reader.py +0 -0
  71. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/input/_sequences.py +0 -0
  72. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/llms.py +0 -0
  73. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/middleware.py +0 -0
  74. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/observability.py +0 -0
  75. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/py.typed +0 -0
  76. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/reducers.py +0 -0
  77. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/streaming.py +0 -0
  78. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/components/command_list.kida +0 -0
  79. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/components/help_page.kida +0 -0
  80. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/error.kida +0 -0
  81. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/field_confirm.kida +0 -0
  82. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/field_select.kida +0 -0
  83. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/field_text.kida +0 -0
  84. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/form.kida +0 -0
  85. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/templates/progress.kida +0 -0
  86. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/testing/__init__.py +0 -0
  87. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/testing/_mcp.py +0 -0
  88. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/testing/_replay.py +0 -0
  89. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/testing/_snapshot.py +0 -0
  90. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo/theme.py +0 -0
  91. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo_cli.egg-info/dependency_links.txt +0 -0
  92. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo_cli.egg-info/entry_points.txt +0 -0
  93. {milo_cli-0.2.0 → milo_cli-0.2.2}/src/milo_cli.egg-info/top_level.txt +0 -0
  94. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_app.py +0 -0
  95. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_bubbletea_patterns.py +0 -0
  96. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_child.py +0 -0
  97. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_cli.py +0 -0
  98. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_commands_middleware.py +0 -0
  99. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_compat.py +0 -0
  100. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_completions.py +0 -0
  101. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_dev.py +0 -0
  102. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_doctor.py +0 -0
  103. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_errors.py +0 -0
  104. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_flow.py +0 -0
  105. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_input.py +0 -0
  106. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_mcp_prompts.py +0 -0
  107. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_mcp_router.py +0 -0
  108. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_middleware.py +0 -0
  109. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_milo_init.py +0 -0
  110. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_mount.py +0 -0
  111. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_observability.py +0 -0
  112. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_protocols.py +0 -0
  113. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_reducers.py +0 -0
  114. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_registry_v2.py +0 -0
  115. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_streaming.py +0 -0
  116. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_templates.py +0 -0
  117. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_testing_mcp.py +0 -0
  118. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_theme.py +0 -0
  119. {milo_cli-0.2.0 → milo_cli-0.2.2}/tests/test_version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: milo-cli
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Template-driven CLI applications for free-threaded Python
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://lbliii.github.io/milo-cli/
@@ -18,7 +18,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.14
20
20
  Description-Content-Type: text/markdown
21
- Requires-Dist: kida-templates>=0.4.0
21
+ Requires-Dist: kida-templates>=0.6.0
22
22
  Provides-Extra: docs
23
23
  Requires-Dist: bengal>=0.2.6; extra == "docs"
24
24
  Provides-Extra: yaml
@@ -150,7 +150,7 @@ Requires Python 3.14+
150
150
  | **Observability** | Built-in request logging with latency stats (`milo://stats` resource) | [MCP →](https://lbliii.github.io/milo-cli/docs/usage/mcp/) |
151
151
  | **State Management** | Redux-style `Store` with dispatch, listeners, middleware, and saga scheduling | [State →](https://lbliii.github.io/milo-cli/docs/usage/state/) |
152
152
  | **Commands** | Lightweight `Cmd` thunks, `Batch`, `Sequence`, `TickCmd` for one-shot effects | [Commands →](https://lbliii.github.io/milo-cli/docs/usage/commands-effects/) |
153
- | **Sagas** | Generator-based side effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry` | [Sagas →](https://lbliii.github.io/milo-cli/docs/usage/sagas/) |
153
+ | **Sagas** | Generator-based side effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry`, `Race`, `All`, `Take`, and more | [Sagas →](https://lbliii.github.io/milo-cli/docs/usage/sagas/) |
154
154
  | **ViewState** | Declarative terminal state (`cursor_visible`, `alt_screen`, `window_title`, `mouse_mode`) | [Commands →](https://lbliii.github.io/milo-cli/docs/usage/commands-effects/) |
155
155
  | **Flows** | Multi-screen state machines with `>>` operator and custom transitions | [Flows →](https://lbliii.github.io/milo-cli/docs/usage/flows/) |
156
156
  | **Forms** | Text, select, confirm, password fields with validation and TTY fallback | [Forms →](https://lbliii.github.io/milo-cli/docs/usage/forms/) |
@@ -159,6 +159,7 @@ Requires Python 3.14+
159
159
  | **Dev Server** | `milo dev` with filesystem polling and `@@HOT_RELOAD` dispatch | [Dev →](https://lbliii.github.io/milo-cli/docs/usage/dev/) |
160
160
  | **Session Recording** | JSONL action log with state hashes for debugging and regression testing | [Testing →](https://lbliii.github.io/milo-cli/docs/usage/testing/) |
161
161
  | **Snapshot Testing** | `assert_renders`, `assert_state`, `assert_saga` for deterministic test coverage | [Testing →](https://lbliii.github.io/milo-cli/docs/usage/testing/) |
162
+ | **Pipeline** | Declarative multi-phase workflows with dependency graphs, retry policies, and output capture | [Pipeline →](https://lbliii.github.io/milo-cli/docs/usage/pipeline/) |
162
163
  | **Help Rendering** | `HelpRenderer` — drop-in `argparse.HelpFormatter` using Kida templates | [Help →](https://lbliii.github.io/milo-cli/docs/usage/help/) |
163
164
  | **Context** | Execution context with verbosity, output format, global options, and `run_app()` bridge | [Context →](https://lbliii.github.io/milo-cli/docs/usage/context/) |
164
165
  | **Configuration** | `Config` with validation, init scaffolding, and profile support | [Config →](https://lbliii.github.io/milo-cli/docs/usage/config/) |
@@ -343,7 +344,7 @@ def reducer(state, action):
343
344
  return state
344
345
  ```
345
346
 
346
- Saga effects: `Call(fn, args)`, `Put(action)`, `Select(selector)`, `Fork(saga)`, `Delay(seconds)`, `Retry(fn, ...)`.
347
+ Saga effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry`, `Timeout`, `TryCall`, `Race`, `All`, `Take`, `Debounce`, `TakeEvery`, `TakeLatest`.
347
348
 
348
349
  For one-shot effects, use `Cmd` instead — no generator needed:
349
350
 
@@ -122,7 +122,7 @@ Requires Python 3.14+
122
122
  | **Observability** | Built-in request logging with latency stats (`milo://stats` resource) | [MCP →](https://lbliii.github.io/milo-cli/docs/usage/mcp/) |
123
123
  | **State Management** | Redux-style `Store` with dispatch, listeners, middleware, and saga scheduling | [State →](https://lbliii.github.io/milo-cli/docs/usage/state/) |
124
124
  | **Commands** | Lightweight `Cmd` thunks, `Batch`, `Sequence`, `TickCmd` for one-shot effects | [Commands →](https://lbliii.github.io/milo-cli/docs/usage/commands-effects/) |
125
- | **Sagas** | Generator-based side effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry` | [Sagas →](https://lbliii.github.io/milo-cli/docs/usage/sagas/) |
125
+ | **Sagas** | Generator-based side effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry`, `Race`, `All`, `Take`, and more | [Sagas →](https://lbliii.github.io/milo-cli/docs/usage/sagas/) |
126
126
  | **ViewState** | Declarative terminal state (`cursor_visible`, `alt_screen`, `window_title`, `mouse_mode`) | [Commands →](https://lbliii.github.io/milo-cli/docs/usage/commands-effects/) |
127
127
  | **Flows** | Multi-screen state machines with `>>` operator and custom transitions | [Flows →](https://lbliii.github.io/milo-cli/docs/usage/flows/) |
128
128
  | **Forms** | Text, select, confirm, password fields with validation and TTY fallback | [Forms →](https://lbliii.github.io/milo-cli/docs/usage/forms/) |
@@ -131,6 +131,7 @@ Requires Python 3.14+
131
131
  | **Dev Server** | `milo dev` with filesystem polling and `@@HOT_RELOAD` dispatch | [Dev →](https://lbliii.github.io/milo-cli/docs/usage/dev/) |
132
132
  | **Session Recording** | JSONL action log with state hashes for debugging and regression testing | [Testing →](https://lbliii.github.io/milo-cli/docs/usage/testing/) |
133
133
  | **Snapshot Testing** | `assert_renders`, `assert_state`, `assert_saga` for deterministic test coverage | [Testing →](https://lbliii.github.io/milo-cli/docs/usage/testing/) |
134
+ | **Pipeline** | Declarative multi-phase workflows with dependency graphs, retry policies, and output capture | [Pipeline →](https://lbliii.github.io/milo-cli/docs/usage/pipeline/) |
134
135
  | **Help Rendering** | `HelpRenderer` — drop-in `argparse.HelpFormatter` using Kida templates | [Help →](https://lbliii.github.io/milo-cli/docs/usage/help/) |
135
136
  | **Context** | Execution context with verbosity, output format, global options, and `run_app()` bridge | [Context →](https://lbliii.github.io/milo-cli/docs/usage/context/) |
136
137
  | **Configuration** | `Config` with validation, init scaffolding, and profile support | [Config →](https://lbliii.github.io/milo-cli/docs/usage/config/) |
@@ -315,7 +316,7 @@ def reducer(state, action):
315
316
  return state
316
317
  ```
317
318
 
318
- Saga effects: `Call(fn, args)`, `Put(action)`, `Select(selector)`, `Fork(saga)`, `Delay(seconds)`, `Retry(fn, ...)`.
319
+ Saga effects: `Call`, `Put`, `Select`, `Fork`, `Delay`, `Retry`, `Timeout`, `TryCall`, `Race`, `All`, `Take`, `Debounce`, `TakeEvery`, `TakeLatest`.
319
320
 
320
321
  For one-shot effects, use `Cmd` instead — no generator needed:
321
322
 
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "milo-cli"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Template-driven CLI applications for free-threaded Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
11
11
  license = "MIT"
12
- dependencies = ["kida-templates>=0.4.0"]
12
+ dependencies = ["kida-templates>=0.6.0"]
13
13
  keywords = ["cli", "terminal", "forms", "free-threading", "template", "elm"]
14
14
  classifiers = [
15
15
  "Development Status :: 3 - Alpha",
@@ -123,7 +123,7 @@ ignore = [
123
123
 
124
124
  [tool.ruff.lint.per-file-ignores]
125
125
  "__init__.py" = ["F401"]
126
- "tests/**/*.py" = ["S101", "S108", "S110", "S604", "SIM117", "N806", "ARG"]
126
+ "tests/**/*.py" = ["S101", "S108", "S110", "S604", "SIM117", "N806", "ARG", "T201"]
127
127
  "src/milo/_child.py" = ["S101"] # asserts for type narrowing on subprocess pipes
128
128
  "src/milo/testing/*.py" = ["S101", "ARG"] # test helpers use assert and may have unused args
129
129
  "src/milo/version_check.py" = ["S110", "S310"] # best-effort cache reads + known PyPI URL
@@ -136,7 +136,7 @@ directory = "changelog.d"
136
136
  filename = "CHANGELOG.md"
137
137
  package = "milo"
138
138
  package_dir = "src"
139
- title_format = "## [{version}] - {project_date}"
139
+ title_format = "## {version} {project_date}"
140
140
  issue_format = "[#{issue}](https://github.com/lbliii/milo-cli/issues/{issue})"
141
141
  underlines = ["", "", ""]
142
142
 
@@ -187,3 +187,6 @@ dev = [
187
187
  # Changelog
188
188
  "towncrier>=24.0",
189
189
  ]
190
+ docs = [
191
+ "bengal>=0.2.6",
192
+ ]
@@ -93,8 +93,14 @@ def __getattr__(name: str):
93
93
  "Pipeline": "pipeline",
94
94
  "Phase": "pipeline",
95
95
  "PhasePolicy": "pipeline",
96
+ "PhaseLog": "pipeline",
96
97
  "PipelineState": "pipeline",
97
98
  "PhaseStatus": "pipeline",
99
+ "PipelineViewState": "pipeline",
100
+ "make_detail_reducer": "pipeline",
101
+ "pipeline_to_timeline": "pipeline",
102
+ "set_active_pipeline": "pipeline",
103
+ "get_active_pipeline": "pipeline",
98
104
  "CycleError": "pipeline",
99
105
  # Plugins
100
106
  "HookRegistry": "plugins",
@@ -154,7 +160,7 @@ def _Py_mod_gil() -> int: # noqa: N802
154
160
  return 0
155
161
 
156
162
 
157
- __version__ = "0.2.0"
163
+ __version__ = "0.2.2"
158
164
  __all__ = [
159
165
  "BUILTIN_ACTIONS",
160
166
  "CLI",
@@ -211,11 +217,13 @@ __all__ = [
211
217
  "MinLen",
212
218
  "Pattern",
213
219
  "Phase",
220
+ "PhaseLog",
214
221
  "PhasePolicy",
215
222
  "PhaseStatus",
216
223
  "Pipeline",
217
224
  "PipelineError",
218
225
  "PipelineState",
226
+ "PipelineViewState",
219
227
  "PluginError",
220
228
  "Progress",
221
229
  "PromptDef",
@@ -254,14 +262,18 @@ __all__ = [
254
262
  "format_render_error",
255
263
  "function_to_schema",
256
264
  "generate_llms_txt",
265
+ "get_active_pipeline",
257
266
  "get_context",
258
267
  "install_completions",
268
+ "make_detail_reducer",
259
269
  "make_form_reducer",
260
270
  "make_style_filter",
271
+ "pipeline_to_timeline",
261
272
  "quit_on",
262
273
  "render_html",
263
274
  "run",
264
275
  "run_doctor",
276
+ "set_active_pipeline",
265
277
  "with_confirm",
266
278
  "with_cursor",
267
279
  "write_output",
@@ -59,6 +59,8 @@ class CommandDef:
59
59
  """If non-empty, prompt for confirmation before running."""
60
60
  annotations: dict[str, Any] = field(default_factory=dict)
61
61
  """MCP tool annotations (readOnlyHint, destructiveHint, etc.)."""
62
+ display_result: bool = True
63
+ """If False, suppress plain-format output (return value still available for --format json)."""
62
64
 
63
65
 
64
66
  class LazyCommandDef:
@@ -80,6 +82,7 @@ class LazyCommandDef:
80
82
  "annotations",
81
83
  "confirm",
82
84
  "description",
85
+ "display_result",
83
86
  "examples",
84
87
  "hidden",
85
88
  "import_path",
@@ -100,6 +103,7 @@ class LazyCommandDef:
100
103
  examples: tuple[dict[str, Any], ...] | list[dict[str, Any]] = (),
101
104
  confirm: str = "",
102
105
  annotations: dict[str, Any] | None = None,
106
+ display_result: bool = True,
103
107
  ) -> None:
104
108
  self.name = name
105
109
  self.description = description
@@ -110,6 +114,7 @@ class LazyCommandDef:
110
114
  self.examples = tuple(examples)
111
115
  self.confirm = confirm
112
116
  self.annotations = annotations or {}
117
+ self.display_result = display_result
113
118
  self._schema = schema
114
119
  self._resolved: CommandDef | None = None
115
120
  self._lock = threading.Lock()
@@ -127,7 +132,11 @@ class LazyCommandDef:
127
132
  return self.resolve().handler
128
133
 
129
134
  def resolve(self) -> CommandDef:
130
- """Import the handler and cache as a full CommandDef. Thread-safe."""
135
+ """Import the handler and cache as a full CommandDef. Thread-safe.
136
+
137
+ Raises :class:`LazyImportError` if the module or attribute cannot
138
+ be imported, wrapping the original exception with a clear message.
139
+ """
131
140
  if self._resolved is not None:
132
141
  return self._resolved
133
142
 
@@ -139,10 +148,13 @@ class LazyCommandDef:
139
148
  module_path, _, attr_name = self.import_path.rpartition(":")
140
149
  if not module_path or not attr_name:
141
150
  msg = f"Invalid import_path {self.import_path!r}: expected 'module.path:attribute'"
142
- raise ValueError(msg)
151
+ raise LazyImportError(self.name, self.import_path, ValueError(msg))
143
152
 
144
- module = importlib.import_module(module_path)
145
- handler = getattr(module, attr_name)
153
+ try:
154
+ module = importlib.import_module(module_path)
155
+ handler = getattr(module, attr_name)
156
+ except Exception as exc:
157
+ raise LazyImportError(self.name, self.import_path, exc) from exc
146
158
 
147
159
  from milo.schema import function_to_schema
148
160
 
@@ -159,6 +171,7 @@ class LazyCommandDef:
159
171
  examples=self.examples,
160
172
  confirm=self.confirm,
161
173
  annotations=self.annotations,
174
+ display_result=self.display_result,
162
175
  )
163
176
  return self._resolved
164
177
 
@@ -174,6 +187,20 @@ class InvokeResult:
174
187
  stderr: str = ""
175
188
 
176
189
 
190
+ class LazyImportError(Exception):
191
+ """Raised when a lazy command's import fails.
192
+
193
+ Wraps the original exception with the command name and import path
194
+ so callers can provide actionable error messages.
195
+ """
196
+
197
+ def __init__(self, command_name: str, import_path: str, cause: Exception) -> None:
198
+ self.command_name = command_name
199
+ self.import_path = import_path
200
+ self.cause = cause
201
+ super().__init__(f"Command {command_name!r} failed to import from {import_path!r}: {cause}")
202
+
203
+
177
204
  def _make_command_def(
178
205
  name: str,
179
206
  func: Callable,
@@ -185,6 +212,7 @@ def _make_command_def(
185
212
  examples: tuple[dict[str, Any], ...] = (),
186
213
  confirm: str = "",
187
214
  annotations: dict[str, Any] | None = None,
215
+ display_result: bool = True,
188
216
  ) -> CommandDef:
189
217
  """Build a CommandDef from a function and decorator kwargs."""
190
218
  from milo.schema import function_to_schema
@@ -204,17 +232,20 @@ def _make_command_def(
204
232
  examples=examples,
205
233
  confirm=confirm,
206
234
  annotations=annotations or {},
235
+ display_result=display_result,
207
236
  )
208
237
 
209
238
 
210
239
  def _is_context_param(param: inspect.Parameter) -> bool:
211
240
  """Check if a parameter is a Context injection point."""
241
+ from milo.context import Context as _MiloContext
242
+
212
243
  annotation = param.annotation
213
244
  if annotation is inspect.Parameter.empty:
214
245
  return False
215
- # Check for Context type or string annotation
246
+ # Check for exact type identity or subclass of milo.context.Context
216
247
  if isinstance(annotation, type):
217
- return annotation.__name__ == "Context"
248
+ return issubclass(annotation, _MiloContext)
218
249
  if isinstance(annotation, str):
219
250
  return annotation in ("Context", "milo.context.Context")
220
251
  return False
@@ -15,11 +15,16 @@ def _write_result(req_id: Any, result: dict[str, Any]) -> None:
15
15
  sys.stdout.flush()
16
16
 
17
17
 
18
- def _write_error(req_id: Any, code: int, message: str) -> None:
18
+ def _write_error(
19
+ req_id: Any, code: int, message: str, *, data: dict[str, Any] | None = None
20
+ ) -> None:
21
+ error_obj: dict[str, Any] = {"code": code, "message": message}
22
+ if data is not None:
23
+ error_obj["data"] = data
19
24
  response = {
20
25
  "jsonrpc": "2.0",
21
26
  "id": req_id,
22
- "error": {"code": code, "message": message},
27
+ "error": error_obj,
23
28
  }
24
29
  sys.stdout.write(json.dumps(response) + "\n")
25
30
  sys.stdout.flush()
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import warnings
5
6
  from collections.abc import Callable, Generator
6
7
  from dataclasses import dataclass, field
7
8
  from enum import Enum, auto
@@ -82,6 +83,7 @@ BUILTIN_ACTIONS: frozenset[str] = frozenset(
82
83
  "@@PHASE_FAILED",
83
84
  "@@PHASE_SKIPPED",
84
85
  "@@PHASE_RETRY",
86
+ "@@PHASE_LOG",
85
87
  "@@SAGA_ERROR",
86
88
  "@@SAGA_CANCELLED",
87
89
  "@@CMD_ERROR",
@@ -326,6 +328,14 @@ class Take:
326
328
  action_type: str
327
329
  timeout: float | None = None
328
330
 
331
+ def __post_init__(self) -> None:
332
+ if self.timeout is None:
333
+ warnings.warn(
334
+ f"Take({self.action_type!r}) has no timeout and may block forever. "
335
+ f"Consider setting timeout= to prevent silent hangs.",
336
+ stacklevel=2,
337
+ )
338
+
329
339
 
330
340
  @dataclass(frozen=True, slots=True)
331
341
  class Debounce:
@@ -241,7 +241,14 @@ class App:
241
241
  from milo.templates import get_env
242
242
 
243
243
  env = get_env()
244
- tmpl = env.get_template(template)
244
+ try:
245
+ tmpl = env.get_template(template)
246
+ except Exception as e:
247
+ raise AppError(
248
+ ErrorCode.APP_RENDER,
249
+ f"Template not found: {template!r}",
250
+ suggestion="Ensure the template file exists and the kida environment is configured correctly.",
251
+ ) from e
245
252
  return tmpl.render(state=state)
246
253
 
247
254
  def run(self) -> Any:
@@ -405,7 +412,14 @@ class App:
405
412
 
406
413
  def _render_exit(self, state: Any, env: Any) -> None:
407
414
  """Render the exit template once to stdout."""
408
- template = env.get_template(self._exit_template)
415
+ try:
416
+ template = env.get_template(self._exit_template)
417
+ except Exception as e:
418
+ raise AppError(
419
+ ErrorCode.APP_RENDER,
420
+ f"Exit template not found: {self._exit_template!r}",
421
+ suggestion="Ensure the exit template file exists and the kida environment is configured correctly.",
422
+ ) from e
409
423
  render_state = state
410
424
  if isinstance(state, FlowState):
411
425
  # For flows, pass all screen states so exit template can reference any data