soothe-cli 0.4.1__tar.gz → 0.4.3__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 (135) hide show
  1. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/.gitignore +1 -0
  2. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/PKG-INFO +10 -12
  3. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/README.md +8 -10
  4. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/pyproject.toml +2 -2
  5. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -4
  6. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/commands/run_cmd.py +12 -8
  7. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/commands/thread_cmd.py +10 -0
  8. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/execution/daemon.py +21 -56
  9. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/execution/headless.py +6 -11
  10. soothe_cli-0.4.3/src/soothe_cli/cli/execution/headless_renderer.py +107 -0
  11. soothe_cli-0.4.3/src/soothe_cli/cli/main.py +151 -0
  12. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/stream/__init__.py +1 -2
  13. soothe_cli-0.4.3/src/soothe_cli/cli/stream/context.py +65 -0
  14. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/stream/display_line.py +1 -9
  15. soothe_cli-0.4.3/src/soothe_cli/cli/stream/formatter.py +290 -0
  16. soothe_cli-0.4.3/src/soothe_cli/cli/stream/pipeline.py +500 -0
  17. soothe_cli-0.4.3/src/soothe_cli/cli/stream/task_scope.py +46 -0
  18. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/config/cli_config.py +25 -35
  19. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/shared/__init__.py +31 -21
  20. soothe_cli-0.4.3/src/soothe_cli/shared/commands/__init__.py +37 -0
  21. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/commands}/command_router.py +6 -6
  22. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/commands}/slash_commands.py +1 -1
  23. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/shared/config_loader.py +1 -1
  24. soothe_cli-0.4.3/src/soothe_cli/shared/core/__init__.py +13 -0
  25. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/core}/event_processor.py +413 -109
  26. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/core}/presentation_engine.py +42 -8
  27. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/core}/processor_state.py +28 -4
  28. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/core}/renderer_protocol.py +36 -1
  29. soothe_cli-0.4.3/src/soothe_cli/shared/events/__init__.py +15 -0
  30. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/events}/display_policy.py +12 -145
  31. soothe_cli-0.4.3/src/soothe_cli/shared/events/explore_task_display.py +89 -0
  32. soothe_cli-0.4.3/src/soothe_cli/shared/events/stream_accumulator.py +144 -0
  33. soothe_cli-0.4.3/src/soothe_cli/shared/presentation_engine.py +5 -0
  34. soothe_cli-0.4.3/src/soothe_cli/shared/renderer_base.py +5 -0
  35. soothe_cli-0.4.3/src/soothe_cli/shared/rendering/__init__.py +9 -0
  36. soothe_cli-0.4.3/src/soothe_cli/shared/rendering/async_renderer_protocol.py +192 -0
  37. soothe_cli-0.4.3/src/soothe_cli/shared/rendering/renderer_base.py +72 -0
  38. soothe_cli-0.4.3/src/soothe_cli/shared/stream_accumulator.py +13 -0
  39. soothe_cli-0.4.1/src/soothe_cli/cli/commands/subagent_names.py → soothe_cli-0.4.3/src/soothe_cli/shared/subagent_routing.py +2 -4
  40. soothe_cli-0.4.3/src/soothe_cli/shared/tools/__init__.py +3 -0
  41. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/message_processing.py +1 -1
  42. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/rendering.py +1 -1
  43. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_call_resolution.py +8 -7
  44. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_card_payload.py +22 -5
  45. soothe_cli-0.4.3/src/soothe_cli/shared/tools/tool_card_visibility.py +116 -0
  46. soothe_cli-0.4.3/src/soothe_cli/shared/tools/tool_formatters/__init__.py +29 -0
  47. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/base.py +1 -1
  48. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/execution.py +18 -5
  49. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/fallback.py +3 -3
  50. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/file_ops.py +6 -3
  51. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/goal_formatter.py +2 -2
  52. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/media.py +2 -2
  53. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/structured.py +2 -2
  54. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/subagent.py +2 -2
  55. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_formatters/web.py +2 -2
  56. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_message_format.py +41 -0
  57. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/tools}/tool_output_formatter.py +2 -2
  58. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/app.py +20 -7
  59. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/config.py +26 -1
  60. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/daemon_session.py +29 -4
  61. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/preview_limits.py +4 -0
  62. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/textual_adapter.py +542 -166
  63. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/tool_display.py +27 -0
  64. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/message_store.py +0 -5
  65. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/messages.py +312 -136
  66. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/welcome.py +2 -2
  67. soothe_cli-0.4.1/src/soothe_cli/cli/commands/config_cmd.py +0 -282
  68. soothe_cli-0.4.1/src/soothe_cli/cli/commands/status_cmd.py +0 -121
  69. soothe_cli-0.4.1/src/soothe_cli/cli/main.py +0 -407
  70. soothe_cli-0.4.1/src/soothe_cli/cli/renderer.py +0 -462
  71. soothe_cli-0.4.1/src/soothe_cli/cli/stream/context.py +0 -142
  72. soothe_cli-0.4.1/src/soothe_cli/cli/stream/formatter.py +0 -499
  73. soothe_cli-0.4.1/src/soothe_cli/cli/stream/pipeline.py +0 -810
  74. soothe_cli-0.4.1/src/soothe_cli/cli/utils.py +0 -46
  75. soothe_cli-0.4.1/src/soothe_cli/shared/suppression_state.py +0 -189
  76. soothe_cli-0.4.1/src/soothe_cli/shared/tool_formatters/__init__.py +0 -29
  77. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/__init__.py +0 -0
  78. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/__init__.py +0 -0
  79. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/commands/__init__.py +0 -0
  80. /soothe_cli-0.4.1/src/soothe_cli/loop_commands.py → /soothe_cli-0.4.3/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  81. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/execution/__init__.py +0 -0
  82. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/cli/execution/launcher.py +0 -0
  83. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/config/__init__.py +0 -0
  84. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/plan/__init__.py +0 -0
  85. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/plan/rich_tree.py +0 -0
  86. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/commands}/subagent_routing.py +0 -0
  87. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/events}/essential_events.py +0 -0
  88. {soothe_cli-0.4.1/src/soothe_cli/shared → soothe_cli-0.4.3/src/soothe_cli/shared/events}/tui_trace_log.py +0 -0
  89. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/__init__.py +0 -0
  90. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  91. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_cli_context.py +0 -0
  92. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_env_vars.py +0 -0
  93. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_session_stats.py +0 -0
  94. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/_version.py +0 -0
  95. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/app.tcss +0 -0
  96. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/command_registry.py +0 -0
  97. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/file_ops.py +0 -0
  98. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/formatting.py +0 -0
  99. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/hooks.py +0 -0
  100. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/input.py +0 -0
  101. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/media_utils.py +0 -0
  102. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/message_display_filter.py +0 -0
  103. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/model_config.py +0 -0
  104. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/output.py +0 -0
  105. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/project_utils.py +0 -0
  106. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/sessions.py +0 -0
  107. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/skills/__init__.py +0 -0
  108. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/skills/invocation.py +0 -0
  109. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/skills/load.py +0 -0
  110. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/theme.py +0 -0
  111. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/unicode_security.py +0 -0
  112. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/update_check.py +0 -0
  113. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  114. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/_links.py +0 -0
  115. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/approval.py +0 -0
  116. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  117. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  118. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  119. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  120. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  121. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  122. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/diff.py +0 -0
  123. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/editor.py +0 -0
  124. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/history.py +0 -0
  125. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/loading.py +0 -0
  126. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  127. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  128. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  129. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  130. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/status.py +0 -0
  131. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  132. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
  133. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  134. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  135. {soothe_cli-0.4.1 → soothe_cli-0.4.3}/src/soothe_cli/tui/widgets/tools.py +0 -0
