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.
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/PKG-INFO +2 -2
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/pyproject.toml +1 -1
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/autopilot_cmd.py +35 -4
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/config_cmd.py +3 -6
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/cron_cmd.py +2 -2
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/loop_cmd.py +3 -62
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/status_cmd.py +3 -6
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/daemon.py +37 -33
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/headless.py +3 -5
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/transport/session.py +1 -13
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_model.py +4 -4
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_startup.py +6 -1
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/command_router.py +15 -2
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/model_config.py +9 -16
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/invocation.py +28 -22
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/.gitignore +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/README.md +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/id_format.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/subagent_task_display.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/token_events_debug.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/token_usage.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_execution.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/markdown_theme.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/shell_color.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/spinner_labels.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/textual_adapter.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/context_data.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/context_viewer.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/help_screen.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_step.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/plan_quick_view_overlay.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/router_profile_selector.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
#
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
317
|
-
Headless output is
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
+
"""Discover skills from daemon RPC.
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
51
|
-
await
|
|
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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/duration_format.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/renderer_protocol.py
RENAMED
|
File without changes
|
{soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/runtime/presentation/subagent_task_display.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py
RENAMED
|
File without changes
|
{soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_reason.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py
RENAMED
|
File without changes
|
{soothe_cli-0.8.1 → soothe_cli-0.8.2}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|