soothe-cli 0.4.2__tar.gz → 0.4.4__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 (134) hide show
  1. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/PKG-INFO +10 -12
  2. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/README.md +8 -10
  3. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/pyproject.toml +2 -2
  4. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -4
  5. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/commands/run_cmd.py +2 -8
  6. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/commands/thread_cmd.py +10 -0
  7. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/execution/daemon.py +15 -56
  8. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/execution/headless.py +5 -10
  9. soothe_cli-0.4.4/src/soothe_cli/cli/execution/headless_renderer.py +107 -0
  10. soothe_cli-0.4.4/src/soothe_cli/cli/main.py +151 -0
  11. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/stream/__init__.py +1 -2
  12. soothe_cli-0.4.4/src/soothe_cli/cli/stream/context.py +65 -0
  13. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/stream/display_line.py +1 -9
  14. soothe_cli-0.4.4/src/soothe_cli/cli/stream/formatter.py +290 -0
  15. soothe_cli-0.4.4/src/soothe_cli/cli/stream/pipeline.py +500 -0
  16. soothe_cli-0.4.4/src/soothe_cli/cli/stream/task_scope.py +46 -0
  17. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/config/cli_config.py +3 -34
  18. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/shared/__init__.py +31 -21
  19. soothe_cli-0.4.4/src/soothe_cli/shared/commands/__init__.py +37 -0
  20. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/commands}/command_router.py +6 -6
  21. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/commands}/slash_commands.py +1 -1
  22. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/shared/config_loader.py +1 -1
  23. soothe_cli-0.4.4/src/soothe_cli/shared/core/__init__.py +13 -0
  24. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/core}/event_processor.py +277 -104
  25. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/core}/presentation_engine.py +39 -5
  26. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/core}/processor_state.py +15 -6
  27. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/core}/renderer_protocol.py +11 -1
  28. soothe_cli-0.4.4/src/soothe_cli/shared/events/__init__.py +15 -0
  29. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/events}/display_policy.py +12 -110
  30. soothe_cli-0.4.4/src/soothe_cli/shared/events/explore_task_display.py +89 -0
  31. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/events}/stream_accumulator.py +3 -0
  32. soothe_cli-0.4.4/src/soothe_cli/shared/presentation_engine.py +5 -0
  33. soothe_cli-0.4.4/src/soothe_cli/shared/renderer_base.py +5 -0
  34. soothe_cli-0.4.4/src/soothe_cli/shared/rendering/__init__.py +9 -0
  35. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/rendering}/async_renderer_protocol.py +9 -1
  36. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/rendering}/renderer_base.py +1 -1
  37. soothe_cli-0.4.4/src/soothe_cli/shared/stream_accumulator.py +13 -0
  38. soothe_cli-0.4.2/src/soothe_cli/cli/commands/subagent_names.py → soothe_cli-0.4.4/src/soothe_cli/shared/subagent_routing.py +2 -4
  39. soothe_cli-0.4.4/src/soothe_cli/shared/tools/__init__.py +3 -0
  40. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/message_processing.py +1 -1
  41. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/rendering.py +1 -1
  42. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_call_resolution.py +8 -7
  43. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_card_payload.py +22 -5
  44. soothe_cli-0.4.4/src/soothe_cli/shared/tools/tool_card_visibility.py +116 -0
  45. soothe_cli-0.4.4/src/soothe_cli/shared/tools/tool_formatters/__init__.py +29 -0
  46. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_formatters/base.py +1 -1
  47. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_formatters/execution.py +18 -5
  48. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_formatters/fallback.py +2 -2
  49. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_formatters/file_ops.py +2 -2
  50. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_formatters/goal_formatter.py +2 -2
  51. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_formatters/media.py +2 -2
  52. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_formatters/structured.py +2 -2
  53. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_formatters/subagent.py +2 -2
  54. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_formatters/web.py +2 -2
  55. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_message_format.py +41 -0
  56. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/tool_output_formatter.py +2 -2
  57. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_env_vars.py +1 -4
  58. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/app.py +17 -103
  59. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/command_registry.py +0 -5
  60. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/config.py +41 -259
  61. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/daemon_session.py +5 -3
  62. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/model_config.py +6 -6
  63. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/preview_limits.py +4 -0
  64. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/textual_adapter.py +412 -94
  65. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/tool_display.py +27 -0
  66. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/messages.py +312 -133
  67. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/welcome.py +1 -0
  68. soothe_cli-0.4.2/src/soothe_cli/cli/commands/config_cmd.py +0 -282
  69. soothe_cli-0.4.2/src/soothe_cli/cli/commands/status_cmd.py +0 -121
  70. soothe_cli-0.4.2/src/soothe_cli/cli/main.py +0 -417
  71. soothe_cli-0.4.2/src/soothe_cli/cli/renderer.py +0 -434
  72. soothe_cli-0.4.2/src/soothe_cli/cli/stream/context.py +0 -142
  73. soothe_cli-0.4.2/src/soothe_cli/cli/stream/formatter.py +0 -456
  74. soothe_cli-0.4.2/src/soothe_cli/cli/stream/pipeline.py +0 -790
  75. soothe_cli-0.4.2/src/soothe_cli/cli/utils.py +0 -46
  76. soothe_cli-0.4.2/src/soothe_cli/shared/tool_formatters/__init__.py +0 -29
  77. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/.gitignore +0 -0
  78. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/__init__.py +0 -0
  79. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/__init__.py +0 -0
  80. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/commands/__init__.py +0 -0
  81. /soothe_cli-0.4.2/src/soothe_cli/loop_commands.py → /soothe_cli-0.4.4/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  82. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/execution/__init__.py +0 -0
  83. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/execution/launcher.py +0 -0
  84. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/config/__init__.py +0 -0
  85. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/plan/__init__.py +0 -0
  86. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/plan/rich_tree.py +0 -0
  87. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/commands}/subagent_routing.py +0 -0
  88. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/events}/essential_events.py +0 -0
  89. {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/events}/tui_trace_log.py +0 -0
  90. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/__init__.py +0 -0
  91. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  92. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_cli_context.py +0 -0
  93. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_session_stats.py +0 -0
  94. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_version.py +0 -0
  95. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/app.tcss +0 -0
  96. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/file_ops.py +0 -0
  97. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/formatting.py +0 -0
  98. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/hooks.py +0 -0
  99. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/input.py +0 -0
  100. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/media_utils.py +0 -0
  101. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/message_display_filter.py +0 -0
  102. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/output.py +0 -0
  103. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/project_utils.py +0 -0
  104. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/sessions.py +0 -0
  105. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/skills/__init__.py +0 -0
  106. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/skills/invocation.py +0 -0
  107. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/skills/load.py +0 -0
  108. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/theme.py +0 -0
  109. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/unicode_security.py +0 -0
  110. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/update_check.py +0 -0
  111. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  112. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/_links.py +0 -0
  113. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/approval.py +0 -0
  114. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  115. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  116. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  117. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  118. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  119. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  120. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/diff.py +0 -0
  121. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/editor.py +0 -0
  122. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/history.py +0 -0
  123. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/loading.py +0 -0
  124. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  125. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  126. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  127. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  128. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  129. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/status.py +0 -0
  130. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  131. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
  132. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  133. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  134. {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.4.2
3
+ Version: 0.4.4
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,12 +17,10 @@ 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,
26
24
  streaming_enabled: bool | None = None,
27
25
  streaming_mode: str | None = None,
28
26
  ) -> None:
@@ -30,21 +28,18 @@ def run_impl(
30
28
 
31
29
  Args:
32
30
  prompt: Optional prompt for headless mode
33
- config: Deprecated; passed through for ``--config`` compatibility (ignored for
34
- client settings; see ``load_config``).
35
31
  thread_id: Thread ID to resume
36
32
  no_tui: Force headless mode
37
33
  autonomous: Enable autonomous iteration mode
38
34
  max_iterations: Max iterations for autonomous mode
39
- output_format: Output format (text or jsonl)
40
35
  streaming_enabled: Override daemon streaming enabled setting (RFC-614)
41
36
  streaming_mode: Override daemon streaming mode ('streaming' or 'batch')
42
37
  """
43
38
  startup_start = time.perf_counter()
44
39
 
45
40
  try:
46
- cfg = load_config(config)
47
- 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)
48
43
  log_file = Path(SOOTHE_HOME) / "logs" / "soothe-cli.log"
49
44
  setup_logging(log_level, log_file=log_file)
50
45
 
@@ -71,7 +66,6 @@ def run_impl(
71
66
  cfg,
72
67
  prompt or "",
73
68
  thread_id=thread_id,
74
- output_format=output_format,
75
69
  autonomous=autonomous,
76
70
  max_iterations=max_iterations,
77
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,28 +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
51
50
  final_output_mode = getattr(cfg, "final_output_mode", "streaming")
52
51
 
53
52
  try:
54
53
  await connect_websocket_with_retries(client)
54
+ cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
55
55
  status_event = await bootstrap_thread_session(
56
56
  client,
57
57
  resume_thread_id=thread_id,
58
- verbosity=verbosity,
58
+ verbosity="normal",
59
+ workspace=cli_ws,
59
60
  thread_status_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
60
61
  subscription_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
61
62
  )
@@ -70,30 +71,26 @@ async def run_headless_via_daemon(
70
71
 
71
72
  subagent_name, cleaned_prompt = parse_subagent_from_input(prompt)
72
73
 
73
- # Send the input
74
74
  await asyncio.wait_for(
75
75
  client.send_input(
76
76
  cleaned_prompt if subagent_name else prompt,
77
77
  autonomous=autonomous,
78
78
  max_iterations=max_iterations,
79
- subagent=subagent_name,
79
+ preferred_subagent=subagent_name,
80
80
  ),
81
81
  timeout=_SESSION_BOOTSTRAP_TIMEOUT_S,
82
82
  )
83
83
 
84
- # Initialize RFC-0019 unified event processor with one PresentationEngine
85
- # for pipeline + message gating (RFC-502).
86
84
  presentation = PresentationEngine()
87
- renderer = CliRenderer(verbosity=verbosity, presentation_engine=presentation)
85
+ renderer = HeadlessCliRenderer()
88
86
  processor = EventProcessor(
89
87
  renderer,
90
- verbosity=verbosity,
91
88
  final_output_mode=final_output_mode,
92
89
  presentation_engine=presentation,
90
+ headless_output=True,
93
91
  )
94
92
 
95
- has_error = False
96
- query_started = False # Track if we've seen the query start running
93
+ query_started = False
97
94
 
98
95
  while True:
99
96
  try:
@@ -110,13 +107,10 @@ async def run_headless_via_daemon(
110
107
 
111
108
  event_type = event.get("type", "")
112
109
 
113
- # IMMEDIATE error check - exit before any other processing
114
- # This ensures errors before query starts return immediately (IG-181)
115
110
  if event_type == "error":
116
111
  typer.echo(f"Daemon error: {event.get('message', 'unknown')}", err=True)
117
112
  return 1
118
113
 
119
- # Check for soothe.error.* events before query starts
120
114
  ev_data = event.get("data")
121
115
  if (
122
116
  not query_started
@@ -126,14 +120,11 @@ async def run_headless_via_daemon(
126
120
  typer.echo(f"Daemon error: {ev_data.get('error', 'unknown')}", err=True)
127
121
  return 1
128
122
 
129
- # Handle status changes (need to track query_started for timeout)
130
123
  if event_type == "status":
131
124
  state = event.get("state", "")
132
125
  if state == "running":
133
126
  query_started = True
134
127
  elif (state == "idle" and query_started) or state == "stopped":
135
- # loop.completed (and stray message chunks) may arrive *after* idle on the
136
- # WebSocket stream; draining avoids dropping completion + final stdout (test-case1).
137
128
  loop_clock = asyncio.get_event_loop()
138
129
  drain_deadline = loop_clock.time() + 2.5
139
130
  while loop_clock.time() < drain_deadline:
@@ -143,45 +134,13 @@ async def run_headless_via_daemon(
143
134
  break
144
135
  if not nxt:
145
136
  break
146
- if output_format == "jsonl":
147
- namespace = nxt.get("namespace", [])
148
- mode = nxt.get("mode", "")
149
- data = nxt.get("data")
150
- sys.stdout.write(
151
- json.dumps(
152
- {"namespace": list(namespace), "mode": mode, "data": data},
153
- default=str,
154
- )
155
- + "\n"
156
- )
157
- sys.stdout.flush()
158
- continue
159
137
  processor.process_event(nxt)
160
138
 
161
- processor.process_event(event) # Finalize (on_turn_end after drain)
139
+ processor.process_event(event)
162
140
  break
163
141
 
164
- # JSONL output bypass processor
165
- if output_format == "jsonl":
166
- namespace = event.get("namespace", [])
167
- mode = event.get("mode", "")
168
- data = event.get("data")
169
- sys.stdout.write(
170
- json.dumps(
171
- {"namespace": list(namespace), "mode": mode, "data": data}, default=str
172
- )
173
- + "\n"
174
- )
175
- sys.stdout.flush()
176
- continue
177
-
178
- # Delegate to unified event processor
179
142
  processor.process_event(event)
180
143
 
181
- # Note: Final newline is handled by renderer.on_turn_end() called
182
- # when status changes to idle/stopped in _handle_status().
183
- # Daemon lifecycle remains silent in normal headless mode.
184
-
185
144
  except (ConnectionError, OSError, TimeoutError) as e:
186
145
  logger.exception("Daemon connection failed")
187
146
  from soothe_sdk.utils import format_cli_error
@@ -195,6 +154,6 @@ async def run_headless_via_daemon(
195
154
  typer.echo(f"Error: {format_cli_error(e)}", err=True)
196
155
  return 1
197
156
  else:
198
- return 1 if has_error else 0
157
+ return 0
199
158
  finally:
200
159
  await client.close()
@@ -23,7 +23,6 @@ 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:
@@ -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."""
@@ -0,0 +1,151 @@
1
+ """Main CLI entry point using Typer."""
2
+
3
+ # Load environment variables from .env file BEFORE any langchain imports
4
+ # so provider API keys and other env-backed config are visible at import time.
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ from importlib.metadata import version # noqa: E402
10
+ from typing import Annotated # noqa: E402
11
+
12
+ import typer # noqa: E402
13
+
14
+ app = typer.Typer(
15
+ name="soothe",
16
+ help="Intelligent AI assistant for complex tasks",
17
+ no_args_is_help=False,
18
+ add_completion=False,
19
+ )
20
+
21
+
22
+ def add_help_alias(nested_app: typer.Typer) -> None:
23
+ """Add -h as an alias for --help to a nested Typer app.
24
+
25
+ This is a workaround for Typer not supporting -h for nested command groups.
26
+ Must be called AFTER creating the nested app but BEFORE adding commands.
27
+
28
+ Args:
29
+ nested_app: The nested Typer app to add -h support to.
30
+ """
31
+
32
+ # Add a callback that defines -h option
33
+ @nested_app.callback(invoke_without_command=True)
34
+ def help_callback(
35
+ ctx: typer.Context,
36
+ show_help: Annotated[ # noqa: FBT002
37
+ bool,
38
+ typer.Option("-h", "--help", is_flag=True, help="Show this message and exit."),
39
+ ] = False,
40
+ ) -> None:
41
+ # If -h/--help is passed, show help and exit before command parsing
42
+ if show_help:
43
+ typer.echo(ctx.get_help())
44
+ raise typer.Exit(code=0)
45
+
46
+ # If no subcommand and no help flag, show help by default
47
+ if ctx.invoked_subcommand is None:
48
+ typer.echo(ctx.get_help())
49
+ raise typer.Exit(code=0)
50
+
51
+
52
+ @app.callback(invoke_without_command=True)
53
+ def main(
54
+ ctx: typer.Context,
55
+ prompt: Annotated[
56
+ str | None,
57
+ typer.Option(
58
+ "--prompt", "-p", help="Prompt to send as user message (headless single-shot mode)."
59
+ ),
60
+ ] = None,
61
+ no_tui: Annotated[ # noqa: FBT002
62
+ bool,
63
+ typer.Option("--no-tui", help="Disable TUI; run single prompt and exit."),
64
+ ] = False,
65
+ streaming: Annotated[
66
+ bool | None,
67
+ typer.Option("--streaming/--no-streaming", help="Enable/disable output streaming."),
68
+ ] = None,
69
+ streaming_mode: Annotated[
70
+ str | None,
71
+ typer.Option("--streaming-mode", help="Streaming mode: 'streaming' or 'batch'"),
72
+ ] = None,
73
+ show_help: Annotated[ # noqa: FBT002
74
+ bool,
75
+ typer.Option("--help", "-h", is_flag=True, help="Show this message and exit."),
76
+ ] = False,
77
+ show_version: Annotated[ # noqa: FBT002
78
+ bool,
79
+ typer.Option("--version", is_flag=True, help="Show version and exit."),
80
+ ] = False,
81
+ ) -> None:
82
+ """Soothe CLI - Intelligent AI assistant client.
83
+
84
+ Run without arguments for interactive TUI mode, or provide a prompt via --prompt/-p option.
85
+
86
+ Note: This is the CLI client. Use 'soothed' command to manage the daemon server.
87
+
88
+ Examples:
89
+ soothe # Interactive TUI mode
90
+ soothe -p "Research AI advances" # Headless single-prompt mode
91
+ soothe loop list # List AgentLoop instances
92
+ """
93
+ # Handle -h/--help flag
94
+ if show_help:
95
+ typer.echo(ctx.get_help())
96
+ raise typer.Exit
97
+
98
+ # Handle --version flag
99
+ if show_version:
100
+ typer.echo(f"soothe {version('soothe-cli')}")
101
+ raise typer.Exit
102
+
103
+ # Only run default behavior if no subcommand is being invoked
104
+ if ctx.invoked_subcommand is None:
105
+ from soothe_cli.cli.commands.run_cmd import run_impl
106
+
107
+ run_impl(
108
+ prompt=prompt,
109
+ thread_id=None,
110
+ no_tui=no_tui,
111
+ autonomous=False,
112
+ max_iterations=None,
113
+ streaming_enabled=streaming,
114
+ streaming_mode=streaming_mode,
115
+ )
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Sub-command groups (nested Typer apps)
120
+ # ---------------------------------------------------------------------------
121
+ # Thread: read-only diagnostics per RFC-503 (Loop-First UX). Lifecycle
122
+ # management lives under `soothe loop <subcommand>`.
123
+
124
+ from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
125
+ from soothe_cli.cli.commands.loop_cmd import loop_app as _loop_app # noqa: E402
126
+ from soothe_cli.cli.commands.thread_cmd import thread_app as _thread_app # noqa: E402
127
+
128
+ for _sub_app, _name in (
129
+ (_thread_app, "thread"),
130
+ (_loop_app, "loop"),
131
+ (_autopilot_app, "autopilot"),
132
+ ):
133
+ add_help_alias(_sub_app)
134
+ app.add_typer(_sub_app, name=_name)
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Help Command
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ @app.command(name="help")
143
+ def help_command(ctx: typer.Context) -> None:
144
+ """Show help message and exit."""
145
+ # Get the parent context (the main app) to show full help
146
+ parent_ctx = ctx.parent or ctx
147
+ typer.echo(parent_ctx.get_help())
148
+
149
+
150
+ if __name__ == "__main__":
151
+ app()