@@ -218,3 +218,4 @@ plot_*
218
218
  checkpoint.json
219
219
  manifest.json
220
220
  _bmad
221
+ __MACOSX
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/OpenSoothe/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Programming Language :: Python :: 3.14
18
18
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
- Requires-Python: <3.15,>=3.11
20
+ Requires-Python: <4.0,>=3.11
21
21
  Requires-Dist: python-dotenv<2.0.0,>=1.0.0
22
22
  Requires-Dist: pyyaml<7.0.0,>=6.0.0
23
23
  Requires-Dist: rich>=13.0.0
@@ -57,9 +57,6 @@ soothe -p "Research AI advances"
57
57
  # Loop management
58
58
  soothe loop list
59
59
  soothe loop continue loop_abc123
60
-
61
- # Configuration
62
- soothe config show
63
60
  ```
64
61
 
65
62
  ## Architecture
@@ -82,13 +79,14 @@ This package is the **client** component that communicates with the Soothe daemo
82
79
  CLI uses `cli_config.yml`:
83
80
 
84
81
  ```yaml
85
- websocket:
86
- host: "localhost"
87
- port: 8765
88
-
89
- ui:
90
- verbosity: "normal"
91
- format: "text"
82
+ daemon:
83
+ transports:
84
+ websocket:
85
+ host: "127.0.0.1"
86
+ port: 8765
87
+
88
+ # Optional: logging_level for ~/.soothe/logs/soothe-cli.log (DEBUG, INFO, …)
89
+ # logging_level: INFO
92
90
 
