soothe-cli 0.6.11__tar.gz → 0.6.13__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 (113) hide show
  1. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/PKG-INFO +1 -1
  2. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/autopilot_cmd.py +34 -42
  3. soothe_cli-0.6.13/src/soothe_cli/cli/commands/cron_cmd.py +241 -0
  4. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/status_cmd.py +34 -3
  5. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/daemon.py +35 -0
  6. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/main.py +2 -0
  7. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/headless/processor.py +1 -1
  8. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_execution.py +15 -2
  9. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_model.py +59 -8
  10. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/command_registry.py +6 -0
  11. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/commands/command_router.py +4 -4
  12. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/commands/slash_commands.py +2 -2
  13. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/theme.py +6 -6
  14. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/messages.py +64 -72
  15. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/.gitignore +0 -0
  16. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/README.md +0 -0
  17. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/pyproject.toml +0 -0
  18. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/__init__.py +0 -0
  19. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/__init__.py +0 -0
  20. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/__init__.py +0 -0
  21. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  22. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  23. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/__init__.py +0 -0
  24. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  25. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/headless.py +0 -0
  26. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  27. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/launcher.py +0 -0
  28. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/config/__init__.py +0 -0
  29. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/config/cli_config.py +0 -0
  30. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/config/loader.py +0 -0
  31. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/config/logging_setup.py +0 -0
  32. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/__init__.py +0 -0
  33. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  34. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/_utils.py +0 -0
  35. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  36. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  37. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
  38. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  39. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  40. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  41. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  42. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  43. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  44. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
  45. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  46. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  47. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  48. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  49. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/step_router.py +0 -0
  50. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  51. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/transcript.py +0 -0
  52. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/transport/session.py +0 -0
  53. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  54. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  55. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  56. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  57. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  58. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/wire/messages.py +0 -0
  59. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/__init__.py +0 -0
  60. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/_cli_context.py +0 -0
  61. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/_env_vars.py +0 -0
  62. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/_version.py +0 -0
  63. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/__init__.py +0 -0
  64. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_app.py +0 -0
  65. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_commands.py +0 -0
  66. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_history.py +0 -0
  67. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  68. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_module_init.py +0 -0
  69. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_startup.py +0 -0
  70. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_ui.py +0 -0
  71. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/app.tcss +0 -0
  72. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/binding.py +0 -0
  73. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/commands/__init__.py +0 -0
  74. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  75. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/config.py +0 -0
  76. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/file_change_notify.py +0 -0
  77. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  78. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/hooks.py +0 -0
  79. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/input.py +0 -0
  80. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/media_utils.py +0 -0
  81. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/model_config.py +0 -0
  82. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/path_utils.py +0 -0
  83. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/preview_limits.py +0 -0
  84. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/project_utils.py +0 -0
  85. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/sessions.py +0 -0
  86. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/skills/__init__.py +0 -0
  87. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/skills/invocation.py +0 -0
  88. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/skills/load.py +0 -0
  89. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/textual_adapter.py +0 -0
  90. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/tips.py +0 -0
  91. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/tool_display.py +0 -0
  92. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/unicode_security.py +0 -0
  93. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/update_check.py +0 -0
  94. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  95. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/_links.py +0 -0
  96. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  97. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  98. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  99. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  100. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  101. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/diff.py +0 -0
  102. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/editor.py +0 -0
  103. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  104. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/history.py +0 -0
  105. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/loading.py +0 -0
  106. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  107. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  108. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  109. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  110. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  111. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/status.py +0 -0
  112. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  113. {soothe_cli-0.6.11 → soothe_cli-0.6.13}/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.6.11
3
+ Version: 0.6.13
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
@@ -1,6 +1,6 @@
1
1
  """Autopilot CLI subcommands for RFC-204.
2
2
 
3
- Daemon-backed control surface: submit tasks and manage goals via HTTP REST.
3
+ Daemon-backed control surface: submit tasks and manage goals via WebSocket.
4
4
  Requires ``soothed start``. Real-time monitoring is via TUI ``/autopilot``.
5
5
  """
6
6
 
