milo-cli 0.2.1__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 (117) hide show
  1. {milo_cli-0.2.1 → milo_cli-0.2.2}/PKG-INFO +5 -4
  2. {milo_cli-0.2.1 → milo_cli-0.2.2}/README.md +3 -2
  3. {milo_cli-0.2.1 → milo_cli-0.2.2}/pyproject.toml +6 -3
  4. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/__init__.py +13 -1
  5. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/_command_defs.py +29 -6
  6. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/_jsonrpc.py +7 -2
  7. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/_types.py +10 -0
  8. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/app.py +16 -2
  9. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/commands.py +312 -176
  10. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/config.py +29 -4
  11. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/context.py +37 -3
  12. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/form.py +74 -22
  13. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/gateway.py +56 -4
  14. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/groups.py +8 -2
  15. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/help.py +18 -2
  16. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/mcp.py +85 -4
  17. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/output.py +11 -1
  18. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/pipeline.py +458 -11
  19. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/plugins.py +22 -5
  20. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/registry.py +15 -3
  21. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/schema.py +49 -12
  22. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/state.py +73 -15
  23. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/__init__.py +3 -2
  24. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/components/_defs.kida +85 -0
  25. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/testing/_record.py +6 -1
  26. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/version_check.py +20 -13
  27. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo_cli.egg-info/PKG-INFO +5 -4
  28. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo_cli.egg-info/SOURCES.txt +4 -0
  29. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo_cli.egg-info/requires.txt +1 -1
  30. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_ai_native.py +68 -3
  31. milo_cli-0.2.2/tests/test_command_defs.py +294 -0
  32. milo_cli-0.2.2/tests/test_commands_core.py +457 -0
  33. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_components.py +139 -0
  34. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_config.py +82 -0
  35. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_context.py +32 -0
  36. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_effects.py +4 -3
  37. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_effects_stress.py +1 -1
  38. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_form.py +58 -0
  39. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_gateway.py +47 -0
  40. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_help.py +17 -1
  41. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_lazy.py +73 -7
  42. milo_cli-0.2.2/tests/test_mcp_handler.py +400 -0
  43. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_mcp_resources.py +62 -2
  44. milo_cli-0.2.2/tests/test_pipeline.py +1318 -0
  45. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_plugins.py +47 -0
  46. milo_cli-0.2.2/tests/test_registry_crud.py +122 -0
  47. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_schema_v2.py +55 -0
  48. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_state.py +52 -0
  49. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_testing.py +14 -0
  50. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_types.py +2 -1
  51. milo_cli-0.2.1/tests/test_pipeline.py +0 -614
  52. {milo_cli-0.2.1 → milo_cli-0.2.2}/setup.cfg +0 -0
  53. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/_child.py +0 -0
  54. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/_cli_help.py +0 -0
  55. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/_compat.py +0 -0
  56. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/_errors.py +0 -0
  57. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/_mcp_router.py +0 -0
  58. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/_protocols.py +0 -0
  59. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/cli.py +0 -0
  60. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/completions.py +0 -0
  61. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/dev.py +0 -0
  62. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/doctor.py +0 -0
  63. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/flow.py +0 -0
  64. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/input/__init__.py +0 -0
  65. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/input/_platform.py +0 -0
  66. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/input/_reader.py +0 -0
  67. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/input/_sequences.py +0 -0
  68. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/llms.py +0 -0
  69. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/middleware.py +0 -0
  70. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/observability.py +0 -0
  71. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/py.typed +0 -0
  72. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/reducers.py +0 -0
  73. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/streaming.py +0 -0
  74. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/components/command_list.kida +0 -0
  75. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/components/help_page.kida +0 -0
  76. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/error.kida +0 -0
  77. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/field_confirm.kida +0 -0
  78. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/field_select.kida +0 -0
  79. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/field_text.kida +0 -0
  80. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/form.kida +0 -0
  81. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/help.kida +0 -0
  82. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/templates/progress.kida +0 -0
  83. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/testing/__init__.py +0 -0
  84. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/testing/_mcp.py +0 -0
  85. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/testing/_replay.py +0 -0
  86. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/testing/_snapshot.py +0 -0
  87. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo/theme.py +0 -0
  88. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo_cli.egg-info/dependency_links.txt +0 -0
  89. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo_cli.egg-info/entry_points.txt +0 -0
  90. {milo_cli-0.2.1 → milo_cli-0.2.2}/src/milo_cli.egg-info/top_level.txt +0 -0
  91. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_app.py +0 -0
  92. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_bubbletea_patterns.py +0 -0
  93. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_child.py +0 -0
  94. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_cli.py +0 -0
  95. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_commands_middleware.py +0 -0
  96. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_compat.py +0 -0
  97. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_completions.py +0 -0
  98. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_dev.py +0 -0
  99. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_doctor.py +0 -0
  100. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_errors.py +0 -0
  101. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_flow.py +0 -0
  102. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_groups.py +0 -0
  103. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_input.py +0 -0
  104. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_mcp_prompts.py +0 -0
  105. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_mcp_router.py +0 -0
  106. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_middleware.py +0 -0
  107. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_milo_init.py +0 -0
  108. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_mount.py +0 -0
  109. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_observability.py +0 -0
  110. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_protocols.py +0 -0
  111. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_reducers.py +0 -0
  112. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_registry_v2.py +0 -0
  113. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_streaming.py +0 -0
  114. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_templates.py +0 -0
  115. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_testing_mcp.py +0 -0
  116. {milo_cli-0.2.1 → milo_cli-0.2.2}/tests/test_theme.py +0 -0
  117. {milo_cli-0.2.1 → 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.1
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.1"
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
@@ -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.1"
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",
@@ -132,7 +132,11 @@ class LazyCommandDef:
132
132
  return self.resolve().handler
133
133
 
134
134
  def resolve(self) -> CommandDef:
135
- """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
+ """
136
140
  if self._resolved is not None:
137
141
  return self._resolved
138
142
 
@@ -144,10 +148,13 @@ class LazyCommandDef:
144
148
  module_path, _, attr_name = self.import_path.rpartition(":")
145
149
  if not module_path or not attr_name:
146
150
  msg = f"Invalid import_path {self.import_path!r}: expected 'module.path:attribute'"
147
- raise ValueError(msg)
151
+ raise LazyImportError(self.name, self.import_path, ValueError(msg))
148
152
 
149
- module = importlib.import_module(module_path)
150
- 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
151
158
 
152
159
  from milo.schema import function_to_schema
153
160
 
@@ -180,6 +187,20 @@ class InvokeResult:
180
187
  stderr: str = ""
181
188
 
182
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
+
183
204
  def _make_command_def(
184
205
  name: str,
185
206
  func: Callable,
@@ -217,12 +238,14 @@ def _make_command_def(
217
238
 
218
239
  def _is_context_param(param: inspect.Parameter) -> bool:
219
240
  """Check if a parameter is a Context injection point."""
241
+ from milo.context import Context as _MiloContext
242
+
220
243
  annotation = param.annotation
221
244
  if annotation is inspect.Parameter.empty:
222
245
  return False
223
- # Check for Context type or string annotation
246
+ # Check for exact type identity or subclass of milo.context.Context
224
247
  if isinstance(annotation, type):
225
- return annotation.__name__ == "Context"
248
+ return issubclass(annotation, _MiloContext)
226
249
  if isinstance(annotation, str):
227
250
  return annotation in ("Context", "milo.context.Context")
228
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