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.
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/PKG-INFO +10 -12
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/README.md +8 -10
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/pyproject.toml +2 -2
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -4
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/commands/run_cmd.py +2 -8
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/commands/thread_cmd.py +10 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/execution/daemon.py +15 -56
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/execution/headless.py +5 -10
- soothe_cli-0.4.4/src/soothe_cli/cli/execution/headless_renderer.py +107 -0
- soothe_cli-0.4.4/src/soothe_cli/cli/main.py +151 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/stream/__init__.py +1 -2
- soothe_cli-0.4.4/src/soothe_cli/cli/stream/context.py +65 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/stream/display_line.py +1 -9
- soothe_cli-0.4.4/src/soothe_cli/cli/stream/formatter.py +290 -0
- soothe_cli-0.4.4/src/soothe_cli/cli/stream/pipeline.py +500 -0
- soothe_cli-0.4.4/src/soothe_cli/cli/stream/task_scope.py +46 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/config/cli_config.py +3 -34
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/shared/__init__.py +31 -21
- soothe_cli-0.4.4/src/soothe_cli/shared/commands/__init__.py +37 -0
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/commands}/command_router.py +6 -6
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/commands}/slash_commands.py +1 -1
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/shared/config_loader.py +1 -1
- soothe_cli-0.4.4/src/soothe_cli/shared/core/__init__.py +13 -0
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/core}/event_processor.py +277 -104
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/core}/presentation_engine.py +39 -5
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/core}/processor_state.py +15 -6
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/core}/renderer_protocol.py +11 -1
- soothe_cli-0.4.4/src/soothe_cli/shared/events/__init__.py +15 -0
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/events}/display_policy.py +12 -110
- soothe_cli-0.4.4/src/soothe_cli/shared/events/explore_task_display.py +89 -0
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/events}/stream_accumulator.py +3 -0
- soothe_cli-0.4.4/src/soothe_cli/shared/presentation_engine.py +5 -0
- soothe_cli-0.4.4/src/soothe_cli/shared/renderer_base.py +5 -0
- soothe_cli-0.4.4/src/soothe_cli/shared/rendering/__init__.py +9 -0
- {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
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/rendering}/renderer_base.py +1 -1
- soothe_cli-0.4.4/src/soothe_cli/shared/stream_accumulator.py +13 -0
- 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
- soothe_cli-0.4.4/src/soothe_cli/shared/tools/__init__.py +3 -0
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/message_processing.py +1 -1
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/tools}/rendering.py +1 -1
- {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
- {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
- soothe_cli-0.4.4/src/soothe_cli/shared/tools/tool_card_visibility.py +116 -0
- soothe_cli-0.4.4/src/soothe_cli/shared/tools/tool_formatters/__init__.py +29 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_env_vars.py +1 -4
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/app.py +17 -103
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/command_registry.py +0 -5
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/config.py +41 -259
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/daemon_session.py +5 -3
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/model_config.py +6 -6
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/preview_limits.py +4 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/textual_adapter.py +412 -94
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/tool_display.py +27 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/messages.py +312 -133
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/welcome.py +1 -0
- soothe_cli-0.4.2/src/soothe_cli/cli/commands/config_cmd.py +0 -282
- soothe_cli-0.4.2/src/soothe_cli/cli/commands/status_cmd.py +0 -121
- soothe_cli-0.4.2/src/soothe_cli/cli/main.py +0 -417
- soothe_cli-0.4.2/src/soothe_cli/cli/renderer.py +0 -434
- soothe_cli-0.4.2/src/soothe_cli/cli/stream/context.py +0 -142
- soothe_cli-0.4.2/src/soothe_cli/cli/stream/formatter.py +0 -456
- soothe_cli-0.4.2/src/soothe_cli/cli/stream/pipeline.py +0 -790
- soothe_cli-0.4.2/src/soothe_cli/cli/utils.py +0 -46
- soothe_cli-0.4.2/src/soothe_cli/shared/tool_formatters/__init__.py +0 -29
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/.gitignore +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/commands/__init__.py +0 -0
- /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
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/plan/__init__.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/plan/rich_tree.py +0 -0
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/commands}/subagent_routing.py +0 -0
- {soothe_cli-0.4.2/src/soothe_cli/shared → soothe_cli-0.4.4/src/soothe_cli/shared/events}/essential_events.py +0 -0
- {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
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_ask_user_types.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_session_stats.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/app.tcss +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/file_ops.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/formatting.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/message_display_filter.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/output.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/approval.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/thread_selector.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
- {soothe_cli-0.4.2 → soothe_cli-0.4.4}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
- {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.
|
|
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: <
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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,<
|
|
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.
|
|
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(
|
|
47
|
-
log_level = resolve_cli_log_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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
45
|
-
"""
|
|
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
|
-
|
|
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()
|