@@ -15,11 +15,9 @@ from pathlib import Path
15
15
 
16
16
  import typer
17
17
  from soothe_sdk.client import (
18
- AutopilotHttpClient,
19
- ensure_http_rest_available,
20
- http_rest_url_from_config,
21
18
  is_daemon_live,
22
19
  websocket_url_from_config,
20
+ ws_command_client_from_config,
23
21
  )
24
22
  from soothe_sdk.client.protocol import preview_first
25
23
 
@@ -36,8 +34,8 @@ def _resolve_submit_workspace(explicit: str | None) -> str:
36
34
  return str(Path(raw).expanduser().resolve())
37
35
 
38
36
 
39
- def _require_daemon_http() -> AutopilotHttpClient:
40
- """Return a live HTTP client or exit."""
37
+ def _require_daemon_ws():
38
+ """Return a live WebSocket command client or exit."""
41
39
  from soothe_cli.runtime import load_config
42
40
 
43
41
  cfg = load_config()
@@ -48,13 +46,7 @@ def _require_daemon_http() -> AutopilotHttpClient:
48
46
  err=True,
49
47
  )
50
48
  sys.exit(1)
51
- base_url = http_rest_url_from_config(cfg)
52
- try:
53
- ensure_http_rest_available(base_url)
54
- except RuntimeError as exc:
55
- typer.echo(f"Error: {exc}", err=True)
56
- sys.exit(1)
57
- return AutopilotHttpClient(base_url)
49
+ return ws_command_client_from_config(cfg)
58
50
 
59
51
 
60
52
  @app.command("run")
