soothe-cli 0.7.18__tar.gz → 0.8.1__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.7.18 → soothe_cli-0.8.1}/PKG-INFO +2 -1
  2. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/pyproject.toml +2 -1
  3. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/commands/autopilot_cmd.py +2 -2
  4. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/commands/config_cmd.py +1 -1
  5. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/commands/cron_cmd.py +1 -1
  6. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/commands/loop_cmd.py +16 -4
  7. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/commands/run_cmd.py +9 -62
  8. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/commands/status_cmd.py +1 -1
  9. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/execution/daemon.py +6 -47
  10. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/execution/headless.py +1 -1
  11. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/main.py +1 -1
  12. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/config/cli_config.py +1 -1
  13. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/headless/processor.py +2 -2
  14. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/headless/processor_state.py +1 -1
  15. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/parse/tool_result.py +2 -0
  16. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/presentation/renderer_protocol.py +1 -1
  17. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/state/step_router.py +52 -1
  18. soothe_cli-0.8.1/src/soothe_cli/runtime/transport/session.py +80 -0
  19. soothe_cli-0.8.1/src/soothe_cli/runtime/turn/pipeline.py +35 -0
  20. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/wire/message_text.py +1 -1
  21. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/_app.py +54 -2
  22. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/_execution.py +2 -13
  23. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/_messages_mixin.py +8 -34
  24. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/_model.py +3 -2
  25. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/_startup.py +4 -4
  26. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/command_registry.py +6 -0
  27. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/commands/__init__.py +2 -2
  28. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/commands/command_router.py +8 -7
  29. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/commands/slash_commands.py +9 -3
  30. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/commands/subagent_routing.py +18 -6
  31. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/config.py +1 -1
  32. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/model_config.py +9 -3
  33. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/skills/invocation.py +5 -2
  34. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/spinner_labels.py +8 -1
  35. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/textual_adapter.py +246 -16
  36. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/theme.py +1 -1
  37. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/tips.py +1 -1
  38. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/tool_display.py +1 -1
  39. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/update_check.py +1 -1
  40. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/chat_input.py +1 -1
  41. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/context_data.py +38 -31
  42. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/context_viewer.py +7 -4
  43. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/help_screen.py +3 -3
  44. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +1 -1
  45. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +10 -3
  46. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/status.py +25 -4
  47. soothe_cli-0.7.18/src/soothe_cli/runtime/transport/session.py +0 -757
  48. soothe_cli-0.7.18/src/soothe_cli/runtime/turn/pipeline.py +0 -404
  49. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/.gitignore +0 -0
  50. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/README.md +0 -0
  51. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/__init__.py +0 -0
  52. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/__init__.py +0 -0
  53. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/commands/__init__.py +0 -0
  54. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/execution/__init__.py +0 -0
  55. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  56. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  57. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/cli/execution/launcher.py +0 -0
  58. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/config/__init__.py +0 -0
  59. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/config/loader.py +0 -0
  60. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/config/logging_setup.py +0 -0
  61. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/__init__.py +0 -0
  62. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  63. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  64. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  65. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  66. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  67. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  68. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  69. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
  70. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  71. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
  72. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  73. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  74. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  75. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/state/transcript.py +0 -0
  76. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/token_events_debug.py +0 -0
  77. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/token_usage.py +0 -0
  78. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  79. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  80. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/runtime/wire/messages.py +0 -0
  81. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/__init__.py +0 -0
  82. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/_cli_context.py +0 -0
  83. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/_env_vars.py +0 -0
  84. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/_version.py +0 -0
  85. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/__init__.py +0 -0
  86. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/_history.py +0 -0
  87. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/_module_init.py +0 -0
  88. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/_ui.py +0 -0
  89. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/app/app.tcss +0 -0
  90. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/binding.py +0 -0
  91. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/file_change_notify.py +0 -0
  92. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  93. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/hooks.py +0 -0
  94. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/input.py +0 -0
  95. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/markdown_theme.py +0 -0
  96. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/media_utils.py +0 -0
  97. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/path_utils.py +0 -0
  98. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/preview_limits.py +0 -0
  99. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/project_utils.py +0 -0
  100. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/sessions.py +0 -0
  101. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/shell_color.py +0 -0
  102. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/skills/__init__.py +0 -0
  103. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/skills/load.py +0 -0
  104. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/unicode_security.py +0 -0
  105. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  106. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/_links.py +0 -0
  107. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  108. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  109. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/diff.py +0 -0
  110. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/editor.py +0 -0
  111. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  112. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/history.py +0 -0
  113. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/loading.py +0 -0
  114. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  115. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  116. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  117. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  118. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
  119. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  120. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
  121. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
  122. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
  123. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
  124. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
  125. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
  126. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  127. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  128. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
  129. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  130. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  131. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  132. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
  133. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  134. {soothe_cli-0.7.18 → soothe_cli-0.8.1}/src/soothe_cli/tui/widgets/welcome.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.7.18
