soothe-cli 0.8.0__tar.gz → 0.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/PKG-INFO +2 -1
  2. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/pyproject.toml +2 -1
  3. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/autopilot_cmd.py +37 -6
  4. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/config_cmd.py +4 -7
  5. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/cron_cmd.py +3 -3
  6. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/loop_cmd.py +4 -61
  7. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/run_cmd.py +9 -62
  8. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/status_cmd.py +4 -7
  9. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/daemon.py +42 -79
  10. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/headless.py +4 -6
  11. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/main.py +1 -1
  12. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/config/cli_config.py +1 -1
  13. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/headless/processor.py +2 -2
  14. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/headless/processor_state.py +1 -1
  15. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/tool_result.py +2 -0
  16. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/renderer_protocol.py +1 -1
  17. soothe_cli-0.8.2/src/soothe_cli/runtime/transport/session.py +68 -0
  18. soothe_cli-0.8.2/src/soothe_cli/runtime/turn/pipeline.py +35 -0
  19. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/message_text.py +1 -1
  20. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_model.py +7 -6
  21. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_startup.py +8 -3
  22. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/command_router.py +16 -3
  23. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/config.py +1 -1
  24. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/model_config.py +16 -17
  25. soothe_cli-0.8.2/src/soothe_cli/tui/skills/invocation.py +70 -0
  26. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/theme.py +1 -1
  27. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/tool_display.py +1 -1
  28. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/update_check.py +1 -1
  29. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/chat_input.py +1 -1
  30. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/context_data.py +38 -31
  31. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/context_viewer.py +7 -4
  32. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +1 -1
  33. soothe_cli-0.8.0/src/soothe_cli/runtime/transport/session.py +0 -773
  34. soothe_cli-0.8.0/src/soothe_cli/runtime/turn/pipeline.py +0 -404
  35. soothe_cli-0.8.0/src/soothe_cli/tui/skills/invocation.py +0 -61
  36. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/.gitignore +0 -0
  37. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/README.md +0 -0
  38. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/__init__.py +0 -0
  39. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/__init__.py +0 -0
  40. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/__init__.py +0 -0
  41. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/__init__.py +0 -0
  42. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  43. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  44. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/launcher.py +0 -0
  45. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/config/__init__.py +0 -0
  46. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/config/loader.py +0 -0
  47. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/config/logging_setup.py +0 -0
  48. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/__init__.py +0 -0
  49. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  50. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  51. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  52. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  53. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  54. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  55. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  56. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
  57. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  58. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
  59. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  60. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  61. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/step_router.py +0 -0
  62. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  63. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/transcript.py +0 -0
  64. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/token_events_debug.py +0 -0
  65. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/token_usage.py +0 -0
  66. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  67. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  68. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/messages.py +0 -0
  69. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/__init__.py +0 -0
  70. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/_cli_context.py +0 -0
  71. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/_env_vars.py +0 -0
  72. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/_version.py +0 -0
  73. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/__init__.py +0 -0
  74. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_app.py +0 -0
  75. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_execution.py +0 -0
  76. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_history.py +0 -0
  77. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  78. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_module_init.py +0 -0
  79. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_ui.py +0 -0
  80. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/app.tcss +0 -0
  81. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/binding.py +0 -0
  82. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/command_registry.py +0 -0
  83. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/__init__.py +0 -0
  84. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
  85. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  86. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/file_change_notify.py +0 -0
  87. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  88. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/hooks.py +0 -0
  89. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/input.py +0 -0
  90. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/markdown_theme.py +0 -0
  91. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/media_utils.py +0 -0
  92. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/path_utils.py +0 -0
  93. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/preview_limits.py +0 -0
  94. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/project_utils.py +0 -0
  95. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/sessions.py +0 -0
  96. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/shell_color.py +0 -0
  97. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/__init__.py +0 -0
  98. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/load.py +0 -0
  99. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/spinner_labels.py +0 -0
  100. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/textual_adapter.py +0 -0
  101. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/tips.py +0 -0
  102. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/unicode_security.py +0 -0
  103. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  104. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/_links.py +0 -0
  105. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  106. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  107. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/diff.py +0 -0
  108. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/editor.py +0 -0
  109. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  110. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
  111. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/history.py +0 -0
  112. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/loading.py +0 -0
  113. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  114. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  115. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  116. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  117. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
  118. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  119. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
  120. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
  121. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
  122. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
  123. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
  124. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
  125. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  126. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  127. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
  128. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  129. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  130. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  131. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
  132. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
  133. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/status.py +0 -0
  134. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  135. {soothe_cli-0.8.0 → soothe_cli-0.8.2}/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.8.0