@@ -79,9 +71,9 @@ def run(
79
71
  path — distinct from a one-shot agentic query via ``soothe -p``.
80
72
  """
81
73
  del max_iterations # daemon owns config for autopilot dispatch
82
- client = _require_daemon_http()
74
+ client = _require_daemon_ws()
83
75
  submit_workspace = _resolve_submit_workspace(workspace)
84
- result = client.submit(prompt, workspace=submit_workspace)
76
+ result = client.autopilot_submit(prompt, workspace=submit_workspace)
85
77
  goal_id = result.get("goal_id", "")
86
78
  typer.echo(f"Submitted goal: {goal_id}")
87
79
  if not wait or not goal_id:
@@ -89,7 +81,7 @@ def run(
89
81
 
90
82
  deadline = time.time() + 600
91
83
  while time.time() < deadline:
92
- detail = client.get_goal(goal_id)
84
+ detail = client.autopilot_get_goal(goal_id)
93
85
  goal = detail.get("goal") or {}
94
86
  status = goal.get("status", "unknown")
95
87
  if status in ("completed", "failed", "cancelled", "suspended"):
@@ -114,9 +106,9 @@ def submit(
114
106
  ),
115
107
  ) -> None:
116
108
  """Submit a new task to the daemon autopilot."""
117
- client = _require_daemon_http()
109
+ client = _require_daemon_ws()
118
110
  submit_workspace = _resolve_submit_workspace(workspace)
119
- result = client.submit(task, priority=priority, workspace=submit_workspace)
111
+ result = client.autopilot_submit(task, priority=priority, workspace=submit_workspace)
120
112
  goal_id = result.get("goal_id", "?")
121
113
  typer.echo(f"Task submitted (goal_id={goal_id})")
122
114
  typer.echo(f" Priority: {priority}")
@@ -126,8 +118,8 @@ def submit(
126
118
  @app.command("status")
127
119
  def status() -> None:
128
120
  """Show overall autopilot state and goal DAG summary."""
129
- client = _require_daemon_http()
130
- data = client.status()
121
+ client = _require_daemon_ws()
122
+ data = client.autopilot_status()
131
123
  state = data.get("state", data.get("status", "unknown"))
132
124
  running = data.get("running", False)
133
125
  dreaming = data.get("dreaming", False)
@@ -139,8 +131,8 @@ def status() -> None:
139
131
  if isinstance(loop_pool, dict) and loop_pool:
140
132
  typer.echo(f"Worker pool: {loop_pool}")
141
133
 
142
- jobs = client.list_jobs().get("jobs") or []
143
- goals = client.list_goals().get("goals") or []
134
+ jobs = client.autopilot_list_jobs().get("jobs") or []
135
+ goals = client.autopilot_list_goals().get("goals") or []
144
136
  typer.echo(f"\nJobs (root goals): {len(jobs)}")
145
137
  if goals:
146
138
  counts = Counter(str(g.get("status", "pending")) for g in goals)
@@ -182,8 +174,8 @@ def list_jobs_alias(
182
174
 
183
175
 
184
176
  def _list_jobs_impl(status_filter: str) -> None:
185
- client = _require_daemon_http()
186
- payload = client.list_jobs()
177
+ client = _require_daemon_ws()
178
+ payload = client.autopilot_list_jobs()
187
179
  jobs = payload.get("jobs") or []
188
180
  if not jobs:
189
181
  typer.echo("No jobs found.")
@@ -204,8 +196,8 @@ def list_goals(
204
196
  status_filter: str = typer.Option("", "--status", "-s", help="Filter by status."),
205
197
  ) -> None:
206
198
  """List all goals in the daemon autopilot DAG (including subgoals)."""
207
- client = _require_daemon_http()
208
- payload = client.list_goals()
199
+ client = _require_daemon_ws()
200
+ payload = client.autopilot_list_goals()
209
201
  goals = payload.get("goals") or []
210
202
  if not goals:
211
203
  typer.echo("No goals found.")
@@ -270,9 +262,9 @@ def show_job(
270
262
  A job is a root goal submitted by the user. This command shows
271
263
  the job's details and the complete goal DAG under it.
272
264
  """
273
- client = _require_daemon_http()
265
+ client = _require_daemon_ws()
274
266
  try:
275
- payload = client.get_job(job_id)
267
+ payload = client.autopilot_get_job(job_id)
276
268
  except RuntimeError as exc:
277
269
  typer.echo(str(exc), err=True)
278
270
  raise typer.Exit(1) from exc
@@ -314,8 +306,8 @@ def show_goal(
314
306
  goal_id: str = typer.Argument(..., help="Goal ID to show details for."),
315
307
  ) -> None:
316
308
  """Show details for a specific goal."""
317
- client = _require_daemon_http()
318
- payload = client.get_goal(goal_id)
309
+ client = _require_daemon_ws()
310
+ payload = client.autopilot_get_goal(goal_id)
319
311
  found = payload.get("goal")
320
312
  if not found:
321
313
  typer.echo(f"Goal '{goal_id}' not found.")
@@ -336,8 +328,8 @@ def cancel_goal(
336
328
  goal_id: str = typer.Argument(..., help="Goal ID to cancel."),
337
329
  ) -> None:
338
330
  """Cancel a goal via the daemon."""
339
- client = _require_daemon_http()
340
- result = client.cancel_goal(goal_id)
331
+ client = _require_daemon_ws()
332
+ result = client.autopilot_cancel_goal(goal_id)
341
333
  typer.echo(f"Cancel result: {result.get('status', result)}")
342
334
 
343
335
 
@@ -348,9 +340,9 @@ def approve_goal(
348
340
  ),
349
341
  ) -> None:
350
342
  """Approve a MUST-confirmation goal."""
351
- client = _require_daemon_http()
343
+ client = _require_daemon_ws()
352
344
  try:
353
- result = client.approve(goal_id)
345
+ result = client.autopilot_approve(goal_id)
354
346
  except RuntimeError as exc:
355
347
  typer.echo(str(exc), err=True)
356
348
  raise typer.Exit(1) from exc
@@ -362,9 +354,9 @@ def reject_goal(
362
354
  goal_id: str = typer.Argument(..., help="Confirmation ID to reject."),
363
355
  ) -> None:
364
356
  """Reject a proposed goal."""
365
- client = _require_daemon_http()
357
+ client = _require_daemon_ws()
366
358
  try:
367
- result = client.reject(goal_id)
359
+ result = client.autopilot_reject(goal_id)
368
360
  except RuntimeError as exc:
369
361
  typer.echo(str(exc), err=True)
370
362
  raise typer.Exit(1) from exc