93
91
  tui:
94
92
  theme: "default"
@@ -22,9 +22,6 @@ soothe -p "Research AI advances"
22
22
  # Loop management
23
23
  soothe loop list
24
24
  soothe loop continue loop_abc123
25
-
26
- # Configuration
27
- soothe config show
28
25
  ```
29
26
 
30
27
  ## Architecture
@@ -47,13 +44,14 @@ This package is the **client** component that communicates with the Soothe daemo
47
44
  CLI uses `cli_config.yml`:
48
45
 
49
46
  ```yaml
50
- websocket:
51
- host: "localhost"
52
- port: 8765
53
-
54
- ui:
55
- verbosity: "normal"
56
- format: "text"
47
+ daemon:
48
+ transports:
49
+ websocket:
50
+ host: "127.0.0.1"
51
+ port: 8765
52
+
53
+ # Optional: logging_level for ~/.soothe/logs/soothe-cli.log (DEBUG, INFO, …)
54
+ # logging_level: INFO
57
55
 
58
56
  tui:
59
57
  theme: "default"
@@ -8,7 +8,7 @@ dynamic = ["version"]
8
8
  description = "Soothe CLI client - communicates with daemon via WebSocket"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
- requires-python = ">=3.11,<3.15"
11
+ requires-python = ">=3.11,<4.0"
12
12
  keywords = ["soothe", "cli", "client", "tui", "websocket"]