3
+ Version: 0.8.2
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.10.0
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.10.0,<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
+ command_client_from_config,
18
19
  is_daemon_live,
19
20
  websocket_url_from_config,
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
 
@@ -46,7 +46,7 @@ def _require_daemon_ws():
46
46
  err=True,
47
47
  )
48
48
  sys.exit(1)
49
- return ws_command_client_from_config(cfg)
49
+ return command_client_from_config(cfg)
50
50
 
51
51
 
52
52
  @app.command("run")
@@ -325,10 +325,41 @@ def show_goal(
325
325
 
326
326
  @app.command("cancel")
327
327
  def cancel_goal(
328
- goal_id: str = typer.Argument(..., help="Goal ID to cancel."),
328
+ goal_id: str | None = typer.Argument(
329
+ None,
330
+ help="Goal ID to cancel (omit when using --all or --job).",
331
+ ),
332
+ cancel_all: bool = typer.Option(
333
+ False,
334
+ "--all",
335
+ help="Cancel every open (non-terminal) autopilot goal.",
336
+ ),
337
+ job_id: str | None = typer.Option(
338
+ None,
339
+ "--job",
340
+ help="Cancel a job root and all descendant goals.",
341
+ ),
329
342
  ) -> None:
330
- """Cancel a goal via the daemon."""
343
+ """Cancel a goal, a job subtree, or all open goals via the daemon."""
344
+ modes = sum(1 for flag in (bool(goal_id), cancel_all, bool(job_id)) if flag)
345
+ if modes != 1:
346
+ typer.echo(
347
+ "Specify exactly one of: GOAL_ID, --all, or --job <id>.",
348
+ err=True,
349
+ )
350
+ raise typer.Exit(1)
351
+
331
352
  client = _require_daemon_ws()
353
+ if cancel_all:
354
+ result = client.autopilot_cancel_all()
355
+ count = result.get("cancelled_count", 0)
356
+ typer.echo(f"Cancelled {count} open goal(s).")
357
+ return
358
+ if job_id:
359
+ result = client.job_cancel(job_id)
360
+ typer.echo(f"Cancel job result: {result.get('status', result)}")
361
+ return
362
+ assert goal_id is not None
332
363
  result = client.autopilot_cancel_goal(goal_id)
333
364
  typer.echo(f"Cancel result: {result.get('status', result)}")
334
365
 
@@ -10,8 +10,8 @@ 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 (
14
- WebSocketClient,
13
+ from soothe_client import (
14
+ connected_websocket,
15
15
  request_daemon_config_reload,
16
16
  websocket_url_from_config,
17
17
  )
@@ -34,14 +34,11 @@ async def _trigger_config_reload(ws_url: str, timeout: float = 5.0) -> dict[str,
34
34
  Returns:
35
35
  Response dict from daemon, or error dict on failure.
36
36
  """
37
- client = WebSocketClient(url=ws_url)
38
37
  try:
39
- await client.connect()
40
- return await request_daemon_config_reload(client, timeout=timeout)
38
+ async with connected_websocket(ws_url, timeout=timeout) as client:
39
+ return await request_daemon_config_reload(client, timeout=timeout)
41
40
  except Exception as e:
42
41
  return {"success": False, "error": str(e)}
43
- finally:
44
- await client.close()
45
42
 
46
43
 
47
44
  @config_app.command("reload")
@@ -21,10 +21,10 @@ 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
+ command_client_from_config,
25
26
  is_daemon_live,
26
27
  websocket_url_from_config,
27
- ws_command_client_from_config,
28
28
  )
29
29
 
30
30
  from soothe_cli.runtime import load_config
@@ -44,7 +44,7 @@ def _require_cron_client():
44
44
  err=True,
45
45
  )
46
46
  sys.exit(1)
47
- return ws_command_client_from_config(cfg)
47
+ return command_client_from_config(cfg)
48
48
 
49
49
 
50
50
  def _format_next_run(next_run: str) -> str:
@@ -17,10 +17,9 @@ 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,
22
- WebSocketClient,
20
+ from soothe_client import (
23
21
  is_daemon_live,
22
+ protocol1_rpc,
24
23
  websocket_url_from_config,
25
24
  )
26
25
 
@@ -47,64 +46,8 @@ async def _check_daemon(ws_url: str) -> bool:
47
46
  return await is_daemon_live(ws_url, timeout=5.0)
48
47
 
49
48
 
50
- async def _rpc(
51
- ws_url: str,
52
- method: str,
53
- params: dict[str, Any] | None = None,
54
- *,
55
- mode: str = "request",
56
- timeout: float = 30.0,
57
- ) -> dict[str, Any]:
58
- """Send a protocol-1 RPC to the daemon and return the response.
59
-
60
- Uses the protocol-1 client API (RFC-450): ``request()`` for RPC,
61
- ``notify()`` for fire-and-forget, ``subscribe()`` for event streams.
62
- The wrapper preserves the dict-based error contract used by command
63
- handlers — callers check ``if "error" in response``.
64
-
65
- Args:
66
- ws_url: WebSocket URL.
67
- method: RPC method / notification target / subscription target
68
- (e.g. ``"loop_get"``, ``"loop_input"``, ``"loop_events"``).
69
- params: Structured parameters object.
70
- mode: One of ``"request"`` (blocking RPC), ``"notify"``
71
- (fire-and-forget), or ``"subscribe"`` (start a stream).
72
- timeout: Maximum seconds to wait.
73
-
74
- Returns:
75
- Response dict from daemon, or ``{"error": ...}`` on failure. For
76
- ``notify`` mode returns ``{}``; for ``subscribe`` mode returns
77
- ``{"subscription_id": <id>}``.
78
- """
79
- client = WebSocketClient(url=ws_url)
80
- try:
81
- await client.connect()
82
- await asyncio.wait_for(client.request_connection_init(), timeout=timeout)
83
- await asyncio.wait_for(
84
- client.wait_for_connection_ack(ack_timeout_s=timeout), timeout=timeout
85
- )
86
- if mode == "notify":
87
- await asyncio.wait_for(client.notify(method, params or {}), timeout=timeout)
88
- return {}
89
- if mode == "subscribe":
90
- sub_id = await asyncio.wait_for(
91
- client.subscribe(method, params or {}, timeout=timeout),
92
- timeout=timeout,
93
- )
94
- return {"subscription_id": sub_id}
95
- result = await asyncio.wait_for(
96
- client.request(method, params or {}, timeout=timeout),
97
- timeout=timeout,
98
- )
99
- return result if isinstance(result, dict) else {"result": result}
100
- except TimeoutError:
101
- return {"error": "Timed out waiting for daemon response"}
102
- except ProtocolError as exc:
103
- return {"error": str(exc)}
104
- except (ConnectionError, OSError) as exc:
105
- return {"error": f"Connection error: {exc}"}
106
- finally:
107
- await client.close()
49
+ # Back-compat alias for tests and internal callers.
50
+ _rpc = protocol1_rpc
108
51
 
109
52
 
110
53
  def _resolve_continue_loop_id(ws_url: str, loop_id: str | None) -> str:
@@ -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,9 +15,9 @@ 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 (
19
- WebSocketClient,
18
+ from soothe_client import (
20
19
  check_daemon_status,
20
+ connected_websocket,
21
21
  is_daemon_live,
22
22
  websocket_url_from_config,
23
23
  )
@@ -40,14 +40,11 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
40
40
  Returns:
41
41
  Status dict from daemon, or error dict on failure.
42
42
  """
43
- client = WebSocketClient(url=ws_url)
44
43
  try:
45
- await client.connect()
46
- return await check_daemon_status(client, timeout=timeout)
44
+ async with connected_websocket(ws_url, timeout=timeout) as client:
45
+ return await check_daemon_status(client, timeout=timeout)
47
46
  except Exception as e:
48
47
  return {"error": str(e)}
49
- finally:
50
- await client.close()
51
48
 
52
49
 
53
50
  def _render_unified_status_table(
@@ -1,7 +1,7 @@
1
1
  """Daemon-based execution for headless mode.
2
2
 
3
- Uses RFC-0019 EventProcessor with HeadlessCliRenderer (stdout: loop-tagged answers only).
4
- Uses WebSocket transport (RFC-0013).
3
+ Uses EventProcessor with HeadlessCliRenderer (stdout: loop-tagged answers only).
4
+ Session lifecycle goes through ``soothe_client.appkit.DaemonSession``.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -12,13 +12,12 @@ import signal
12
12
  from typing import Any
13
13
 
14
14
  import typer
15
- from soothe_sdk.client import (
16
- WebSocketClient,
17
- async_ws_command_client_from_config,
18
- bootstrap_loop_session,
19
- connect_websocket_with_retries,
15
+ from soothe_client import (
16
+ async_command_client_from_config,
20
17
  websocket_url_from_config,
21
18
  )
19
+ from soothe_client.appkit import DaemonSession
20
+ from soothe_client.appkit.events import is_loop_scoped_event, unwrap_next
22
21
 
23
22
  from soothe_cli.cli.execution.daemon_errors import (
24
23
  friendly_daemon_execution_error,
@@ -39,56 +38,16 @@ _HEADLESS_WORKER_LOST_RETRIES = 1
39
38
  _CANCEL_SEND_TIMEOUT_S = 3.0
40
39
 
41
40
 
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
41
  def _emit_headless_error(message: str) -> None:
83
42
  """Write a user-facing error line to stderr (headless renderer convention)."""
84
43
  typer.echo(f"ERROR: {message}", err=True)
85
44
 
86
45
 
87
- async def _send_cancel_to_daemon(client: WebSocketClient) -> None:
46
+ async def _send_cancel_to_daemon(session: DaemonSession) -> None:
88
47
  """Send /cancel to the daemon with a short timeout."""
89
48
  try:
90
49
  await asyncio.wait_for(
91
- client.notify("slash_command", {"cmd": "/cancel"}),
50
+ session.cancel_remote_query(),
92
51
  timeout=_CANCEL_SEND_TIMEOUT_S,
93
52
  )
94
53
  except Exception:
@@ -120,16 +79,12 @@ async def _run_headless_session_once(
120
79
  max_iterations: int | None = None,
121
80
  ) -> tuple[int, bool]:
122
81
  """Run one headless daemon session; return ``(exit_code, retry_on_worker_loss)``."""
123
- from soothe_sdk.client import WebSocketClient
124
-
125
- ws_url = websocket_url_from_config(cfg)
126
- client = WebSocketClient(url=ws_url)
127
-
128
82
  # Track whether the daemon was notified of cancellation.
129
83
  cancel_sent = False
130
84
  sigint_received = False
131
85
  sigint_count = 0
132
86
  original_sigint: Any = None
87
+ session: DaemonSession | None = None
133
88
 
134
89
  cron_text = _parse_cron_slash_prompt(prompt)
135
90
  if cron_text is not None:
@@ -137,7 +92,7 @@ async def _run_headless_session_once(
137
92
  _emit_headless_error("Usage: /cron <natural language schedule>")
138
93
  return 1, False
139
94
  try:
140
- ws_client = async_ws_command_client_from_config(cfg)
95
+ ws_client = async_command_client_from_config(cfg)
141
96
  result = await ws_client.cron_add(cron_text)
142
97
  except RuntimeError as exc:
143
98
  _emit_headless_error(str(exc))
@@ -150,27 +105,34 @@ async def _run_headless_session_once(
150
105
  return 0, False
151
106
 
152
107
  try:
153
- await connect_websocket_with_retries(client)
154
108
  cli_ws = resolve_cli_loop_workspace()
155
- # IG-441: three first-class modes (batch / adaptive / streaming).
109
+ # Three first-class modes (batch / adaptive / streaming).
156
110
  # Default is ``adaptive`` for headless runs as well — it gives smooth
157
111
  # progress on long synthesis while keeping wire traffic bounded.
158
112
  override = getattr(cfg, "output_streaming_mode", None)
159
113
  stream_delivery = override if override in ("batch", "adaptive", "streaming") else "adaptive"
160
114
 
161
- status_event = await bootstrap_loop_session(
162
- client,
163
- resume_loop_id=resume_loop_id,
164
- stream_delivery=stream_delivery,
115
+ session = DaemonSession(
116
+ websocket_url_from_config(cfg),
165
117
  workspace=cli_ws,
166
- subscribe_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
118
+ stream_delivery=stream_delivery,
167
119
  )
120
+ try:
121
+ status_event = await asyncio.wait_for(
122
+ session.connect(resume_loop_id=resume_loop_id),
123
+ timeout=_SESSION_BOOTSTRAP_TIMEOUT_S,
124
+ )
125
+ except RuntimeError as exc:
126
+ raw = str(exc)
127
+ _emit_headless_error(friendly_daemon_execution_error(raw))
128
+ return 1, is_daemon_worker_subprocess_lost(raw)
129
+
168
130
  if status_event.get("type") == "error":
169
131
  raw = str(status_event.get("message", "unknown"))
170
132
  _emit_headless_error(friendly_daemon_execution_error(raw))
171
133
  return 1, is_daemon_worker_subprocess_lost(raw)
172
134
 
173
- active_loop_id = status_event.get("loop_id")
135
+ active_loop_id = session.loop_id or status_event.get("loop_id")
174
136
  if not active_loop_id:
175
137
  _emit_headless_error("No loop_id after session bootstrap")
176
138
  return 1, False
@@ -179,8 +141,7 @@ async def _run_headless_session_once(
179
141
  effective_prompt = cleaned_prompt if subagent_name else prompt
180
142
 
181
143
  await asyncio.wait_for(
182
- client.send_input(
183
- active_loop_id,
144
+ session.send_turn(
184
145
  effective_prompt,
185
146
  autonomous=autonomous,
186
147
  max_iterations=max_iterations,
@@ -223,13 +184,14 @@ async def _run_headless_session_once(
223
184
  )
224
185
 
225
186
  query_started = False
187
+ client = session.client
226
188
 
227
189
  while True:
228
190
  # Check if SIGINT fired and send /cancel to the daemon.
229
191
  if sigint_received and not cancel_sent:
230
192
  cancel_sent = True
231
193
  logger.info("Headless query interrupted; sending /cancel to daemon")
232
- await _send_cancel_to_daemon(client)
194
+ await _send_cancel_to_daemon(session)
233
195
  # After notifying the daemon, cancel the main task so
234
196
  # asyncio.run() can unwind cleanly.
235
197
  if main_task is not None and not main_task.done():
@@ -248,13 +210,13 @@ async def _run_headless_session_once(
248
210
  break
249
211
 
250
212
  event_type = event.get("type", "")
251
- if not _is_loop_scoped_event(event, active_loop_id=active_loop_id):
213
+ if not is_loop_scoped_event(event, active_loop_id=active_loop_id):
252
214
  continue
253
215
 
254
- # Unwrap protocol-1 ``next`` envelopes to the inner streaming frame
255
- # (RFC-450 §9.3). ``status``/``error`` arrive raw and pass through.
216
+ # Unwrap protocol-1 ``next`` envelopes to the inner streaming frame.
217
+ # ``status``/``error`` arrive raw and pass through.
256
218
  if event_type == "next":
257
- inner = _unwrap_next(event)
219
+ inner = unwrap_next(event)
258
220
  if isinstance(inner, dict):
259
221
  event = inner
260
222
  event_type = event.get("type", "")
@@ -288,12 +250,12 @@ async def _run_headless_session_once(
288
250
  break
289
251
  if not nxt:
290
252
  break
291
- if not _is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
253
+ if not is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
292
254
  continue
293
255
  # Unwrap ``next`` envelopes before handing to the
294
256
  # processor so it sees the legacy frame shape.
295
257
  if nxt.get("type") == "next":
296
- inner = _unwrap_next(nxt)
258
+ inner = unwrap_next(nxt)
297
259
  if isinstance(inner, dict):
298
260
  nxt = inner
299
261
  processor.process_event(nxt)
@@ -304,18 +266,18 @@ async def _run_headless_session_once(
304
266
  processor.process_event(event)
305
267
 
306
268
  except KeyboardInterrupt:
307
- if not cancel_sent:
269
+ if session is not None and not cancel_sent:
308
270
  cancel_sent = True
309
271
  logger.info("Headless query interrupted by user; sending /cancel to daemon")
310
- await _send_cancel_to_daemon(client)
272
+ await _send_cancel_to_daemon(session)
311
273
  return 1, False
312
274
  except asyncio.CancelledError:
313
- if not cancel_sent:
275
+ if session is not None and not cancel_sent:
314
276
  cancel_sent = True
315
277
  logger.info("Headless query cancelled; sending /cancel to daemon")
316
278
  # Best-effort: the task is being cancelled so awaiting may fail.
317
279
  try:
318
- await asyncio.shield(_send_cancel_to_daemon(client))
280
+ await asyncio.shield(_send_cancel_to_daemon(session))
319
281
  except (asyncio.CancelledError, Exception):
320
282
  pass
321
283
  raise
@@ -341,7 +303,8 @@ async def _run_headless_session_once(
341
303
  signal.signal(signal.SIGINT, original_sigint)
342
304
  except (ValueError, OSError, RuntimeError):
343
305
  pass
344
- await client.close()
306
+ if session is not None:
307
+ await session.close()
345
308
 
346
309
 
347
310
  async def run_headless_via_daemon(
@@ -354,8 +317,8 @@ async def run_headless_via_daemon(
354
317
  ) -> int:
355
318
  """Run a single prompt by connecting to a running daemon.
356
319
 
357
- Uses WebSocket transport for all connections (RFC-0013).
358
- Headless output is RFC-614 loop-tagged main-graph assistant text only (IG-343).
320
+ Uses WebSocket transport for all connections.
321
+ Headless output is loop-tagged main-graph assistant text only.
359
322
 
360
323
  Retries once when the worker pool loses a subprocess mid-query (common idle-timeout
361
324
  race or transient worker recycle).
@@ -4,8 +4,8 @@ import asyncio
4
4
  import sys
5
5
 
6
6
  import typer
7
- from soothe_sdk.client import (
8
- WebSocketClient,
7
+ from soothe_client import (
8
+ connected_websocket,
9
9
  is_daemon_live,
10
10
  request_daemon_shutdown,
11
11
  websocket_url_from_config,
@@ -49,10 +49,8 @@ def run_headless(
49
49
  if not daemon_live:
50
50
  # Attempt cleanup if stale daemon (connection exists but daemon not responsive)
51
51
  try:
52
- client = WebSocketClient(url=ws_url)
53
- await client.connect()
54
- await request_daemon_shutdown(client, timeout=10.0)
55
- await client.close()
52
+ async with connected_websocket(ws_url, timeout=10.0) as client:
53
+ await request_daemon_shutdown(client, timeout=10.0)
56
54
  except Exception:
57
55
  pass # No daemon running or already stopped
58
56
 
@@ -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(