@@ -380,9 +372,9 @@ def resume_goal(
380
372
  Reactivates a paused goal back to pending status so the scheduler
381
373
  can pick it up for execution. Use 'jobs' to list goals and their status.
382
374
  """
383
- client = _require_daemon_http()
375
+ client = _require_daemon_ws()
384
376
  try:
385
- result = client.resume(goal_id)
377
+ result = client.autopilot_resume(goal_id)
386
378
  except RuntimeError as exc:
387
379
  typer.echo(str(exc), err=True)
388
380
  raise typer.Exit(1) from exc
@@ -394,16 +386,16 @@ def resume_goal(
394
386
  @app.command("wake")
395
387
  def wake() -> None:
396
388
  """Exit dreaming mode — resume active execution."""
397
- client = _require_daemon_http()
398
- client.wake()
389
+ client = _require_daemon_ws()
390
+ client.autopilot_wake()
399
391
  typer.echo("Wake signal sent.")
400
392
 
401
393
 
402
394
  @app.command("dream")
403
395
  def dream() -> None:
404
396
  """Force enter dreaming mode."""
405
- client = _require_daemon_http()
406
- client.dream()
397
+ client = _require_daemon_ws()
398
+ client.autopilot_dream()
407
399
  typer.echo("Dream signal sent.")
408
400
 
409
401
 
@@ -0,0 +1,241 @@
1
+ """Cron CLI subcommands for RFC-229.
2
+
3
+ Daemon-backed control surface: manage scheduled jobs via WebSocket.
4
+ Requires ``soothed start``.
5
+
6
+ Commands:
7
+ soothe cron add <text> # Add scheduled job (natural language)
8
+ soothe cron list # List scheduled jobs
9
+ soothe cron show <job_id> # Show job details
10
+ soothe cron cancel <job_id> # Cancel a job
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import sys
17
+ from datetime import datetime
18
+ from typing import Annotated
19
+
20
+ import typer
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+ from rich.table import Table
24
+ from soothe_sdk.client import (
25
+ is_daemon_live,
26
+ websocket_url_from_config,
27
+ ws_command_client_from_config,
28
+ )
29
+
30
+ from soothe_cli.runtime import load_config
31
+
32
+ console = Console()
33
+
34
+ app = typer.Typer(help="Manage scheduled cron jobs — natural language scheduled tasks.")
35
+
36
+
37
+ def _require_cron_client():
38
+ """Return a live WebSocket command client or exit."""
39
+ cfg = load_config()
40
+ ws_url = websocket_url_from_config(cfg)
41
+ if not asyncio.run(is_daemon_live(ws_url, timeout=5.0)):
42
+ typer.echo(
43
+ "Error: Daemon not running. Start with 'soothed start'.",
44
+ err=True,
45
+ )
46
+ sys.exit(1)
47
+ return ws_command_client_from_config(cfg)
48
+
49
+
50
+ def _format_next_run(next_run: str) -> str:
51
+ if not next_run:
52
+ return "-"
53
+ try:
54
+ dt = datetime.fromisoformat(next_run.replace("Z", "+00:00"))
55
+ now = datetime.now(dt.tzinfo)
56
+ delta = dt - now
57
+ if delta.total_seconds() < 0:
58
+ return "due now"
59
+ if delta.total_seconds() < 3600:
60
+ return f"in {int(delta.total_seconds() / 60)}m"
61
+ if delta.total_seconds() < 86400:
62
+ return f"in {int(delta.total_seconds() / 3600)}h"
63
+ return f"in {int(delta.total_seconds() / 86400)}d"
64
+ except ValueError:
65
+ return next_run[:19]
66
+
67
+
68
+ @app.command("add")
69
+ def add_job(
70
+ text: Annotated[str, typer.Argument(help="Natural language schedule and task.")],
71
+ priority: Annotated[
72
+ int | None,
73
+ typer.Option("--priority", "-p", help="Job priority (0-100)."),
74
+ ] = None,
75
+ ) -> None:
76
+ """Add a scheduled cron job via natural language.
77
+
78
+ The daemon extracts schedule semantics via LLM and persists the job.
79
+
80
+ Example:
81
+ soothe cron add "in 1 hour remind me to check the deploy"
82
+ soothe cron add "every weekday at 9am send status report" --priority 70
83
+ """
84
+ client = _require_cron_client()
85
+ try:
86
+ result = client.cron_add(text, priority=priority)
87
+ except RuntimeError as exc:
88
+ typer.echo(f"Error: {exc}", err=True)
89
+ sys.exit(1)
90
+
91
+ job = result.get("job") or {}
92
+ job_id = job.get("id", "?")
93
+ console.print(
94
+ Panel(
95
+ "\n".join(
96
+ [
97
+ f"[bold]ID:[/bold] {job_id}",
98
+ f"[bold]Description:[/bold] {job.get('description', '-')}",
99
+ f"[bold]Schedule:[/bold] {job.get('schedule_kind', '?')} = {job.get('schedule_value', '?')}",
100
+ f"[bold]Next Run:[/bold] [yellow]{str(job.get('next_run', ''))[:19]}[/yellow]",
101
+ f"[bold]Status:[/bold] [green]{job.get('status', 'pending')}[/green]",
102
+ ]
103
+ ),
104
+ title="Scheduled Job Created",
105
+ border_style="green",
106
+ )
107
+ )
108
+
109
+
110
+ @app.command("list")
111
+ def list_jobs(
112
+ status: Annotated[
113
+ str | None,
114
+ typer.Option(
115
+ "--status",
116
+ "-s",
117
+ help="Filter by status (pending, running, completed, failed, cancelled).",
118
+ ),
119
+ ] = None,
120
+ ) -> None:
121
+ """List scheduled cron jobs.
122
+
123
+ Shows jobs with their ID, description, status, next run time, and run count.
124
+
125
+ Example:
126
+ soothe cron list
127
+ soothe cron list --status pending
128
+ """
129
+ client = _require_cron_client()
130
+ try:
131
+ result = client.cron_list_jobs(status=status)
132
+ except RuntimeError as exc:
133
+ typer.echo(f"Error: {exc}", err=True)
134
+ sys.exit(1)
135
+
136
+ jobs = result.get("jobs", [])
137
+ if not jobs:
138
+ console.print("[info]No scheduled jobs found.[/info]")
139
+ return
140
+
141
+ table = Table(title="Scheduled Jobs")
142
+ table.add_column("Job ID", style="cyan", width=12)
143
+ table.add_column("Description", width=40)
144
+ table.add_column("Status", style="green", width=10)
145
+ table.add_column("Next Run", style="yellow", width=20)
146
+ table.add_column("Runs", justify="right", width=6)
147
+
148
+ for job in jobs:
149
+ desc = job.get("description", "")
150
+ if len(desc) > 40:
151
+ desc = desc[:37] + "..."
152
+ table.add_row(
153
+ job.get("id", "?")[:12],
154
+ desc,
155
+ job.get("status", "pending"),
156
+ _format_next_run(job.get("next_run", "")),
157
+ str(job.get("run_count", 0)),
158
+ )
159
+
160
+ console.print(table)
161
+
162
+
163
+ @app.command("show")
164
+ def show_job(
165
+ job_id: Annotated[str, typer.Argument(help="Job ID to show details for.")],
166
+ ) -> None:
167
+ """Show details of a scheduled cron job.
168
+
169
+ Displays full job information including schedule kind, schedule value,
170
+ end condition, timestamps, and execution history.
171
+
172
+ Example:
173
+ soothe cron show abc123def456
174
+ """
175
+ client = _require_cron_client()
176
+ try:
177
+ result = client.cron_show(job_id)
178
+ except RuntimeError as exc:
179
+ typer.echo(f"Error: {exc}", err=True)
180
+ sys.exit(1)
181
+
182
+ job = result.get("job")
183
+ if not job:
184
+ typer.echo(f"Error: Job '{job_id}' not found.", err=True)
185
+ sys.exit(1)
186
+
187
+ details = [
188
+ f"[bold]ID:[/bold] {job.get('id', '?')}",
189
+ f"[bold]Description:[/bold] {job.get('description', '-')}",
190
+ f"[bold]Status:[/bold] [green]{job.get('status', 'pending')}[/green]",
191
+ f"[bold]Priority:[/bold] {job.get('priority', 50)}",
192
+ f"[bold]Schedule:[/bold] {job.get('schedule_kind', 'once')} = {job.get('schedule_value', '')}",
193
+ ]
194
+ if job.get("end_condition"):
195
+ details.append(f"[bold]End Condition:[/bold] {job['end_condition']}")
196
+ next_run = job.get("next_run", "")
197
+ if next_run:
198
+ details.append(f"[bold]Next Run:[/bold] [yellow]{next_run[:19]}[/yellow]")
199
+ last_run = job.get("last_run")
200
+ if last_run:
201
+ details.append(f"[bold]Last Run:[/bold] {last_run[:19]}")
202
+ details.append(f"[bold]Run Count:[/bold] {job.get('run_count', 0)}")
203
+ created = job.get("created_at", "")
204
+ if created:
205
+ details.append(f"[bold]Created:[/bold] [dim]{created[:19]}[/dim]")
206
+
207
+ console.print(Panel("\n".join(details), title=f"Cron Job: {job_id[:12]}", border_style="cyan"))
208
+
209
+
210
+ @app.command("cancel")
211
+ def cancel_job(
212
+ job_id: Annotated[str, typer.Argument(help="Job ID to cancel.")],
213
+ ) -> None:
214
+ """Cancel a scheduled cron job.
215
+
216
+ Marks the job as cancelled. Only pending jobs can be cancelled.
217
+
218
+ Example:
219
+ soothe cron cancel abc123def456
220
+ """
221
+ client = _require_cron_client()
222
+ try:
223
+ result = client.cron_cancel(job_id)
224
+ except RuntimeError as exc:
225
+ typer.echo(f"Error: {exc}", err=True)
226
+ sys.exit(1)
227
+
228
+ if result.get("cancelled"):
229
+ console.print(f"[success]Cancelled job: {job_id[:12]}[/success]")
230
+ else:
231
+ typer.echo(f"Error: Could not cancel job '{job_id}'.", err=True)
232
+ sys.exit(1)
233
+
234
+
235
+ __all__ = [
236
+ "app",
237
+ "add_job",
238
+ "list_jobs",
239
+ "show_job",
240
+ "cancel_job",
241
+ ]
@@ -83,12 +83,14 @@ def _render_unified_status_table(
83
83
  daemon_pid: int | None = None,
84
84
  ready_state: dict[str, Any] | None = None,
85
85
  daemon_live: bool = True,
86
+ daemon_version: str | None = None,
87
+ core_version: str | None = None,
86
88
  ) -> Table:
87
89
  """Render unified status table without duplicated info.
88
90
 
89
91
  Sections:
90
92
  - Connection: WebSocket URL, Soothe Home
91
- - Daemon: Running, Threads, PID (only when daemon is live)
93
+ - Daemon: Running, Threads, PID, Versions (only when daemon is live)
92
94
  """
93
95
  table = Table(title="Soothe Status")
94
96
  table.add_column("Section", style="dim", width=12)
@@ -109,6 +111,10 @@ def _render_unified_status_table(
109
111
  table.add_row("", "PID", str(daemon_pid))
110
112
  if active_threads is not None:
111
113
  table.add_row("", "Active Threads", str(active_threads))
114
+ if daemon_version:
115
+ table.add_row("", "Daemon Version", daemon_version)
116
+ if core_version:
117
+ table.add_row("", "Core Version", core_version)
112
118
 
113
119
  if ready_state:
114
120
  state = ready_state.get("state", "unknown")
@@ -207,6 +213,8 @@ def daemon_status(
207
213
  "port_live": status.get("port_live", True),
208
214
  "active_threads": status.get("active_threads", 0),
209
215
  "daemon_pid": status.get("daemon_pid"),
216
+ "daemon_version": status.get("daemon_version"),
217
+ "core_version": status.get("core_version"),
210
218
  }
211
219
  if ready_state:
212
220
  output["readiness_state"] = ready_state.get("state", "unknown")
@@ -219,9 +227,20 @@ def daemon_status(
219
227
  port_live = status.get("port_live", True)
220
228
  active_threads = status.get("active_threads", 0)
221
229
  daemon_pid = status.get("daemon_pid")
230
+ daemon_version = status.get("daemon_version")
231
+ core_version = status.get("core_version")
222
232
 
223
233
  table = _render_unified_status_table(
224
- config, ws_url, running, port_live, active_threads, daemon_pid, ready_state
234
+ config,
235
+ ws_url,
236
+ running,
237
+ port_live,
238
+ active_threads,
239
+ daemon_pid,
240
+ ready_state,
241
+ daemon_live=True,
242
+ daemon_version=daemon_version,
243
+ core_version=core_version,
225
244
  )
226
245
  console.print(table)
227
246
 
@@ -311,6 +330,8 @@ def status_main(
311
330
  output["daemon"]["port_live"] = status.get("port_live", True)
312
331
  output["daemon"]["active_threads"] = status.get("active_threads", 0)
313
332
  output["daemon"]["daemon_pid"] = status.get("daemon_pid")
333
+ output["daemon"]["daemon_version"] = status.get("daemon_version")
334
+ output["daemon"]["core_version"] = status.get("core_version")
314
335
  console.print_json(json.dumps(output))
315
336
  return
316
337
 
@@ -338,9 +359,19 @@ def status_main(
338
359
  port_live = status.get("port_live", True)
339
360
  active_threads = status.get("active_threads", 0)
340
361
  daemon_pid = status.get("daemon_pid")
362
+ daemon_version = status.get("daemon_version")
363
+ core_version = status.get("core_version")
341
364
 
342
365
  table = _render_unified_status_table(
343
- config, ws_url, running, port_live, active_threads, daemon_pid
366
+ config,
367
+ ws_url,
368
+ running,
369
+ port_live,
370
+ active_threads,
371
+ daemon_pid,
372
+ daemon_live=True,
373
+ daemon_version=daemon_version,
374
+ core_version=core_version,
344
375
  )
345
376
  console.print(table)
346
377
 
@@ -15,6 +15,7 @@ from typing import Any
15
15
  import typer
16
16
  from soothe_sdk.client import (
17
17
  WebSocketClient,
18
+ async_ws_command_client_from_config,
18
19
  bootstrap_loop_session,
19
20
  connect_websocket_with_retries,
20
21
  websocket_url_from_config,
@@ -59,6 +60,22 @@ async def _send_cancel_to_daemon(client: WebSocketClient) -> None:
59
60
  logger.warning("Failed to send /cancel to daemon", exc_info=True)
60
61
 
61
62
 
63
+ def _parse_cron_slash_prompt(prompt: str) -> str | None:
64
+ """Return natural-language cron text if ``prompt`` is a ``/cron`` slash command.
65
+
66
+ Args:
67
+ prompt: User input (e.g. ``/cron in 1 hour remind me to deploy``).
68
+
69
+ Returns:
70
+ Text after ``/cron``, or ``None`` if not a cron slash command.
71
+ """
72
+ stripped = prompt.strip()
73
+ if not stripped.lower().startswith("/cron"):
74
+ return None
75
+ rest = stripped[len("/cron") :].strip()
76
+ return rest
77
+
78
+
62
79
  async def _run_headless_session_once(
63
80
  cfg: Any,
64
81
  prompt: str,
@@ -79,6 +96,24 @@ async def _run_headless_session_once(
79
96
  sigint_count = 0
80
97
  original_sigint: Any = None
81
98
 
99
+ cron_text = _parse_cron_slash_prompt(prompt)
100
+ if cron_text is not None:
101
+ if not cron_text.strip():
102
+ _emit_headless_error("Usage: /cron <natural language schedule>")
103
+ return 1, False
104
+ try:
105
+ ws_client = async_ws_command_client_from_config(cfg)
106
+ result = await ws_client.cron_add(cron_text)
107
+ except RuntimeError as exc:
108
+ _emit_headless_error(str(exc))
109
+ return 1, False
110
+ job = result.get("job") or {}
111
+ job_id = job.get("id", "?")
112
+ typer.echo(f"Scheduled cron job: {job_id}")
113
+ typer.echo(f" Description: {job.get('description', cron_text)}")
114
+ typer.echo(f" Next run: {str(job.get('next_run', ''))[:19]}")
115
+ return 0, False
116
+
82
117
  try:
83
118
  await connect_websocket_with_retries(client)
84
119
  cli_ws = os.environ.get("SOOTHE_CLI_WORKSPACE", "").strip() or os.getcwd()
@@ -201,6 +201,7 @@ def main(
201
201
  # ---------------------------------------------------------------------------
202
202
 
203
203
  from soothe_cli.cli.commands.autopilot_cmd import app as _autopilot_app # noqa: E402
204
+ from soothe_cli.cli.commands.cron_cmd import app as _cron_app # noqa: E402
204
205
  from soothe_cli.cli.commands.loop_cmd import loop_app as _loop_app # noqa: E402
205
206
  from soothe_cli.cli.commands.status_cmd import status_app as _status_app # noqa: E402
206
207
 
@@ -208,6 +209,7 @@ from soothe_cli.cli.commands.status_cmd import status_app as _status_app # noqa
208
209
  for _sub_app, _name in (
209
210
  (_loop_app, "loop"),
210
211
  (_autopilot_app, "autopilot"),
212
+ (_cron_app, "cron"),
211
213
  ):
212
214
  add_help_alias(_sub_app)
213
215
  app.add_typer(_sub_app, name=_name)
@@ -387,7 +387,7 @@ class EventProcessor:
387
387
  self._handle_clear_event(event)
388
388
 
389
389
  def _handle_command_response(self, event: dict[str, Any]) -> None:
390
- """Handle command response from daemon (RFC-404)."""
390
+ """Handle command response from daemon (RFC-454)."""
391
391
  command = event.get("command")
392
392
  data = event.get("data")
393
393
  error = event.get("error")
@@ -461,7 +461,7 @@ class _ExecutionMixin:
461
461
  )
462
462
  from soothe_cli.tui.commands.slash_commands import COMMANDS as _RFC404_COMMANDS
463
463
 
464
- # RFC-404 daemon *routing* commands (/research, /explore, /plan, optional /«id»):
464
+ # RFC-454 daemon *routing* commands (/research, /explore, /plan, optional /«id»):
465
465
  # send the full line as a normal user turn so ``parse_subagent_from_input``
466
466
  # runs in the daemon adapter (same as headless CLI). Without this branch,
467
467
  # ``cmd == "/research …"`` never matches the bare single-token handlers below.
@@ -493,7 +493,8 @@ class _ExecutionMixin:
493
493
  elif cmd == "/help":
494
494
  await self._mount_message(UserMessage(command))
495
495
  help_body = (
496
- "Commands: /quit, /clear, /editor, /autopilot <task>, /autopilot-dashboard, /mcp, "
496
+ "Commands: /quit, /clear, /editor, /autopilot <task>, /cron <schedule>, "
497
+ "/autopilot-dashboard, /mcp, "
497
498
  "/model [--model-params JSON] [--default], /notifications, "
498
499
  "/reload, /skill:<name>, /theme, "
499
500
  "/tokens, /resume, "
@@ -649,6 +650,18 @@ class _ExecutionMixin:
649
650
  )
650
651
  return
651
652
  await self._submit_autopilot_job(args)
653
+ elif cmd == "/cron" or cmd.startswith("/cron "):
654
+ args = command.strip()[len("/cron") :].strip()
655
+ if not args:
656
+ await self._mount_message(UserMessage(command))
657
+ await self._mount_message(
658
+ AppMessage(
659
+ "Usage: /cron <natural language schedule>\n"
660
+ "Example: /cron in 1 hour remind me to check the deploy"
661
+ )
662
+ )
663
+ return
664
+ await self._submit_cron_job(args, slash_input=command)
652
665
  elif cmd == "/autopilot-dashboard":
653
666
  await self._show_autopilot_dashboard()
654
667
  elif cmd == "/autopilot-toggle":