13
13
  classifiers = [
14
14
  "Development Status :: 3 - Alpha",
@@ -76,7 +76,7 @@ path = "../../VERSION"
76
76
  pattern = "^(?P<version>[0-9]+\\.[0-9]+\\.[0-9]+)$"
77
77
 
78
78
  [tool.mypy]
79
- python_version = "3.11"
79
+ python_version = "3.12"
80
80
  warn_return_any = true
81
81
  warn_unused_configs = true
82
82
  disallow_untyped_defs = true
@@ -21,9 +21,6 @@ def run(
21
21
  max_iterations: int | None = typer.Option(
22
22
  None, "--max-iterations", help="Maximum autonomous iterations."
23
23
  ),
24
- output_format: str = typer.Option(
25
- "text", "--format", "-f", help="Output format: text or jsonl."
26
- ),
27
24
  ) -> None:
28
25
  """Run autonomous agent loop for complex tasks.
29
26
 
@@ -39,7 +36,6 @@ def run(
39
36
  no_tui=True,
40
37
  autonomous=True,
41
38
  max_iterations=max_iterations,
42
- output_format=output_format,
43
39
  )
44
40
 
45
41
 
@@ -17,30 +17,29 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
  def run_impl(
19
19
  prompt: str | None,
20
- config: str | None,
21
20
  thread_id: str | None,
22
21
  no_tui: bool, # noqa: FBT001
23
22
  autonomous: bool, # noqa: FBT001
24
23
  max_iterations: int | None,
25
- output_format: str,
24
+ streaming_enabled: bool | None = None,
25
+ streaming_mode: str | None = None,
26
26
  ) -> None:
27
27
  """Core implementation for running Soothe agent.
28
28
 
29
29
  Args:
30
30
  prompt: Optional prompt for headless mode
31
- config: Deprecated; passed through for ``--config`` compatibility (ignored for
32
- client settings; see ``load_config``).
33
31
  thread_id: Thread ID to resume
34
32
  no_tui: Force headless mode
35
33
  autonomous: Enable autonomous iteration mode
36
34
  max_iterations: Max iterations for autonomous mode
37
- output_format: Output format (text or jsonl)
35
+ streaming_enabled: Override daemon streaming enabled setting (RFC-614)
36
+ streaming_mode: Override daemon streaming mode ('streaming' or 'batch')
38
37
  """
39
38
  startup_start = time.perf_counter()
40
39
 
41
40
  try:
42
- cfg = load_config(config)
43
- log_level = resolve_cli_log_level(cfg.verbosity, logging_level=cfg.logging_level)
41
+ cfg = load_config()
42
+ log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
44
43
  log_file = Path(SOOTHE_HOME) / "logs" / "soothe-cli.log"
45
44
  setup_logging(log_level, log_file=log_file)
46
45
 
@@ -50,6 +49,12 @@ def run_impl(
50
49
  if checkpointer == "postgresql":
51
50
  logger.info("PostgreSQL checkpointer configured; ensure server is running.")
52
51
 
52
+ # Apply CLI streaming overrides (RFC-614)
53
+ if streaming_enabled is not None:
54
+ cfg.output_streaming_enabled = streaming_enabled
55
+ if streaming_mode is not None:
56
+ cfg.output_streaming_mode = streaming_mode
57
+
53
58
  startup_elapsed_ms = (time.perf_counter() - startup_start) * 1000
54
59
  logger.info("[Startup] ✓ Ready (%.1fms)", startup_elapsed_ms)
55
60
 
@@ -61,7 +66,6 @@ def run_impl(
61
66
  cfg,
62
67
  prompt or "",
63
68
  thread_id=thread_id,
64
- output_format=output_format,
65
69
  autonomous=autonomous,
66
70
  max_iterations=max_iterations,
67
71
  )
@@ -19,6 +19,11 @@ from soothe_sdk.client import WebSocketClient, is_daemon_live, websocket_url_fro
19
19
 
20
20
  from soothe_cli.shared import load_config
21
21
 
22
+ thread_app = typer.Typer(
23
+ name="thread",
24
+ help="Inspect conversation threads (read-only diagnostics)",
25
+ )
26
+
22
27
  # Display limits for thread list
23
28
  _TOPIC_DISPLAY_LIMIT = 30 # Max chars for last human message
24
29
  _TOPIC_TRUNCATE_KEEP = 27 # Leave room for "..."
@@ -116,6 +121,7 @@ def _echo_thread_table(rows: list[dict[str, object]]) -> None:
116
121
  typer.echo(f"{tid:<20} {t_status:<10} {created:<19} {last_msg:<19} {topic:<30}")
117
122
 
118
123
 
124
+ @thread_app.command("list")
119
125
  def thread_list(
120
126
  config: Annotated[
121
127
  str | None,
@@ -186,6 +192,7 @@ def thread_list(
186
192
  asyncio.run(_list())
187
193
 
188
194
 
195
+ @thread_app.command("show")
189
196
  def thread_show(
190
197
  thread_id: Annotated[str, typer.Argument(help="Thread ID to show.")],
191
198
  config: Annotated[
@@ -239,6 +246,7 @@ def thread_show(
239
246
  asyncio.run(_show())
240
247
 
241
248
 
249
+ @thread_app.command("export")
242
250
  def thread_export(
243
251
  thread_id: Annotated[str, typer.Argument(help="Thread ID to export.")],
244
252
  output: Annotated[
@@ -309,6 +317,7 @@ def thread_export(
309
317
  asyncio.run(_export())
310
318
 
311
319
 
320
+ @thread_app.command("stats")
312
321
  def thread_stats(
313
322
  thread_id: Annotated[str, typer.Argument(help="Thread ID.")],
314
323
  config: Annotated[
@@ -368,6 +377,7 @@ def thread_stats(
368
377
  asyncio.run(_stats())
369
378
 
370
379
 
380
+ @thread_app.command("artifacts")
371
381
  def thread_artifacts(
372
382
  thread_id: Annotated[str, typer.Argument(help="Thread ID to list artifacts for.")],
373
383
  config: Annotated[
@@ -1,13 +1,14 @@
1
1
  """Daemon-based execution for headless mode.
2
2
 
3
- Refactored to use RFC-0019 EventProcessor with CliRenderer.
3
+ Uses RFC-0019 EventProcessor with HeadlessCliRenderer (stdout: loop-tagged answers only).
4
4
  Uses WebSocket transport (RFC-0013).
5
5
  """
6
6
 
7
+ from __future__ import annotations
8
+
7
9
  import asyncio
8
- import json
9
10
  import logging
10
- import sys
11
+ import os
11
12
  from typing import Any
12
13
 
13
14
  import typer
@@ -17,7 +18,7 @@ from soothe_sdk.client import (
17
18
  websocket_url_from_config,
18
19
  )
19
20
 
20
- from soothe_cli.cli.renderer import CliRenderer
21
+ from soothe_cli.cli.execution.headless_renderer import HeadlessCliRenderer
21
22
  from soothe_cli.shared import EventProcessor
22
23
  from soothe_cli.shared.presentation_engine import PresentationEngine
23
24
  from soothe_cli.shared.subagent_routing import parse_subagent_from_input
@@ -34,27 +35,28 @@ async def run_headless_via_daemon(
34
35
  prompt: str,
35
36
  *,
36
37
  thread_id: str | None = None,
37
- output_format: str = "text",
38
38
  autonomous: bool = False,
39
39
  max_iterations: int | None = None,
40
40
  ) -> int:
41
41
  """Run a single prompt by connecting to a running daemon.
42
42
 
43
43
  Uses WebSocket transport for all connections (RFC-0013).
44
- Refactored to use RFC-0019 EventProcessor with CliRenderer.
44
+ Headless output is RFC-614 loop-tagged main-graph assistant text only (IG-343).
45
45
  """
46
46
  from soothe_sdk.client import WebSocketClient
47
47
 
48
48
  ws_url = websocket_url_from_config(cfg)
49
49
  client = WebSocketClient(url=ws_url)
50
- verbosity = cfg.logging.verbosity
50
+ final_output_mode = getattr(cfg, "final_output_mode", "streaming")
51
51
 
52
52
  try:
53
53
  await connect_websocket_with_retries(client)
54
+ cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
54
55
  status_event = await bootstrap_thread_session(
55
56
  client,
56
57
  resume_thread_id=thread_id,
57
- verbosity=verbosity,
58
+ verbosity="normal",
59
+ workspace=cli_ws,
58
60
  thread_status_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
59
61
  subscription_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
60
62
  )
@@ -69,25 +71,26 @@ async def run_headless_via_daemon(
69
71
 
70
72
  subagent_name, cleaned_prompt = parse_subagent_from_input(prompt)
71
73
 
72
- # Send the input
73
74
  await asyncio.wait_for(
74
75
  client.send_input(
75
76
  cleaned_prompt if subagent_name else prompt,
76
77
  autonomous=autonomous,
77
78
  max_iterations=max_iterations,
78
- subagent=subagent_name,
79
+ preferred_subagent=subagent_name,
79
80
  ),
80
81
  timeout=_SESSION_BOOTSTRAP_TIMEOUT_S,
81
82
  )
82
83
 
83
- # Initialize RFC-0019 unified event processor with one PresentationEngine
84
- # for pipeline + message gating (RFC-502).
85
84
  presentation = PresentationEngine()
86
- renderer = CliRenderer(verbosity=verbosity, presentation_engine=presentation)
87
- processor = EventProcessor(renderer, verbosity=verbosity, presentation_engine=presentation)
85
+ renderer = HeadlessCliRenderer()
86
+ processor = EventProcessor(
87
+ renderer,
88
+ final_output_mode=final_output_mode,
89
+ presentation_engine=presentation,
90
+ headless_output=True,
91
+ )
88
92
 
89
- has_error = False
90
- query_started = False # Track if we've seen the query start running
93
+ query_started = False
91
94
 
92
95
  while True:
93
96
  try:
@@ -104,13 +107,10 @@ async def run_headless_via_daemon(
104
107
 
105
108
  event_type = event.get("type", "")
106
109
 
107
- # IMMEDIATE error check - exit before any other processing
108
- # This ensures errors before query starts return immediately (IG-181)
109
110
  if event_type == "error":
110
111
  typer.echo(f"Daemon error: {event.get('message', 'unknown')}", err=True)
111
112
  return 1
112
113
 
113
- # Check for soothe.error.* events before query starts
114
114
  ev_data = event.get("data")
115
115
  if (
116
116
  not query_started
@@ -120,14 +120,11 @@ async def run_headless_via_daemon(
120
120
  typer.echo(f"Daemon error: {ev_data.get('error', 'unknown')}", err=True)
121
121
  return 1
122
122
 
123
- # Handle status changes (need to track query_started for timeout)
124
123
  if event_type == "status":
125
124
  state = event.get("state", "")
126
125
  if state == "running":
127
126
  query_started = True
128
127
  elif (state == "idle" and query_started) or state == "stopped":
129
- # loop.completed (and stray message chunks) may arrive *after* idle on the
130
- # WebSocket stream; draining avoids dropping completion + final stdout (test-case1).
131
128
  loop_clock = asyncio.get_event_loop()
132
129
  drain_deadline = loop_clock.time() + 2.5
133
130
  while loop_clock.time() < drain_deadline:
@@ -137,45 +134,13 @@ async def run_headless_via_daemon(
137
134
  break
138
135
  if not nxt:
139
136
  break
140
- if output_format == "jsonl":
141
- namespace = nxt.get("namespace", [])
142
- mode = nxt.get("mode", "")
143
- data = nxt.get("data")
144
- sys.stdout.write(
145
- json.dumps(
146
- {"namespace": list(namespace), "mode": mode, "data": data},
147
- default=str,
148
- )
149
- + "\n"
150
- )
151
- sys.stdout.flush()
152
- continue
153
137
  processor.process_event(nxt)
154
138
 
155
- processor.process_event(event) # Finalize (on_turn_end after drain)
139
+ processor.process_event(event)
156
140
  break
157
141
 
158
- # JSONL output bypass processor
159
- if output_format == "jsonl":
160
- namespace = event.get("namespace", [])
161
- mode = event.get("mode", "")
162
- data = event.get("data")
163
- sys.stdout.write(
164
- json.dumps(
165
- {"namespace": list(namespace), "mode": mode, "data": data}, default=str
166
- )
167
- + "\n"
168
- )
169
- sys.stdout.flush()
170
- continue
171
-
172
- # Delegate to unified event processor
173
142
  processor.process_event(event)
174
143
 
175
- # Note: Final newline is handled by renderer.on_turn_end() called
176
- # when status changes to idle/stopped in _handle_status().
177
- # Daemon lifecycle remains silent in normal headless mode.
178
-
179
144
  except (ConnectionError, OSError, TimeoutError) as e:
180
145
  logger.exception("Daemon connection failed")
181
146
  from soothe_sdk.utils import format_cli_error
@@ -189,6 +154,6 @@ async def run_headless_via_daemon(
189
154
  typer.echo(f"Error: {format_cli_error(e)}", err=True)
190
155
  return 1
191
156
  else:
192
- return 1 if has_error else 0
157
+ return 0
193
158
  finally:
194
159
  await client.close()
@@ -23,13 +23,12 @@ def run_headless(
23
23
  prompt: str,
24
24
  *,
25
25
  thread_id: str | None = None,
26
- output_format: str = "text",
27
26
  autonomous: bool = False,
28
27
  max_iterations: int | None = None,
29
28
  ) -> None:
30
29
  """Run a single prompt with streaming output and progress events.
31
30
 
32
- Connects to running daemon via WebSocket if available to avoid RocksDB lock conflicts.
31
+ Connects to running daemon via WebSocket if available to avoid database lock conflicts.
33
32
  Auto-starts daemon if not running (RFC-0013 daemon lifecycle).
34
33
 
35
34
  Note (RFC-0013): Daemon persists after request completion. Use 'soothed stop'
@@ -41,8 +40,8 @@ def run_headless(
41
40
  ws_url = websocket_url_from_config(cfg)
42
41
 
43
42
  # Auto-start daemon if not running (RFC-0013) - WebSocket RPC checks (IG-174 Phase 1)
44
- async def _check_and_ensure_daemon() -> None:
45
- """Check daemon status and auto-start if needed."""
43
+ async def _run_headless_pipeline() -> int:
44
+ """Ensure daemon is reachable, then run the headless daemon session."""
46
45
  daemon_live = await is_daemon_live(ws_url, timeout=5.0)
47
46
 
48
47
  if not daemon_live:
@@ -75,19 +74,15 @@ def run_headless(
75
74
  # Note: We don't fail here - let the connection attempt handle errors
76
75
  # This allows tests and edge cases to proceed with mocked daemons
77
76
 
78
- asyncio.run(_check_and_ensure_daemon())
79
-
80
- # Connect to daemon and execute
81
- daemon_exit_code = asyncio.run(
82
- run_headless_via_daemon(
77
+ return await run_headless_via_daemon(
83
78
  cfg,
84
79
  prompt,
85
80
  thread_id=thread_id,
86
- output_format=output_format,
87
81
  autonomous=autonomous,
88
82
  max_iterations=max_iterations,
89
83
  )
90
- )
84
+
85
+ daemon_exit_code = asyncio.run(_run_headless_pipeline())
91
86
 
92
87
  # Handle daemon fallback (unresponsive daemon)
93
88
  if daemon_exit_code == _DAEMON_FALLBACK_EXIT_CODE:
@@ -0,0 +1,107 @@
1
+ """Minimal stdout-only renderer for headless CLI (IG-343).
2
+
3
+ Emits RFC-614 loop-tagged assistant text for the main graph (empty LangGraph namespace)
4
+ and loop-tagged finals (including replayed ``goal_completion`` from IG-355). Subgraph
5
+ namespaced prose is suppressed unless loop-tagged. Stderr is used for errors.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from typing import Any
12
+
13
+ from rich.console import Console
14
+
15
+ from soothe_cli.shared.renderer_base import RendererBase
16
+
17
+
18
+ class HeadlessCliRenderer(RendererBase):
19
+ """Headless mode: clean assistant output on stdout, errors on stderr."""
20
+
21
+ def __init__(self) -> None:
22
+ super().__init__()
23
+ self.console = Console()
24
+
25
+ def on_assistant_text(
26
+ self,
27
+ text: str,
28
+ *,
29
+ is_main: bool,
30
+ is_streaming: bool,
31
+ task_scope: tuple[str, str] | None = None,
32
+ ) -> None:
33
+ if not is_main or task_scope:
34
+ return
35
+ payload = text if is_streaming else self.repair_concatenated_output(text)
36
+ if not payload:
37
+ return
38
+ sys.stdout.write(payload)
39
+ sys.stdout.flush()
40
+
41
+ def on_streaming_output(
42
+ self,
43
+ event_type: str,
44
+ text: str,
45
+ *,
46
+ is_chunk: bool,
47
+ namespace: tuple[str, ...],
48
+ ) -> None:
49
+ self.on_assistant_text(text, is_main=True, is_streaming=is_chunk)
50
+
51
+ def on_tool_call(
52
+ self,
53
+ name: str,
54
+ args: dict[str, Any],
55
+ tool_call_id: str,
56
+ *,
57
+ is_main: bool,
58
+ task_scope: tuple[str, str] | None = None,
59
+ ) -> None:
60
+ del name, args, tool_call_id, is_main, task_scope
61
+
62
+ def on_tool_result(
63
+ self,
64
+ name: str,
65
+ result: str,
66
+ tool_call_id: str,
67
+ *,
68
+ is_error: bool,
69
+ is_main: bool,
70
+ task_scope: tuple[str, str] | None = None,
71
+ ) -> None:
72
+ del name, result, tool_call_id, is_error, is_main, task_scope
73
+
74
+ def on_status_change(self, state: str) -> None:
75
+ del state
76
+
77
+ def on_error(self, error: str, *, context: str | None = None) -> None:
78
+ prefix = f"[{context}] " if context else ""
79
+ sys.stderr.write(f"{prefix}ERROR: {error}\n")
80
+ sys.stderr.flush()
81
+
82
+ def on_progress_event(
83
+ self,
84
+ event_type: str,
85
+ data: dict[str, Any],
86
+ *,
87
+ namespace: tuple[str, ...],
88
+ task_scope: tuple[str, str] | None = None,
89
+ ) -> None:
90
+ del event_type, data, namespace, task_scope
91
+
92
+ def on_plan_created(self, plan: Any) -> None:
93
+ del plan
94
+
95
+ def on_plan_step_started(self, step_id: str, description: str) -> None:
96
+ del step_id, description
97
+
98
+ def on_plan_step_completed(
99
+ self,
100
+ step_id: str,
101
+ success: bool,
102
+ duration_ms: int,
103
+ ) -> None:
104
+ del step_id, success, duration_ms
105
+
106
+ def on_turn_end(self) -> None:
107
+ """End of turn; headless does not append synthetic newlines to stdout."""