3
+ Version: 0.8.1
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/mirasoth/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -22,6 +22,7 @@ Requires-Dist: pillow<12.0.0,>=10.0.0
22
22
  Requires-Dist: python-dotenv<2.0.0,>=1.0.0
23
23
  Requires-Dist: pyyaml<7.0.0,>=6.0.0
24
24
  Requires-Dist: rich<15.0.0,>=13.0.0
25
+ Requires-Dist: soothe-client-python<1.0.0,>=0.5.25
25
26
  Requires-Dist: soothe-sdk<1.0.0,>=0.5.25
26
27
  Requires-Dist: textual<10.0.0,>=8.0.0
27
28
  Requires-Dist: typer<2.0.0,>=0.9.0
@@ -23,7 +23,8 @@ classifiers = [
23
23
  "Topic :: Software Development :: Libraries :: Python Modules",
24
24
  ]
25
25
  dependencies = [
26
- "soothe-sdk>=0.5.25,<1.0.0", # Subagent wire helpers (types owned by soothe subagents)
26
+ "soothe-sdk>=0.5.25,<1.0.0", # events/display/wire contracts
27
+ "soothe-client-python>=0.5.25,<1.0.0", # WebSocket transport client
27
28
  "typer>=0.9.0,<2.0.0", # CLI framework
28
29
  "textual>=8.0.0,<10.0.0", # TUI framework
29
30
  "rich>=13.0.0,<15.0.0", # Console output
@@ -14,12 +14,12 @@ from collections import Counter
14
14
  from pathlib import Path
15
15
 
16
16
  import typer
17
- from soothe_sdk.client import (
17
+ from soothe_client import (
18
18
  is_daemon_live,
19
19
  websocket_url_from_config,
20
20
  ws_command_client_from_config,
21
21
  )
22
- from soothe_sdk.client.protocol import preview_first
22
+ from soothe_sdk.wire.protocol import preview_first
23
23
 
24
24
  app = typer.Typer(help="Autopilot mode — long-running autonomous agent control.")
25
25
 
@@ -10,7 +10,7 @@ from typing import Annotated, Any
10
10
  import typer
11
11
  from rich.console import Console
12
12
  from rich.panel import Panel
13
- from soothe_sdk.client import (
13
+ from soothe_client import (
14
14
  WebSocketClient,
15
15
  request_daemon_config_reload,
16
16
  websocket_url_from_config,
@@ -21,7 +21,7 @@ import typer
21
21
  from rich.console import Console
22
22
  from rich.panel import Panel
23
23
  from rich.table import Table
24
- from soothe_sdk.client import (
24
+ from soothe_client import (
25
25
  is_daemon_live,
26
26
  websocket_url_from_config,
27
27
  ws_command_client_from_config,
@@ -17,12 +17,14 @@ from rich.console import Console
17
17
  from rich.panel import Panel
18
18
  from rich.prompt import Prompt
19
19
  from rich.table import Table
20
- from soothe_sdk.client import (
21
- ProtocolError,
20
+ from soothe_client import (
22
21
  WebSocketClient,
23
22
  is_daemon_live,
24
23
  websocket_url_from_config,
25
24
  )
25
+ from soothe_sdk.client import (
26
+ ProtocolError,
27
+ )
26
28
 
27
29
  from soothe_cli.runtime import load_config
28
30
 
@@ -79,13 +81,23 @@ async def _rpc(
79
81
  client = WebSocketClient(url=ws_url)
80
82
  try:
81
83
  await client.connect()
84
+ await asyncio.wait_for(client.request_connection_init(), timeout=timeout)
85
+ await asyncio.wait_for(
86
+ client.wait_for_connection_ack(ack_timeout_s=timeout), timeout=timeout
87
+ )
82
88
  if mode == "notify":
83
89
  await asyncio.wait_for(client.notify(method, params or {}), timeout=timeout)
84
90
  return {}
85
91
  if mode == "subscribe":
86
- sub_id = await asyncio.wait_for(client.subscribe(method, params or {}), timeout=timeout)
92
+ sub_id = await asyncio.wait_for(
93
+ client.subscribe(method, params or {}, timeout=timeout),
94
+ timeout=timeout,
95
+ )
87
96
  return {"subscription_id": sub_id}
88
- result = await asyncio.wait_for(client.request(method, params or {}), timeout=timeout)
97
+ result = await asyncio.wait_for(
98
+ client.request(method, params or {}, timeout=timeout),
99
+ timeout=timeout,
100
+ )
89
101
  return result if isinstance(result, dict) else {"result": result}
90
102
  except TimeoutError:
91
103
  return {"error": "Timed out waiting for daemon response"}
@@ -4,10 +4,9 @@ import logging
4
4
  import sys
5
5
  import time
6
6
  from pathlib import Path
7
- from typing import Any
8
7
 
9
8
  import typer
10
- from soothe_sdk.client.config import SOOTHE_HOME
9
+ from soothe_sdk.paths import SOOTHE_HOME
11
10
  from soothe_sdk.utils.logging import resolve_cli_log_level
12
11
 
13
12
  from soothe_cli.cli.execution import run_headless, run_tui
@@ -37,7 +36,8 @@ def run_impl(
37
36
  autonomous: Enable autonomous iteration mode
38
37
  max_iterations: Max iterations for autonomous mode
39
38
  tui_with_prompt: When True with a prompt, open the TUI instead of headless.
40
- mcp_config: Path to additional MCP server config (JSON/YAML) to merge.
39
+ mcp_config: Deprecated. MCP servers must be configured on the daemon;
40
+ passing this flag only emits a warning.
41
41
  """
42
42
  startup_start = time.perf_counter()
43
43
  use_headless: bool | None = None # Track execution mode for exit tip
@@ -45,9 +45,13 @@ def run_impl(
45
45
  try:
46
46
  cfg = load_config()
47
47
 
48
- # Merge --mcp-config file into SootheConfig.mcp_servers
48
+ # MCP servers are owned by the daemon config; CLI cannot merge them post-split.
49
49
  if mcp_config:
50
- _merge_mcp_config(cfg, mcp_config)
50
+ typer.echo(
51
+ "--mcp-config is ignored in the CLI; configure MCP servers on the daemon "
52
+ f"(~/.soothe/config). Requested file: {mcp_config}",
53
+ err=True,
54
+ )
51
55
  log_level = resolve_cli_log_level(logging_level=cfg.logging_level)
52
56
  log_file = Path(SOOTHE_HOME) / "logs" / "cli.log"
53
57
  setup_logging(log_level, log_file=log_file)
@@ -123,60 +127,3 @@ def run_impl(
123
127
 
124
128
  typer.echo(f"Error: {format_cli_error(e)}", err=True)
125
129
  sys.exit(1)
126
-
127
-
128
- def _merge_mcp_config(cfg: Any, mcp_config_path: str) -> None:
129
- """Load MCP server config from a JSON/YAML file and merge into SootheConfig.
130
-
131
- Args:
132
- cfg: SootheConfig instance.
133
- mcp_config_path: Path to MCP config file (JSON or YAML).
134
- """
135
- import json
136
-
137
- path = Path(mcp_config_path).expanduser().resolve()
138
- if not path.is_file():
139
- typer.echo(f"--mcp-config: file not found: {path}", err=True)
140
- return
141
-
142
- try:
143
- raw = path.read_text(encoding="utf-8")
144
- if path.suffix in (".yml", ".yaml"):
145
- try:
146
- import yaml
147
-
148
- data = yaml.safe_load(raw)
149
- except ImportError:
150
- typer.echo("--mcp-config: PyYAML not installed; use JSON format", err=True)
151
- return
152
- else:
153
- data = json.loads(raw)
154
- except Exception as e:
155
- typer.echo(f"--mcp-config: failed to parse {path}: {e}", err=True)
156
- return
157
-
158
- servers = data.get("mcp_servers") or data.get("servers") or []
159
- if not isinstance(servers, list):
160
- typer.echo("--mcp-config: expected 'mcp_servers' or 'servers' list", err=True)
161
- return
162
-
163
- from soothe.config.models import MCPServerConfig
164
-
165
- new_servers: list[MCPServerConfig] = []
166
- for entry in servers:
167
- if isinstance(entry, dict):
168
- try:
169
- new_servers.append(MCPServerConfig(**entry))
170
- except Exception as e:
171
- logger.warning("Skipping invalid MCP server entry: %s", e)
172
-
173
- if new_servers:
174
- existing = list(cfg.mcp_servers) if cfg.mcp_servers else []
175
- existing_names = {s.name for s in existing}
176
- for s in new_servers:
177
- if s.name in existing_names:
178
- logger.warning("--mcp-config: server '%s' already in config, skipping", s.name)
179
- else:
180
- existing.append(s)
181
- cfg.mcp_servers = existing
182
- logger.info("--mcp-config: merged %d MCP server(s)", len(new_servers))
@@ -15,7 +15,7 @@ import typer
15
15
  from rich.console import Console
16
16
  from rich.panel import Panel
17
17
  from rich.table import Table
18
- from soothe_sdk.client import (
18
+ from soothe_client import (
19
19
  WebSocketClient,
20
20
  check_daemon_status,
21
21
  is_daemon_live,
@@ -12,13 +12,14 @@ import signal
12
12
  from typing import Any
13
13
 
14
14
  import typer
15
- from soothe_sdk.client import (
15
+ from soothe_client import (
16
16
  WebSocketClient,
17
17
  async_ws_command_client_from_config,
18
18
  bootstrap_loop_session,
19
19
  connect_websocket_with_retries,
20
20
  websocket_url_from_config,
21
21
  )
22
+ from soothe_client.appkit import is_loop_scoped_event, unwrap_next
22
23
 
23
24
  from soothe_cli.cli.execution.daemon_errors import (
24
25
  friendly_daemon_execution_error,
@@ -39,46 +40,6 @@ _HEADLESS_WORKER_LOST_RETRIES = 1
39
40
  _CANCEL_SEND_TIMEOUT_S = 3.0
40
41
 
41
42
 
42
- def _unwrap_next(event: dict[str, Any] | None) -> dict[str, Any] | None:
43
- """Unwrap a protocol-1 ``next`` envelope to its inner streaming frame.
44
-
45
- Under protocol-1 (RFC-450 §9.3) the daemon wraps free-form streaming
46
- frames (``event``/``command_response``/card replay) in a
47
- ``{proto, type:"next", payload:{namespace, mode, data}}`` envelope. This
48
- helper returns the inner ``data`` dict (the legacy frame) so the headless
49
- loop and EventProcessor can branch on the same fields as before.
50
- ``status``/``error``/``response``/``complete`` are sent raw and pass
51
- through unchanged.
52
- """
53
- if not isinstance(event, dict):
54
- return event
55
- if event.get("type") != "next":
56
- return event
57
- payload = event.get("payload")
58
- if not isinstance(payload, dict):
59
- return event
60
- data = payload.get("data")
61
- return data if isinstance(data, dict) else event
62
-
63
-
64
- def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
65
- """Return whether a daemon frame belongs to the active StrangeLoop session.
66
-
67
- Unwraps protocol-1 ``next`` envelopes first, then checks ``loop_id`` on the
68
- inner streaming frame. Non-scoped types (``response``, ``error``,
69
- ``complete``, etc.) are always considered in-scope.
70
- """
71
- event_type = event.get("type", "")
72
- if event_type == "next":
73
- inner = _unwrap_next(event)
74
- if isinstance(inner, dict):
75
- event_type = inner.get("type", "")
76
- return event_type not in {"status", "event"} or (inner.get("loop_id") == active_loop_id)
77
- if event_type not in {"status", "event"}:
78
- return True
79
- return event.get("loop_id") == active_loop_id
80
-
81
-
82
43
  def _emit_headless_error(message: str) -> None:
83
44
  """Write a user-facing error line to stderr (headless renderer convention)."""
84
45
  typer.echo(f"ERROR: {message}", err=True)
@@ -120,8 +81,6 @@ async def _run_headless_session_once(
120
81
  max_iterations: int | None = None,
121
82
  ) -> tuple[int, bool]:
122
83
  """Run one headless daemon session; return ``(exit_code, retry_on_worker_loss)``."""
123
- from soothe_sdk.client import WebSocketClient
124
-
125
84
  ws_url = websocket_url_from_config(cfg)
126
85
  client = WebSocketClient(url=ws_url)
127
86
 
@@ -248,13 +207,13 @@ async def _run_headless_session_once(
248
207
  break
249
208
 
250
209
  event_type = event.get("type", "")
251
- if not _is_loop_scoped_event(event, active_loop_id=active_loop_id):
210
+ if not is_loop_scoped_event(event, active_loop_id=active_loop_id):
252
211
  continue
253
212
 
254
213
  # Unwrap protocol-1 ``next`` envelopes to the inner streaming frame
255
214
  # (RFC-450 §9.3). ``status``/``error`` arrive raw and pass through.
256
215
  if event_type == "next":
257
- inner = _unwrap_next(event)
216
+ inner = unwrap_next(event)
258
217
  if isinstance(inner, dict):
259
218
  event = inner
260
219
  event_type = event.get("type", "")
@@ -288,12 +247,12 @@ async def _run_headless_session_once(
288
247
  break
289
248
  if not nxt:
290
249
  break
291
- if not _is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
250
+ if not is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
292
251
  continue
293
252
  # Unwrap ``next`` envelopes before handing to the
294
253
  # processor so it sees the legacy frame shape.
295
254
  if nxt.get("type") == "next":
296
- inner = _unwrap_next(nxt)
255
+ inner = unwrap_next(nxt)
297
256
  if isinstance(inner, dict):
298
257
  nxt = inner
299
258
  processor.process_event(nxt)
@@ -4,7 +4,7 @@ import asyncio
4
4
  import sys
5
5
 
6
6
  import typer
7
- from soothe_sdk.client import (
7
+ from soothe_client import (
8
8
  WebSocketClient,
9
9
  is_daemon_live,
10
10
  request_daemon_shutdown,
@@ -10,7 +10,7 @@ from pathlib import Path # noqa: E402
10
10
  from typing import Annotated # noqa: E402
11
11
 
12
12
  import typer # noqa: E402
13
- from soothe_sdk.client.config import SOOTHE_HOME # noqa: E402
13
+ from soothe_sdk.paths import SOOTHE_HOME # noqa: E402
14
14
 
15
15
  from soothe_cli.config.cli_config import CLIConfig # noqa: E402
16
16
  from soothe_cli.config.loader import set_runtime_config # noqa: E402
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass, field
6
6
  from pathlib import Path
7
7
 
8
- from soothe_sdk.client.config import SOOTHE_HOME
8
+ from soothe_sdk.paths import SOOTHE_HOME
9
9
 
10
10
 
11
11
  @dataclass
@@ -35,7 +35,7 @@ from soothe_cli.runtime.policy.tui_trace_log import log_tui_trace
35
35
  from soothe_cli.runtime.presentation.engine import PresentationEngine
36
36
 
37
37
  if TYPE_CHECKING:
38
- from soothe_sdk.client.schemas import Plan
38
+ from soothe_client.schemas import Plan
39
39
 
40
40
  from soothe_cli.runtime.presentation.renderer_protocol import RendererProtocol
41
41
 
@@ -962,7 +962,7 @@ class EventProcessor:
962
962
 
963
963
  def _handle_plan_created(self, data: dict[str, Any]) -> None:
964
964
  """Handle plan creation event."""
965
- from soothe_sdk.client.schemas import Plan, PlanStep
965
+ from soothe_client.schemas import Plan, PlanStep
966
966
 
967
967
  steps = [
968
968
  PlanStep(
@@ -11,7 +11,7 @@ from dataclasses import dataclass, field
11
11
  from typing import TYPE_CHECKING, Any
12
12
 
13
13
  if TYPE_CHECKING:
14
- from soothe_sdk.client.schemas import Plan
14
+ from soothe_client.schemas import Plan
15
15
 
16
16
  from soothe_cli.runtime.state.stream_accumulator import StreamingTextAccumulator
17
17
 
@@ -5,9 +5,11 @@ from __future__ import annotations
5
5
  from soothe_sdk.display.tool_result import (
6
6
  extract_tool_result_payload,
7
7
  infer_tool_output_suggests_error,
8
+ is_error_tool_result_text,
8
9
  )
9
10
 
10
11
  __all__ = [
11
12
  "extract_tool_result_payload",
12
13
  "infer_tool_output_suggests_error",
14
+ "is_error_tool_result_text",
13
15
  ]
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  from typing import TYPE_CHECKING, Any, Protocol
10
10
 
11
11
  if TYPE_CHECKING:
12
- from soothe_sdk.client.schemas import Plan
12
+ from soothe_client.schemas import Plan
13
13
 
14
14
  TaskScope = tuple[str, str]
15
15
 
@@ -24,6 +24,7 @@ from soothe_sdk.ux.task_namespace import (
24
24
  resolve_task_parent_lookup,
25
25
  resolve_task_scope_for_namespace,
26
26
  row_key_for_subgraph_tool,
27
+ step_level_parent_task_call_id,
27
28
  task_scope_step_id,
28
29
  try_bind_namespace_from_tool_call_id,
29
30
  )
@@ -93,6 +94,10 @@ class StepTaskRouter:
93
94
  _spawns_by_task_id: dict[str, TaskScope] = field(default_factory=dict, repr=False)
94
95
  _pending_unscoped_namespaces: deque[tuple[str, ...]] = field(default_factory=deque, repr=False)
95
96
  _spawn_recorded: set[tuple[str, str]] = field(default_factory=set, repr=False)
97
+ _task_index_by_step_and_call: dict[tuple[str, str], int] = field(
98
+ default_factory=dict, repr=False
99
+ )
100
+ _next_task_index_by_step: dict[str, int] = field(default_factory=dict, repr=False)
96
101
  _pending_main_tools: dict[str, PendingMainTool] = field(default_factory=dict, repr=False)
97
102
  _pending_subgraph_tools: dict[tuple[tuple[str, ...], str], PendingSubgraphTool] = field(
98
103
  default_factory=dict,
@@ -108,6 +113,8 @@ class StepTaskRouter:
108
113
  self._spawns_by_task_id.clear()
109
114
  self._pending_unscoped_namespaces.clear()
110
115
  self._spawn_recorded.clear()
116
+ self._task_index_by_step_and_call.clear()
117
+ self._next_task_index_by_step.clear()
111
118
  self._pending_main_tools.clear()
112
119
  self._pending_subgraph_tools.clear()
113
120
 
@@ -220,6 +227,11 @@ class StepTaskRouter:
220
227
  ]
221
228
  for tcid in drop_tcid:
222
229
  self._spawns_by_task_id.pop(tcid, None)
230
+ # Cleanse per-step synthetic task-index state once the step is done.
231
+ self._next_task_index_by_step.pop(sid, None)
232
+ stale_index_keys = [key for key in self._task_index_by_step_and_call if key[0] == sid]
233
+ for key in stale_index_keys:
234
+ self._task_index_by_step_and_call.pop(key, None)
223
235
 
224
236
  def step_id_for_tool(self, tool_call_id: str) -> str:
225
237
  """Return execute step id encoded in a unified root tool_call_id."""
@@ -287,7 +299,10 @@ class StepTaskRouter:
287
299
  sid = str(step_id).strip()
288
300
  if not sid:
289
301
  return False
290
- normalized_tcid = normalize_main_task_delegation_id(sid, tcid, tool_name="task")
302
+ normalized_tcid = self.normalize_task_delegation_id(
303
+ step_id=sid,
304
+ tool_call_id=tcid,
305
+ )
291
306
  spawn_key = (sid, normalized_tcid)
292
307
  if spawn_key in self._spawn_recorded:
293
308
  return False
@@ -311,6 +326,42 @@ class StepTaskRouter:
311
326
  )
312
327
  return True
313
328
 
329
+ def normalize_task_delegation_id(self, *, step_id: str, tool_call_id: str) -> str:
330
+ """Return canonical ``{step}:s:task:{idx}`` for main ``task`` delegations.
331
+
332
+ Some providers emit unified ids like ``{step}:s:call_<uuid>`` that do not
333
+ encode a task index. In that case allocate a stable per-step synthetic
334
+ index so task activity rows and standalone SubAgent cards stay 1:1.
335
+ """
336
+ sid = str(step_id).strip()
337
+ tcid = str(tool_call_id).strip()
338
+ if not sid or not tcid:
339
+ return tcid
340
+
341
+ normalized = normalize_main_task_delegation_id(sid, tcid, tool_name="task")
342
+ _, type_code, _, tool_info = parse_unified_tool_call_id(normalized)
343
+ if type_code == "s" and isinstance(tool_info, str):
344
+ parts = tool_info.split(":")
345
+ if parts and parts[0] == "task":
346
+ tail = parts[-1] if parts else ""
347
+ if tail.isdigit():
348
+ idx = int(tail)
349
+ key = (sid, tcid)
350
+ self._task_index_by_step_and_call[key] = idx
351
+ next_idx = self._next_task_index_by_step.get(sid, 0)
352
+ if idx >= next_idx:
353
+ self._next_task_index_by_step[sid] = idx + 1
354
+ return normalized
355
+
356
+ key = (sid, tcid)
357
+ if key in self._task_index_by_step_and_call:
358
+ idx = self._task_index_by_step_and_call[key]
359
+ else:
360
+ idx = self._next_task_index_by_step.get(sid, 0)
361
+ self._task_index_by_step_and_call[key] = idx
362
+ self._next_task_index_by_step[sid] = idx + 1
363
+ return step_level_parent_task_call_id(sid, idx)
364
+
314
365
  def resolve_task_scope(self, namespace: tuple[str, ...]) -> TaskScope | None:
315
366
  """Task scope for a stream namespace, if bound."""
316
367
  return resolve_task_scope_for_namespace(self._namespace_bindings, namespace)
@@ -0,0 +1,80 @@
1
+ """Daemon-backed session helpers for the Textual TUI.
2
+
3
+ Thin CLI wrapper over ``soothe_client.appkit.DaemonSession`` that derives
4
+ WebSocket URL / stream delivery from CLI config and uses the richer CLI early
5
+ chunk filter (LangChain-aware).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from soothe_client.appkit import DaemonSession
13
+ from soothe_client.helpers import websocket_url_from_config
14
+
15
+ # Re-exported for test monkeypatches / historical import sites.
16
+ from soothe_client.session import ( # noqa: F401
17
+ bootstrap_loop_session,
18
+ connect_websocket_with_retries,
19
+ )
20
+
21
+ from soothe_cli.runtime.state.session_stats import TurnEventStats
22
+ from soothe_cli.runtime.wire.chunk_filter import should_drop_stream_chunk_early
23
+
24
+ # Match headless daemon client post-idle drain window.
25
+ _POST_IDLE_DRAIN_DEADLINE_S = 0.5
26
+
27
+ # Brief close handshake on TUI exit — the daemon cleans up on disconnect anyway.
28
+ TUI_EXIT_HANDSHAKE_TIMEOUT_S = 0.3
29
+
30
+
31
+ class TuiDaemonSession(DaemonSession):
32
+ """Own the daemon websocket session used by the TUI."""
33
+
34
+ def __init__(
35
+ self,
36
+ cfg: Any,
37
+ *,
38
+ workspace: str | None = None,
39
+ post_idle_drain_deadline: float = _POST_IDLE_DRAIN_DEADLINE_S,
40
+ ) -> None:
41
+ self._cfg = cfg
42
+ super().__init__(
43
+ websocket_url_from_config(cfg),
44
+ workspace=workspace,
45
+ stream_delivery=self._resolve_stream_delivery_mode,
46
+ post_idle_drain_deadline=post_idle_drain_deadline,
47
+ early_drop_fn=should_drop_stream_chunk_early,
48
+ stats_factory=TurnEventStats,
49
+ )
50
+
51
+ def _resolve_stream_delivery_mode(self) -> str:
52
+ """Determine stream delivery mode from config (RFC-614).
53
+
54
+ Returns one of ``batch`` | ``adaptive`` | ``streaming``. CLI override
55
+ wins, then config; defaults to ``adaptive``.
56
+ """
57
+ if (
58
+ self._cfg
59
+ and hasattr(self._cfg, "output_streaming_mode")
60
+ and self._cfg.output_streaming_mode
61
+ ):
62
+ return str(self._cfg.output_streaming_mode)
63
+
64
+ if self._cfg and hasattr(self._cfg, "agent"):
65
+ streaming_cfg = self._cfg.agent.loop.output_streaming
66
+ return str(streaming_cfg.mode)
67
+
68
+ return "adaptive"
69
+
70
+
71
+ # Historical alias used by some call sites / docs.
72
+ DaemonSession = TuiDaemonSession # noqa: F811
73
+
74
+ __all__ = [
75
+ "DaemonSession",
76
+ "TUI_EXIT_HANDSHAKE_TIMEOUT_S",
77
+ "TuiDaemonSession",
78
+ "bootstrap_loop_session",
79
+ "connect_websocket_with_retries",
80
+ ]
@@ -0,0 +1,35 @@
1
+ """Decouple daemon stream ingestion from chunk processing and UI application.
2
+
3
+ Canonical implementation lives in ``soothe_client.appkit.turn`` (RFC-629 Layer 1).
4
+ This module re-exports the public API for CLI import stability.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from soothe_client.appkit.turn import (
10
+ _DEFAULT_BATCH_DELAY_MS,
11
+ _DEFAULT_BATCH_SIZE,
12
+ _DEFAULT_INBOUND_MAXSIZE,
13
+ _DEFAULT_OUTBOUND_MAXSIZE,
14
+ PRIORITY_CRITICAL,
15
+ PRIORITY_HIGH,
16
+ PRIORITY_LOW,
17
+ PRIORITY_NORMAL,
18
+ TurnApplyBatcher,
19
+ TurnEventPipeline,
20
+ run_turn_pipeline,
21
+ )
22
+
23
+ __all__ = [
24
+ "PRIORITY_CRITICAL",
25
+ "PRIORITY_HIGH",
26
+ "PRIORITY_LOW",
27
+ "PRIORITY_NORMAL",
28
+ "TurnApplyBatcher",
29
+ "TurnEventPipeline",
30
+ "run_turn_pipeline",
31
+ "_DEFAULT_BATCH_SIZE",
32
+ "_DEFAULT_BATCH_DELAY_MS",
33
+ "_DEFAULT_INBOUND_MAXSIZE",
34
+ "_DEFAULT_OUTBOUND_MAXSIZE",
35
+ ]
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
- from soothe_sdk.client.wire import flatten_enveloped_message_dict
7
+ from soothe_sdk.wire.codec import flatten_enveloped_message_dict
8
8
 
9
9
 
10
10
  def wire_message_body(msg: Any) -> Any: