soothe-cli 0.6.17__tar.gz → 0.7.0__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.6.17 → soothe_cli-0.7.0}/PKG-INFO +1 -1
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/loop_cmd.py +59 -47
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/status_cmd.py +26 -9
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/daemon.py +54 -3
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/headless/processor.py +14 -3
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/transport/session.py +124 -33
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_app.py +15 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_execution.py +11 -6
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_history.py +0 -2
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_messages_mixin.py +38 -29
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_model.py +5 -11
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_module_init.py +3 -1
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_startup.py +17 -50
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_ui.py +0 -1
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/binding.py +2 -1
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/command_registry.py +6 -6
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/commands/__init__.py +0 -6
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/commands/command_router.py +7 -13
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/commands/slash_commands.py +0 -33
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/commands/subagent_routing.py +7 -1
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/media_utils.py +1 -5
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/model_config.py +5 -4
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/sessions.py +24 -1
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/skills/invocation.py +0 -13
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/textual_adapter.py +132 -57
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/theme.py +0 -5
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/chat_input.py +3 -3
- soothe_cli-0.7.0/src/soothe_cli/tui/widgets/context_viewer.py +298 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/loop_selector.py +126 -112
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/cognition_step.py +59 -8
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/cognition_step_activity.py +13 -5
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/status.py +2 -2
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/.gitignore +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/README.md +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/pyproject.toml +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/cron_cmd.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/__init__.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/_helpers.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/app.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/assistant.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/clarification.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/cognition_goal_tree.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/cognition_reason.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/cognition_subagent.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/diff_message.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/error.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/skill.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/messages/user.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {soothe_cli-0.6.17 → soothe_cli-0.7.0}/src/soothe_cli/tui/widgets/welcome.py +0 -0
|
@@ -17,7 +17,12 @@ 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
|
|
20
|
+
from soothe_sdk.client import (
|
|
21
|
+
ProtocolError,
|
|
22
|
+
WebSocketClient,
|
|
23
|
+
is_daemon_live,
|
|
24
|
+
websocket_url_from_config,
|
|
25
|
+
)
|
|
21
26
|
|
|
22
27
|
from soothe_cli.runtime import load_config
|
|
23
28
|
|
|
@@ -44,37 +49,50 @@ async def _check_daemon(ws_url: str) -> bool:
|
|
|
44
49
|
|
|
45
50
|
async def _rpc(
|
|
46
51
|
ws_url: str,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
method: str,
|
|
53
|
+
params: dict[str, Any] | None = None,
|
|
54
|
+
*,
|
|
55
|
+
mode: str = "request",
|
|
50
56
|
timeout: float = 30.0,
|
|
51
57
|
) -> dict[str, Any]:
|
|
52
|
-
"""Send
|
|
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``.
|
|
53
64
|
|
|
54
65
|
Args:
|
|
55
66
|
ws_url: WebSocket URL.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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).
|
|
59
72
|
timeout: Maximum seconds to wait.
|
|
60
73
|
|
|
61
74
|
Returns:
|
|
62
|
-
Response dict from daemon.
|
|
75
|
+
Response dict from daemon, or ``{"error": ...}`` on failure. For
|
|
76
|
+
``notify`` mode returns ``{}``; for ``subscribe`` mode returns
|
|
77
|
+
``{"subscription_id": <id>}``.
|
|
63
78
|
"""
|
|
64
79
|
client = WebSocketClient(url=ws_url)
|
|
65
80
|
try:
|
|
66
81
|
await client.connect()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return event
|
|
82
|
+
if mode == "notify":
|
|
83
|
+
await asyncio.wait_for(client.notify(method, params or {}), timeout=timeout)
|
|
84
|
+
return {}
|
|
85
|
+
if mode == "subscribe":
|
|
86
|
+
sub_id = await asyncio.wait_for(client.subscribe(method, params or {}), timeout=timeout)
|
|
87
|
+
return {"subscription_id": sub_id}
|
|
88
|
+
result = await asyncio.wait_for(client.request(method, params or {}), timeout=timeout)
|
|
89
|
+
return result if isinstance(result, dict) else {"result": result}
|
|
76
90
|
except TimeoutError:
|
|
77
91
|
return {"error": "Timed out waiting for daemon response"}
|
|
92
|
+
except ProtocolError as exc:
|
|
93
|
+
return {"error": str(exc)}
|
|
94
|
+
except (ConnectionError, OSError) as exc:
|
|
95
|
+
return {"error": f"Connection error: {exc}"}
|
|
78
96
|
finally:
|
|
79
97
|
await client.close()
|
|
80
98
|
|
|
@@ -91,9 +109,8 @@ def _resolve_continue_loop_id(ws_url: str, loop_id: str | None) -> str:
|
|
|
91
109
|
response = asyncio.run(
|
|
92
110
|
_rpc(
|
|
93
111
|
ws_url,
|
|
94
|
-
"
|
|
95
|
-
{"
|
|
96
|
-
"loop_list_response",
|
|
112
|
+
"loop_list",
|
|
113
|
+
{"limit": 20},
|
|
97
114
|
)
|
|
98
115
|
)
|
|
99
116
|
if "error" in response:
|
|
@@ -153,9 +170,8 @@ def list_loops(
|
|
|
153
170
|
response = asyncio.run(
|
|
154
171
|
_rpc(
|
|
155
172
|
ws_url,
|
|
156
|
-
"
|
|
157
|
-
{"
|
|
158
|
-
"loop_list_response",
|
|
173
|
+
"loop_list",
|
|
174
|
+
{"filter": {"status": status} if status else None, "limit": limit},
|
|
159
175
|
)
|
|
160
176
|
)
|
|
161
177
|
|
|
@@ -211,9 +227,8 @@ def describe_loop(
|
|
|
211
227
|
response = asyncio.run(
|
|
212
228
|
_rpc(
|
|
213
229
|
ws_url,
|
|
214
|
-
"
|
|
230
|
+
"loop_get",
|
|
215
231
|
{"loop_id": loop_id, "verbose": verbose},
|
|
216
|
-
"loop_get_response",
|
|
217
232
|
)
|
|
218
233
|
)
|
|
219
234
|
|
|
@@ -327,9 +342,8 @@ def visualize_loop_tree(
|
|
|
327
342
|
response = asyncio.run(
|
|
328
343
|
_rpc(
|
|
329
344
|
ws_url,
|
|
330
|
-
"
|
|
345
|
+
"loop_tree",
|
|
331
346
|
{"loop_id": loop_id, "format": format},
|
|
332
|
-
"loop_tree_response",
|
|
333
347
|
)
|
|
334
348
|
)
|
|
335
349
|
|
|
@@ -384,9 +398,8 @@ def prune_loop_branches(
|
|
|
384
398
|
response = asyncio.run(
|
|
385
399
|
_rpc(
|
|
386
400
|
ws_url,
|
|
387
|
-
"
|
|
401
|
+
"loop_prune",
|
|
388
402
|
{"loop_id": loop_id, "retention_days": retention_days, "dry_run": dry_run},
|
|
389
|
-
"loop_prune_response",
|
|
390
403
|
)
|
|
391
404
|
)
|
|
392
405
|
|
|
@@ -394,10 +407,13 @@ def prune_loop_branches(
|
|
|
394
407
|
typer.echo(f"Error: {response['error']}", err=True)
|
|
395
408
|
sys.exit(1)
|
|
396
409
|
|
|
397
|
-
|
|
410
|
+
# Protocol-1: request() returns the result dict directly (e.g.
|
|
411
|
+
# {"pruned": N, "remaining": N, "dry_run": bool}), not wrapped in {"result": ...}.
|
|
412
|
+
pruned = response.get("pruned", 0)
|
|
413
|
+
remaining = response.get("remaining", 0)
|
|
398
414
|
console.print("[green]Summary:[/green]")
|
|
399
|
-
console.print(f" Branches pruned: {
|
|
400
|
-
console.print(f" Remaining: {
|
|
415
|
+
console.print(f" Branches pruned: {pruned}")
|
|
416
|
+
console.print(f" Remaining: {remaining}")
|
|
401
417
|
|
|
402
418
|
|
|
403
419
|
@loop_app.command("delete")
|
|
@@ -424,9 +440,8 @@ def delete_loop(
|
|
|
424
440
|
response = asyncio.run(
|
|
425
441
|
_rpc(
|
|
426
442
|
ws_url,
|
|
427
|
-
"
|
|
443
|
+
"loop_get",
|
|
428
444
|
{"loop_id": loop_id, "verbose": False},
|
|
429
|
-
"loop_get_response",
|
|
430
445
|
)
|
|
431
446
|
)
|
|
432
447
|
|
|
@@ -456,9 +471,8 @@ def delete_loop(
|
|
|
456
471
|
delete_response = asyncio.run(
|
|
457
472
|
_rpc(
|
|
458
473
|
ws_url,
|
|
459
|
-
"
|
|
474
|
+
"loop_delete",
|
|
460
475
|
{"loop_id": loop_id},
|
|
461
|
-
"loop_delete_response",
|
|
462
476
|
)
|
|
463
477
|
)
|
|
464
478
|
|
|
@@ -728,9 +742,9 @@ def detach_loop(
|
|
|
728
742
|
response = asyncio.run(
|
|
729
743
|
_rpc(
|
|
730
744
|
ws_url,
|
|
731
|
-
"
|
|
745
|
+
"loop_detach",
|
|
732
746
|
{"loop_id": loop_id},
|
|
733
|
-
"
|
|
747
|
+
mode="notify",
|
|
734
748
|
)
|
|
735
749
|
)
|
|
736
750
|
|
|
@@ -766,9 +780,9 @@ def attach_loop(
|
|
|
766
780
|
response = asyncio.run(
|
|
767
781
|
_rpc(
|
|
768
782
|
ws_url,
|
|
769
|
-
"
|
|
783
|
+
"loop_events",
|
|
770
784
|
{"loop_id": loop_id},
|
|
771
|
-
"
|
|
785
|
+
mode="subscribe",
|
|
772
786
|
)
|
|
773
787
|
)
|
|
774
788
|
|
|
@@ -782,9 +796,8 @@ def attach_loop(
|
|
|
782
796
|
status_response = asyncio.run(
|
|
783
797
|
_rpc(
|
|
784
798
|
ws_url,
|
|
785
|
-
"
|
|
799
|
+
"loop_get",
|
|
786
800
|
{"loop_id": loop_id, "verbose": False},
|
|
787
|
-
"loop_get_response",
|
|
788
801
|
)
|
|
789
802
|
)
|
|
790
803
|
|
|
@@ -824,9 +837,8 @@ def new_loop(
|
|
|
824
837
|
response = asyncio.run(
|
|
825
838
|
_rpc(
|
|
826
839
|
ws_url,
|
|
827
|
-
"
|
|
840
|
+
"loop_new",
|
|
828
841
|
{},
|
|
829
|
-
"loop_new_response",
|
|
830
842
|
)
|
|
831
843
|
)
|
|
832
844
|
|
|
@@ -842,9 +854,9 @@ def new_loop(
|
|
|
842
854
|
input_response = asyncio.run(
|
|
843
855
|
_rpc(
|
|
844
856
|
ws_url,
|
|
845
|
-
"
|
|
857
|
+
"loop_input",
|
|
846
858
|
{"loop_id": loop_id, "content": prompt},
|
|
847
|
-
"
|
|
859
|
+
mode="notify",
|
|
848
860
|
)
|
|
849
861
|
)
|
|
850
862
|
if "error" in input_response:
|
|
@@ -15,7 +15,12 @@ import typer
|
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
from rich.panel import Panel
|
|
17
17
|
from rich.table import Table
|
|
18
|
-
from soothe_sdk.client import
|
|
18
|
+
from soothe_sdk.client import (
|
|
19
|
+
WebSocketClient,
|
|
20
|
+
check_daemon_status,
|
|
21
|
+
is_daemon_live,
|
|
22
|
+
websocket_url_from_config,
|
|
23
|
+
)
|
|
19
24
|
|
|
20
25
|
from soothe_cli.config.loader import load_config
|
|
21
26
|
|
|
@@ -38,8 +43,7 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
|
|
|
38
43
|
client = WebSocketClient(url=ws_url)
|
|
39
44
|
try:
|
|
40
45
|
await client.connect()
|
|
41
|
-
|
|
42
|
-
return status
|
|
46
|
+
return await check_daemon_status(client, timeout=timeout)
|
|
43
47
|
except Exception as e:
|
|
44
48
|
return {"error": str(e)}
|
|
45
49
|
finally:
|
|
@@ -47,28 +51,41 @@ async def _fetch_status(ws_url: str, timeout: float = 5.0) -> dict[str, Any]:
|
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
async def _fetch_ready_state(ws_url: str, timeout: float = 5.0) -> dict[str, Any] | None:
|
|
50
|
-
"""Fetch daemon readiness state via WebSocket handshake.
|
|
54
|
+
"""Fetch daemon readiness state via WebSocket connection_init/ack handshake.
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
Sends ``connection_init`` and waits for ``connection_ack`` which carries
|
|
57
|
+
the daemon's ``readiness_state``.
|
|
53
58
|
|
|
54
59
|
Args:
|
|
55
60
|
ws_url: WebSocket URL.
|
|
56
61
|
timeout: Timeout for handshake.
|
|
57
62
|
|
|
58
63
|
Returns:
|
|
59
|
-
|
|
64
|
+
``connection_ack`` result dict (containing ``readiness_state``) or None.
|
|
60
65
|
"""
|
|
61
66
|
import websockets
|
|
62
67
|
|
|
68
|
+
init_msg = json.dumps(
|
|
69
|
+
{
|
|
70
|
+
"proto": "1",
|
|
71
|
+
"type": "connection_init",
|
|
72
|
+
"params": {
|
|
73
|
+
"client_version": "0.5.0",
|
|
74
|
+
"accept_proto": ["1"],
|
|
75
|
+
"capabilities": ["streaming"],
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
63
80
|
try:
|
|
64
81
|
async with asyncio.timeout(timeout):
|
|
65
82
|
async with websockets.connect(ws_url) as ws:
|
|
66
|
-
|
|
83
|
+
await ws.send(init_msg)
|
|
67
84
|
for _ in range(3):
|
|
68
85
|
msg = await ws.recv()
|
|
69
86
|
data = json.loads(msg)
|
|
70
|
-
if data.get("type") == "
|
|
71
|
-
return data
|
|
87
|
+
if data.get("type") == "connection_ack":
|
|
88
|
+
return data.get("result", data)
|
|
72
89
|
except Exception:
|
|
73
90
|
pass
|
|
74
91
|
return None
|
|
@@ -39,9 +39,41 @@ _HEADLESS_WORKER_LOST_RETRIES = 1
|
|
|
39
39
|
_CANCEL_SEND_TIMEOUT_S = 3.0
|
|
40
40
|
|
|
41
41
|
|
|
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
|
+
|
|
42
64
|
def _is_loop_scoped_event(event: dict[str, Any], *, active_loop_id: str) -> bool:
|
|
43
|
-
"""Return whether a daemon frame belongs to the active StrangeLoop session.
|
|
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
|
+
"""
|
|
44
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)
|
|
45
77
|
if event_type not in {"status", "event"}:
|
|
46
78
|
return True
|
|
47
79
|
return event.get("loop_id") == active_loop_id
|
|
@@ -55,7 +87,10 @@ def _emit_headless_error(message: str) -> None:
|
|
|
55
87
|
async def _send_cancel_to_daemon(client: WebSocketClient) -> None:
|
|
56
88
|
"""Send /cancel to the daemon with a short timeout."""
|
|
57
89
|
try:
|
|
58
|
-
await asyncio.wait_for(
|
|
90
|
+
await asyncio.wait_for(
|
|
91
|
+
client.notify("slash_command", {"cmd": "/cancel"}),
|
|
92
|
+
timeout=_CANCEL_SEND_TIMEOUT_S,
|
|
93
|
+
)
|
|
59
94
|
except Exception:
|
|
60
95
|
logger.warning("Failed to send /cancel to daemon", exc_info=True)
|
|
61
96
|
|
|
@@ -216,8 +251,18 @@ async def _run_headless_session_once(
|
|
|
216
251
|
if not _is_loop_scoped_event(event, active_loop_id=active_loop_id):
|
|
217
252
|
continue
|
|
218
253
|
|
|
254
|
+
# Unwrap protocol-1 ``next`` envelopes to the inner streaming frame
|
|
255
|
+
# (RFC-450 §9.3). ``status``/``error`` arrive raw and pass through.
|
|
256
|
+
if event_type == "next":
|
|
257
|
+
inner = _unwrap_next(event)
|
|
258
|
+
if isinstance(inner, dict):
|
|
259
|
+
event = inner
|
|
260
|
+
event_type = event.get("type", "")
|
|
261
|
+
|
|
219
262
|
if event_type == "error":
|
|
220
|
-
|
|
263
|
+
# Protocol-1 error envelope: {type:'error', error:{code, message, data}}
|
|
264
|
+
err_obj = event.get("error") or {}
|
|
265
|
+
raw = str(err_obj.get("message") or event.get("message") or "unknown")
|
|
221
266
|
_emit_headless_error(friendly_daemon_execution_error(raw))
|
|
222
267
|
return 1, is_daemon_worker_subprocess_lost(raw)
|
|
223
268
|
|
|
@@ -245,6 +290,12 @@ async def _run_headless_session_once(
|
|
|
245
290
|
break
|
|
246
291
|
if not _is_loop_scoped_event(nxt, active_loop_id=active_loop_id):
|
|
247
292
|
continue
|
|
293
|
+
# Unwrap ``next`` envelopes before handing to the
|
|
294
|
+
# processor so it sees the legacy frame shape.
|
|
295
|
+
if nxt.get("type") == "next":
|
|
296
|
+
inner = _unwrap_next(nxt)
|
|
297
|
+
if isinstance(inner, dict):
|
|
298
|
+
nxt = inner
|
|
248
299
|
processor.process_event(nxt)
|
|
249
300
|
|
|
250
301
|
processor.process_event(event)
|
|
@@ -465,9 +465,20 @@ class EventProcessor:
|
|
|
465
465
|
self._handle_custom_event(data, namespace)
|
|
466
466
|
|
|
467
467
|
def _handle_error_event(self, event: dict[str, Any]) -> None:
|
|
468
|
-
"""Handle error events.
|
|
469
|
-
|
|
470
|
-
|
|
468
|
+
"""Handle error events.
|
|
469
|
+
|
|
470
|
+
Supports both legacy flat errors (``{type:'error', message:...}``) and
|
|
471
|
+
protocol-1 error envelopes (``{type:'error', error:{code, message, data}}``).
|
|
472
|
+
"""
|
|
473
|
+
err_obj = event.get("error")
|
|
474
|
+
if isinstance(err_obj, dict):
|
|
475
|
+
# Protocol-1 error envelope.
|
|
476
|
+
error = str(err_obj.get("message", "Unknown error"))
|
|
477
|
+
context = err_obj.get("code")
|
|
478
|
+
else:
|
|
479
|
+
# Legacy flat error or bare string.
|
|
480
|
+
error = str(event.get("message", err_obj or "Unknown error"))
|
|
481
|
+
context = event.get("code")
|
|
471
482
|
self._renderer.on_error(error, context=context)
|
|
472
483
|
|
|
473
484
|
def _handle_messages(
|