soothe-cli 0.8.1__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 (132) hide show
  1. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/PKG-INFO +2 -2
  2. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/pyproject.toml +1 -1
  3. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/autopilot_cmd.py +35 -4
  4. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/config_cmd.py +3 -6
  5. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/cron_cmd.py +2 -2
  6. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/loop_cmd.py +3 -62
  7. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/status_cmd.py +3 -6
  8. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/daemon.py +37 -33
  9. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/headless.py +3 -5
  10. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/transport/session.py +1 -13
  11. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_model.py +4 -4
  12. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_startup.py +6 -1
  13. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/command_router.py +15 -2
  14. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/model_config.py +9 -16
  15. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/invocation.py +28 -22
  16. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/.gitignore +0 -0
  17. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/README.md +0 -0
  18. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/__init__.py +0 -0
  19. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/__init__.py +0 -0
  20. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/__init__.py +0 -0
  21. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  22. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/__init__.py +0 -0
  23. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  24. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  25. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/launcher.py +0 -0
  26. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/main.py +0 -0
  27. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/config/__init__.py +0 -0
  28. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/config/cli_config.py +0 -0
  29. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/config/loader.py +0 -0
  30. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/config/logging_setup.py +0 -0
  31. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/__init__.py +0 -0
  32. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/headless/processor.py +0 -0
  33. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  34. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  35. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  36. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  37. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  38. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  39. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  40. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  41. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  42. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
  43. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  44. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  45. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
  46. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  47. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  48. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/step_router.py +0 -0
  49. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  50. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/transcript.py +0 -0
  51. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/token_events_debug.py +0 -0
  52. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/token_usage.py +0 -0
  53. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  54. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  55. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  56. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  57. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/messages.py +0 -0
  58. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/__init__.py +0 -0
  59. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/_cli_context.py +0 -0
  60. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/_env_vars.py +0 -0
  61. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/_version.py +0 -0
  62. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/__init__.py +0 -0
  63. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_app.py +0 -0
  64. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_execution.py +0 -0
  65. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_history.py +0 -0
  66. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  67. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_module_init.py +0 -0
  68. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_ui.py +0 -0
  69. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/app.tcss +0 -0
  70. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/binding.py +0 -0
  71. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/command_registry.py +0 -0
  72. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/__init__.py +0 -0
  73. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
  74. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  75. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/config.py +0 -0
  76. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/file_change_notify.py +0 -0
  77. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  78. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/hooks.py +0 -0
  79. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/input.py +0 -0
  80. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/markdown_theme.py +0 -0
  81. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/media_utils.py +0 -0
  82. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/path_utils.py +0 -0
  83. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/preview_limits.py +0 -0
  84. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/project_utils.py +0 -0
  85. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/sessions.py +0 -0
  86. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/shell_color.py +0 -0
  87. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/__init__.py +0 -0
  88. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/load.py +0 -0
  89. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/spinner_labels.py +0 -0
  90. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/textual_adapter.py +0 -0
  91. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/theme.py +0 -0
  92. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/tips.py +0 -0
  93. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/tool_display.py +0 -0
  94. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/unicode_security.py +0 -0
  95. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/update_check.py +0 -0
  96. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  97. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/_links.py +0 -0
  98. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  99. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  100. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  101. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/context_data.py +0 -0
  102. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
  103. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/diff.py +0 -0
  104. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/editor.py +0 -0
  105. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  106. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
  107. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/history.py +0 -0
  108. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/loading.py +0 -0
  109. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  110. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  111. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  112. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
  113. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
  114. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
  115. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
  116. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
  117. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +0 -0
  118. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
  119. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
  120. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
  121. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
  122. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
  123. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
  124. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
  125. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
  126. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  127. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  128. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
  129. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
  130. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/status.py +0 -0
  131. {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  132. {soothe_cli-0.8.1 → 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.1
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,7 +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
+ Requires-Dist: soothe-client-python<1.0.0,>=0.10.0
26
26
  Requires-Dist: soothe-sdk<1.0.0,>=0.5.25
27
27
  Requires-Dist: textual<10.0.0,>=8.0.0
28
28
  Requires-Dist: typer<2.0.0,>=0.9.0
@@ -24,7 +24,7 @@ classifiers = [
24
24
  ]
25
25
  dependencies = [
26
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
+ "soothe-client-python>=0.10.0,<1.0.0", # WebSocket transport client
28
28
  "typer>=0.9.0,<2.0.0", # CLI framework
29
29
  "textual>=8.0.0,<10.0.0", # TUI framework
30
30
  "rich>=13.0.0,<15.0.0", # Console output
@@ -15,9 +15,9 @@ from pathlib import Path
15
15
 
16
16
  import typer
17
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
22
  from soothe_sdk.wire.protocol import preview_first
23
23
 
@@ -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
 
@@ -11,7 +11,7 @@ import typer
11
11
  from rich.console import Console
12
12
  from rich.panel import Panel
13
13
  from soothe_client import (
14
- WebSocketClient,
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")
@@ -22,9 +22,9 @@ from rich.console import Console
22
22
  from rich.panel import Panel
23
23
  from rich.table import Table
24
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:
@@ -18,13 +18,10 @@ from rich.panel import Panel
18
18
  from rich.prompt import Prompt
19
19
  from rich.table import Table
20
20
  from soothe_client import (
21
- WebSocketClient,
22
21
  is_daemon_live,
22
+ protocol1_rpc,
23
23
  websocket_url_from_config,
24
24
  )
25
- from soothe_sdk.client import (
26
- ProtocolError,
27
- )
28
25
 
29
26
  from soothe_cli.runtime import load_config
30
27
 
@@ -49,64 +46,8 @@ async def _check_daemon(ws_url: str) -> bool:
49
46
  return await is_daemon_live(ws_url, timeout=5.0)
50
47
 
51
48
 
52
- async def _rpc(
53
- ws_url: str,
54
- method: str,
55
- params: dict[str, Any] | None = None,
56
- *,
57
- mode: str = "request",
58
- timeout: float = 30.0,
59
- ) -> dict[str, Any]:
60
- """Send a protocol-1 RPC to the daemon and return the response.
61
-
62
- Uses the protocol-1 client API (RFC-450): ``request()`` for RPC,
63
- ``notify()`` for fire-and-forget, ``subscribe()`` for event streams.
64
- The wrapper preserves the dict-based error contract used by command
65
- handlers — callers check ``if "error" in response``.
66
-
67
- Args:
68
- ws_url: WebSocket URL.
69
- method: RPC method / notification target / subscription target
70
- (e.g. ``"loop_get"``, ``"loop_input"``, ``"loop_events"``).
71
- params: Structured parameters object.
72
- mode: One of ``"request"`` (blocking RPC), ``"notify"``
73
- (fire-and-forget), or ``"subscribe"`` (start a stream).
74
- timeout: Maximum seconds to wait.
75
-
76
- Returns:
77
- Response dict from daemon, or ``{"error": ...}`` on failure. For
78
- ``notify`` mode returns ``{}``; for ``subscribe`` mode returns
79
- ``{"subscription_id": <id>}``.
80
- """
81
- client = WebSocketClient(url=ws_url)
82
- try:
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
- )
88
- if mode == "notify":
89
- await asyncio.wait_for(client.notify(method, params or {}), timeout=timeout)
90
- return {}
91
- if mode == "subscribe":
92
- sub_id = await asyncio.wait_for(
93
- client.subscribe(method, params or {}, timeout=timeout),
94
- timeout=timeout,
95
- )
96
- return {"subscription_id": sub_id}
97
- result = await asyncio.wait_for(
98
- client.request(method, params or {}, timeout=timeout),
99
- timeout=timeout,
100
- )
101
- return result if isinstance(result, dict) else {"result": result}
102
- except TimeoutError:
103
- return {"error": "Timed out waiting for daemon response"}
104
- except ProtocolError as exc:
105
- return {"error": str(exc)}
106
- except (ConnectionError, OSError) as exc:
107
- return {"error": f"Connection error: {exc}"}
108
- finally:
109
- await client.close()
49
+ # Back-compat alias for tests and internal callers.
50
+ _rpc = protocol1_rpc
110
51
 
111
52
 
112
53
  def _resolve_continue_loop_id(ws_url: str, loop_id: str | None) -> str:
@@ -16,8 +16,8 @@ from rich.console import Console
16
16
  from rich.panel import Panel
17
17
  from rich.table import Table
18
18
  from soothe_client import (
19
- WebSocketClient,
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
@@ -13,13 +13,11 @@ from typing import Any
13
13
 
14
14
  import typer
15
15
  from soothe_client import (
16
- WebSocketClient,
17
- async_ws_command_client_from_config,
18
- bootstrap_loop_session,
19
- connect_websocket_with_retries,
16
+ async_command_client_from_config,
20
17
  websocket_url_from_config,
21
18
  )
22
- from soothe_client.appkit import is_loop_scoped_event, unwrap_next
19
+ from soothe_client.appkit import DaemonSession
20
+ from soothe_client.appkit.events import is_loop_scoped_event, unwrap_next
23
21
 
24
22
  from soothe_cli.cli.execution.daemon_errors import (
25
23
  friendly_daemon_execution_error,
@@ -45,11 +43,11 @@ def _emit_headless_error(message: str) -> None:
45
43
  typer.echo(f"ERROR: {message}", err=True)
46
44
 
47
45
 
48
- async def _send_cancel_to_daemon(client: WebSocketClient) -> None:
46
+ async def _send_cancel_to_daemon(session: DaemonSession) -> None:
49
47
  """Send /cancel to the daemon with a short timeout."""
50
48
  try:
51
49
  await asyncio.wait_for(
52
- client.notify("slash_command", {"cmd": "/cancel"}),
50
+ session.cancel_remote_query(),
53
51
  timeout=_CANCEL_SEND_TIMEOUT_S,
54
52
  )
55
53
  except Exception:
@@ -81,14 +79,12 @@ async def _run_headless_session_once(
81
79
  max_iterations: int | None = None,
82
80
  ) -> tuple[int, bool]:
83
81
  """Run one headless daemon session; return ``(exit_code, retry_on_worker_loss)``."""
84
- ws_url = websocket_url_from_config(cfg)
85
- client = WebSocketClient(url=ws_url)
86
-
87
82
  # Track whether the daemon was notified of cancellation.
88
83
  cancel_sent = False
89
84
  sigint_received = False
90
85
  sigint_count = 0
91
86
  original_sigint: Any = None
87
+ session: DaemonSession | None = None
92
88
 
93
89
  cron_text = _parse_cron_slash_prompt(prompt)
94
90
  if cron_text is not None:
@@ -96,7 +92,7 @@ async def _run_headless_session_once(
96
92
  _emit_headless_error("Usage: /cron <natural language schedule>")
97
93
  return 1, False
98
94
  try:
99
- ws_client = async_ws_command_client_from_config(cfg)
95
+ ws_client = async_command_client_from_config(cfg)
100
96
  result = await ws_client.cron_add(cron_text)
101
97
  except RuntimeError as exc:
102
98
  _emit_headless_error(str(exc))
@@ -109,27 +105,34 @@ async def _run_headless_session_once(
109
105
  return 0, False
110
106
 
111
107
  try:
112
- await connect_websocket_with_retries(client)
113
108
  cli_ws = resolve_cli_loop_workspace()
114
- # IG-441: three first-class modes (batch / adaptive / streaming).
109
+ # Three first-class modes (batch / adaptive / streaming).
115
110
  # Default is ``adaptive`` for headless runs as well — it gives smooth
116
111
  # progress on long synthesis while keeping wire traffic bounded.
117
112
  override = getattr(cfg, "output_streaming_mode", None)
118
113
  stream_delivery = override if override in ("batch", "adaptive", "streaming") else "adaptive"
119
114
 
120
- status_event = await bootstrap_loop_session(
121
- client,
122
- resume_loop_id=resume_loop_id,
123
- stream_delivery=stream_delivery,
115
+ session = DaemonSession(
116
+ websocket_url_from_config(cfg),
124
117
  workspace=cli_ws,
125
- subscribe_timeout_s=_SESSION_BOOTSTRAP_TIMEOUT_S,
118
+ stream_delivery=stream_delivery,
126
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
+
127
130
  if status_event.get("type") == "error":
128
131
  raw = str(status_event.get("message", "unknown"))
129
132
  _emit_headless_error(friendly_daemon_execution_error(raw))
130
133
  return 1, is_daemon_worker_subprocess_lost(raw)
131
134
 
132
- active_loop_id = status_event.get("loop_id")
135
+ active_loop_id = session.loop_id or status_event.get("loop_id")
133
136
  if not active_loop_id:
134
137
  _emit_headless_error("No loop_id after session bootstrap")
135
138
  return 1, False
@@ -138,8 +141,7 @@ async def _run_headless_session_once(
138
141
  effective_prompt = cleaned_prompt if subagent_name else prompt
139
142
 
140
143
  await asyncio.wait_for(
141
- client.send_input(
142
- active_loop_id,
144
+ session.send_turn(
143
145
  effective_prompt,
144
146
  autonomous=autonomous,
145
147
  max_iterations=max_iterations,
@@ -182,13 +184,14 @@ async def _run_headless_session_once(
182
184
  )
183
185
 
184
186
  query_started = False
187
+ client = session.client
185
188
 
186
189
  while True:
187
190
  # Check if SIGINT fired and send /cancel to the daemon.
188
191
  if sigint_received and not cancel_sent:
189
192
  cancel_sent = True
190
193
  logger.info("Headless query interrupted; sending /cancel to daemon")
191
- await _send_cancel_to_daemon(client)
194
+ await _send_cancel_to_daemon(session)
192
195
  # After notifying the daemon, cancel the main task so
193
196
  # asyncio.run() can unwind cleanly.
194
197
  if main_task is not None and not main_task.done():
@@ -210,8 +213,8 @@ async def _run_headless_session_once(
210
213
  if not is_loop_scoped_event(event, active_loop_id=active_loop_id):
211
214
  continue
212
215
 
213
- # Unwrap protocol-1 ``next`` envelopes to the inner streaming frame
214
- # (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.
215
218
  if event_type == "next":
216
219
  inner = unwrap_next(event)
217
220
  if isinstance(inner, dict):
@@ -263,18 +266,18 @@ async def _run_headless_session_once(
263
266
  processor.process_event(event)
264
267
 
265
268
  except KeyboardInterrupt:
266
- if not cancel_sent:
269
+ if session is not None and not cancel_sent:
267
270
  cancel_sent = True
268
271
  logger.info("Headless query interrupted by user; sending /cancel to daemon")
269
- await _send_cancel_to_daemon(client)
272
+ await _send_cancel_to_daemon(session)
270
273
  return 1, False
271
274
  except asyncio.CancelledError:
272
- if not cancel_sent:
275
+ if session is not None and not cancel_sent:
273
276
  cancel_sent = True
274
277
  logger.info("Headless query cancelled; sending /cancel to daemon")
275
278
  # Best-effort: the task is being cancelled so awaiting may fail.
276
279
  try:
277
- await asyncio.shield(_send_cancel_to_daemon(client))
280
+ await asyncio.shield(_send_cancel_to_daemon(session))
278
281
  except (asyncio.CancelledError, Exception):
279
282
  pass
280
283
  raise
@@ -300,7 +303,8 @@ async def _run_headless_session_once(
300
303
  signal.signal(signal.SIGINT, original_sigint)
301
304
  except (ValueError, OSError, RuntimeError):
302
305
  pass
303
- await client.close()
306
+ if session is not None:
307
+ await session.close()
304
308
 
305
309
 
306
310
  async def run_headless_via_daemon(
@@ -313,8 +317,8 @@ async def run_headless_via_daemon(
313
317
  ) -> int:
314
318
  """Run a single prompt by connecting to a running daemon.
315
319
 
316
- Uses WebSocket transport for all connections (RFC-0013).
317
- 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.
318
322
 
319
323
  Retries once when the worker pool loses a subprocess mid-query (common idle-timeout
320
324
  race or transient worker recycle).
@@ -5,7 +5,7 @@ import sys
5
5
 
6
6
  import typer
7
7
  from soothe_client import (
8
- WebSocketClient,
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
 
@@ -12,12 +12,6 @@ from typing import Any
12
12
  from soothe_client.appkit import DaemonSession
13
13
  from soothe_client.helpers import websocket_url_from_config
14
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
15
  from soothe_cli.runtime.state.session_stats import TurnEventStats
22
16
  from soothe_cli.runtime.wire.chunk_filter import should_drop_stream_chunk_early
23
17
 
@@ -49,7 +43,7 @@ class TuiDaemonSession(DaemonSession):
49
43
  )
50
44
 
51
45
  def _resolve_stream_delivery_mode(self) -> str:
52
- """Determine stream delivery mode from config (RFC-614).
46
+ """Determine stream delivery mode from config.
53
47
 
54
48
  Returns one of ``batch`` | ``adaptive`` | ``streaming``. CLI override
55
49
  wins, then config; defaults to ``adaptive``.
@@ -68,13 +62,7 @@ class TuiDaemonSession(DaemonSession):
68
62
  return "adaptive"
69
63
 
70
64
 
71
- # Historical alias used by some call sites / docs.
72
- DaemonSession = TuiDaemonSession # noqa: F811
73
-
74
65
  __all__ = [
75
- "DaemonSession",
76
66
  "TUI_EXIT_HANDSHAKE_TIMEOUT_S",
77
67
  "TuiDaemonSession",
78
- "bootstrap_loop_session",
79
- "connect_websocket_with_retries",
80
68
  ]
@@ -310,7 +310,7 @@ class _ModelMixin:
310
310
  task: Task description for autonomous execution.
311
311
  """
312
312
  from soothe_client import (
313
- async_ws_command_client_from_config,
313
+ async_command_client_from_config,
314
314
  is_daemon_live,
315
315
  websocket_url_from_config,
316
316
  )
@@ -333,7 +333,7 @@ class _ModelMixin:
333
333
  workspace = self._cwd if hasattr(self, "_cwd") else os.getcwd()
334
334
 
335
335
  try:
336
- client = async_ws_command_client_from_config(cfg)
336
+ client = async_command_client_from_config(cfg)
337
337
  result = await client.autopilot_submit(task, workspace=workspace)
338
338
  except RuntimeError as exc:
339
339
  await self._mount_message(ErrorMessage(str(exc)))
@@ -358,7 +358,7 @@ class _ModelMixin:
358
358
  slash_input: Original slash command for chat display.
359
359
  """
360
360
  from soothe_client import (
361
- async_ws_command_client_from_config,
361
+ async_command_client_from_config,
362
362
  is_daemon_live,
363
363
  websocket_url_from_config,
364
364
  )
@@ -378,7 +378,7 @@ class _ModelMixin:
378
378
  return
379
379
 
380
380
  try:
381
- client = async_ws_command_client_from_config(cfg)
381
+ client = async_command_client_from_config(cfg)
382
382
  result = await client.cron_add(text)
383
383
  except RuntimeError as exc:
384
384
  message = str(exc)
@@ -416,7 +416,12 @@ class _StartupMixin:
416
416
  """
417
417
  from soothe_cli.tui.skills.invocation import discover_skills_async
418
418
 
419
- return await discover_skills_async(daemon_config=self._daemon_config)
419
+ session = getattr(self, "_daemon_session", None)
420
+ client = getattr(session, "client", None) if session is not None else None
421
+ return await discover_skills_async(
422
+ daemon_config=self._daemon_config,
423
+ client=client,
424
+ )
420
425
 
421
426
  @staticmethod
422
427
  def _daemon_connect_hint_extra(*, attempt: int, max_attempts: int) -> str | None:
@@ -112,22 +112,35 @@ def parse_command_params(entry: dict[str, Any], query: str) -> dict[str, Any]:
112
112
  async def route_slash_command(
113
113
  cmd_input: str,
114
114
  console: Console,
115
- client: WebSocketClient,
115
+ client: WebSocketClient | None = None,
116
116
  *,
117
117
  loop_id: str | None = None,
118
+ session: Any | None = None,
118
119
  ) -> bool:
119
- """Route slash command based on registry metadata (RFC-454).
120
+ """Route slash command based on registry metadata.
120
121
 
121
122
  Args:
122
123
  cmd_input: Full command input (e.g., "/memory", "/deep_research topic")
123
124
  console: Rich console for rendering
124
125
  client: WebSocket client for daemon communication
126
+ loop_id: Active StrangeLoop id
127
+ session: Optional ``DaemonSession``; when set, uses ``session.client`` and
128
+ ``session.loop_id`` unless overridden.
125
129
 
126
130
  Returns:
127
131
  True if command was handled, False if unknown command
128
132
  """
129
133
  from soothe_cli.tui.commands.slash_commands import COMMANDS
130
134
 
135
+ if session is not None:
136
+ client = client or getattr(session, "client", None)
137
+ if loop_id is None:
138
+ loop_id = getattr(session, "loop_id", None)
139
+
140
+ if client is None:
141
+ console.print("[red]Error: No daemon connection[/red]")
142
+ return True
143
+
131
144
  command, query = parse_slash_command(cmd_input)
132
145
 
133
146
  # Not a slash command
@@ -73,7 +73,7 @@ async def _fetch_provider_config(provider_name: str) -> dict[str, Any] | None:
73
73
  """
74
74
  try:
75
75
  from soothe_client import (
76
- WebSocketClient,
76
+ connected_websocket,
77
77
  fetch_config_section,
78
78
  )
79
79
 
@@ -82,13 +82,12 @@ async def _fetch_provider_config(provider_name: str) -> dict[str, Any] | None:
82
82
  cli_cfg = load_config()
83
83
  ws_url = cli_cfg.websocket_url()
84
84
 
85
- client = WebSocketClient(url=ws_url)
86
- await client.connect()
87
- try:
88
- providers_data = await fetch_config_section(client, "providers", timeout=5.0)
89
- return providers_data.get(provider_name)
90
- finally:
91
- await client.close()
85
+ async def _fetch() -> dict | None:
86
+ async with connected_websocket(ws_url, timeout=5.0) as client:
87
+ providers_data = await fetch_config_section(client, "providers", timeout=5.0)
88
+ return providers_data.get(provider_name)
89
+
90
+ return await _fetch()
92
91
  except Exception:
93
92
  logger.debug("Could not fetch provider config from daemon", exc_info=True)
94
93
  return None
@@ -484,7 +483,7 @@ def get_available_models() -> list[ModelProfileEntry]:
484
483
  import asyncio
485
484
 
486
485
  from soothe_client import (
487
- WebSocketClient,
486
+ connected_websocket,
488
487
  fetch_config_section,
489
488
  )
490
489
 
@@ -494,15 +493,9 @@ def get_available_models() -> list[ModelProfileEntry]:
494
493
  cli_cfg = load_config()
495
494
  ws_url = cli_cfg.websocket_url()
496
495
 
497
- # Fetch providers section from daemon
498
- client = WebSocketClient(url=ws_url)
499
-
500
496
  async def _fetch_providers() -> dict:
501
- await client.connect()
502
- try:
497
+ async with connected_websocket(ws_url, timeout=5.0) as client:
503
498
  return await fetch_config_section(client, "providers", timeout=5.0)
504
- finally:
505
- await client.close()
506
499
 
507
500
  providers_data = asyncio.run(_fetch_providers())
508
501
 
@@ -4,9 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  from collections import OrderedDict
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from soothe_client import (
10
+ connected_websocket,
10
11
  fetch_skills_catalog,
11
12
  websocket_url_from_config,
12
13
  )
@@ -14,6 +15,8 @@ from soothe_client import (
14
15
  from soothe_cli.tui.skills.load import ExtendedSkillMetadata
15
16
 
16
17
  if TYPE_CHECKING:
18
+ from soothe_client import WebSocketClient
19
+
17
20
  from soothe_cli.config.cli_config import CLIConfig
18
21
 
19
22
  logger = logging.getLogger(__name__)
@@ -21,44 +24,47 @@ logger = logging.getLogger(__name__)
21
24
 
22
25
  async def discover_skills_async(
23
26
  daemon_config: CLIConfig | None = None,
27
+ *,
28
+ client: WebSocketClient | None = None,
24
29
  ) -> list[ExtendedSkillMetadata]:
25
- """Discover skills from daemon RPC (IG-174 Phase 2).
30
+ """Discover skills from daemon RPC.
26
31
 
27
- Fetches wire-safe skill metadata from daemon via WebSocket RPC.
28
- Daemon handles all skill discovery (built-in, user, project, etc.)
29
- and returns wire-safe metadata. No local filesystem access.
32
+ Prefers an already-connected ``client`` (e.g. ``DaemonSession.client``) so the
33
+ TUI does not open a second WebSocket. Falls back to a one-shot connection
34
+ when only ``daemon_config`` is provided.
30
35
 
31
36
  Args:
32
- daemon_config: Daemon config for WebSocket URL construction.
37
+ daemon_config: Daemon config for WebSocket URL construction (oneshot path).
38
+ client: Optional live WebSocket client to reuse.
33
39
 
34
40
  Returns:
35
41
  List of skill metadata dicts sorted by ascending precedence
36
42
  (built-in first, winning entry last). Empty list if daemon
37
43
  unavailable.
38
44
  """
39
- from soothe_client import WebSocketClient
40
-
41
- if daemon_config is None:
42
- logger.warning("No daemon_config provided for skills discovery; returning empty catalog")
43
- return []
44
-
45
- ws_url = websocket_url_from_config(daemon_config)
46
- client = WebSocketClient(url=ws_url)
47
-
48
45
  by_name: OrderedDict[str, ExtendedSkillMetadata] = OrderedDict()
49
46
 
50
- try:
51
- await client.connect()
52
- skills_wire = await fetch_skills_catalog(client, timeout=15.0)
53
- await client.close()
54
-
55
- # Build by_name mapping from wire-safe metadata
47
+ async def _load(ws: Any) -> None:
48
+ skills_wire = await fetch_skills_catalog(ws, timeout=15.0)
56
49
  for skill_meta in skills_wire:
57
50
  name = skill_meta.get("name")
58
51
  if name:
59
52
  by_name[name] = skill_meta
53
+
54
+ try:
55
+ if client is not None:
56
+ await _load(client)
57
+ elif daemon_config is not None:
58
+ ws_url = websocket_url_from_config(daemon_config)
59
+ async with connected_websocket(ws_url, timeout=15.0) as ws:
60
+ await _load(ws)
61
+ else:
62
+ logger.warning(
63
+ "No daemon_config or client provided for skills discovery; returning empty catalog"
64
+ )
65
+ return []
60
66
  except Exception as e:
61
- logger.warning(f"Failed to fetch skills from daemon: {e}")
67
+ logger.warning("Skills discovery failed: %s", e, exc_info=True)
62
68
  return []
63
69
 
64
70
  return list(by_name.values())
File without changes
File without changes