soothe-cli 0.6.12__tar.gz → 0.6.14__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.12 → soothe_cli-0.6.14}/PKG-INFO +1 -1
  2. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/autopilot_cmd.py +34 -42
  3. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/cron_cmd.py +90 -102
  4. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/daemon.py +35 -0
  5. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_execution.py +14 -1
  6. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_model.py +59 -8
  7. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/textual_adapter.py +23 -0
  8. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/messages.py +70 -18
  9. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/.gitignore +0 -0
  10. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/README.md +0 -0
  11. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/pyproject.toml +0 -0
  12. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/__init__.py +0 -0
  13. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/__init__.py +0 -0
  14. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/__init__.py +0 -0
  15. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
  16. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  17. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  18. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/__init__.py +0 -0
  19. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
  20. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/headless.py +0 -0
  21. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
  22. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/launcher.py +0 -0
  23. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/main.py +0 -0
  24. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/config/__init__.py +0 -0
  25. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/config/cli_config.py +0 -0
  26. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/config/loader.py +0 -0
  27. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/config/logging_setup.py +0 -0
  28. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/__init__.py +0 -0
  29. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/headless/processor.py +0 -0
  30. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
  31. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/_utils.py +0 -0
  32. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
  33. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
  34. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
  35. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
  36. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
  37. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
  38. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
  39. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
  40. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/engine.py +0 -0
  41. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
  42. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
  43. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
  44. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
  45. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/session_stats.py +0 -0
  46. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/step_router.py +0 -0
  47. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
  48. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/transcript.py +0 -0
  49. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/transport/session.py +0 -0
  50. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
  51. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/turn/prepare.py +0 -0
  52. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
  53. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/wire/display_text.py +0 -0
  54. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/wire/message_text.py +0 -0
  55. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/wire/messages.py +0 -0
  56. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/__init__.py +0 -0
  57. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/_cli_context.py +0 -0
  58. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/_env_vars.py +0 -0
  59. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/_version.py +0 -0
  60. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/__init__.py +0 -0
  61. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_app.py +0 -0
  62. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_commands.py +0 -0
  63. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_history.py +0 -0
  64. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
  65. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_module_init.py +0 -0
  66. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_startup.py +0 -0
  67. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_ui.py +0 -0
  68. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/app.tcss +0 -0
  69. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/binding.py +0 -0
  70. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/command_registry.py +0 -0
  71. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/commands/__init__.py +0 -0
  72. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/commands/command_router.py +0 -0
  73. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
  74. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
  75. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/config.py +0 -0
  76. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/file_change_notify.py +0 -0
  77. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/file_change_renderers.py +0 -0
  78. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/hooks.py +0 -0
  79. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/input.py +0 -0
  80. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/media_utils.py +0 -0
  81. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/model_config.py +0 -0
  82. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/path_utils.py +0 -0
  83. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/preview_limits.py +0 -0
  84. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/project_utils.py +0 -0
  85. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/sessions.py +0 -0
  86. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/skills/__init__.py +0 -0
  87. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/skills/invocation.py +0 -0
  88. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/skills/load.py +0 -0
  89. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/theme.py +0 -0
  90. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/tips.py +0 -0
  91. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/tool_display.py +0 -0
  92. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/unicode_security.py +0 -0
  93. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/update_check.py +0 -0
  94. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  95. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/_links.py +0 -0
  96. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  97. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  98. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  99. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  100. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  101. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/diff.py +0 -0
  102. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/editor.py +0 -0
  103. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
  104. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/history.py +0 -0
  105. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/loading.py +0 -0
  106. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
  107. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  108. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  109. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  110. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  111. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/status.py +0 -0
  112. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  113. {soothe_cli-0.6.12 → soothe_cli-0.6.14}/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.12
3
+ Version: 0.6.14
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
 
@@ -1,9 +1,10 @@
1
1
  """Cron CLI subcommands for RFC-229.
2
2
 
3
- Daemon-backed control surface: manage scheduled jobs via HTTP REST.
3
+ Daemon-backed control surface: manage scheduled jobs via WebSocket.
4
4
  Requires ``soothed start``.
5
5
 
6
6
  Commands:
7
+ soothe cron add <text> # Add scheduled job (natural language)
7
8
  soothe cron list # List scheduled jobs
8
9
  soothe cron show <job_id> # Show job details
9
10
  soothe cron cancel <job_id> # Cancel a job
@@ -23,6 +24,7 @@ from rich.table import Table
23
24
  from soothe_sdk.client import (
24
25
  is_daemon_live,
25
26
  websocket_url_from_config,
27
+ ws_command_client_from_config,
26
28
  )
27
29
 
28
30
  from soothe_cli.runtime import load_config
@@ -32,8 +34,8 @@ console = Console()
32
34
  app = typer.Typer(help="Manage scheduled cron jobs — natural language scheduled tasks.")
33
35
 
34
36
 
35
- def _require_daemon_http() -> tuple[str, str]:
36
- """Return WebSocket URL and HTTP base URL, or exit if daemon not running."""
37
+ def _require_cron_client():
38
+ """Return a live WebSocket command client or exit."""
37
39
  cfg = load_config()
38
40
  ws_url = websocket_url_from_config(cfg)
39
41
  if not asyncio.run(is_daemon_live(ws_url, timeout=5.0)):
@@ -42,50 +44,68 @@ def _require_daemon_http() -> tuple[str, str]:
42
44
  err=True,
43
45
  )
44
46
  sys.exit(1)
47
+ return ws_command_client_from_config(cfg)
45
48
 
46
- # Derive HTTP URL from WebSocket URL
47
- if ws_url.startswith("wss://"):
48
- http_url = "https://" + ws_url[len("wss://") :]
49
- elif ws_url.startswith("ws://"):
50
- http_url = "http://" + ws_url[len("ws://") :]
51
- else:
52
- http_url = ws_url
53
-
54
- return ws_url, http_url
55
-
56
-
57
- def _http_request(
58
- base_url: str,
59
- method: str,
60
- path: str,
61
- body: dict | None = None,
62
- timeout: float = 30.0,
63
- ) -> dict:
64
- """Make HTTP request to daemon REST API."""
65
- import json
66
- import urllib.error
67
- import urllib.request
68
-
69
- url = f"{base_url.rstrip('/')}{path}"
70
- data = None
71
- headers = {"Accept": "application/json"}
72
- if body is not None:
73
- data = json.dumps(body).encode("utf-8")
74
- headers["Content-Type"] = "application/json"
75
-
76
- req = urllib.request.Request(url, data=data, headers=headers, method=method)
49
+
50
+ def _format_next_run(next_run: str) -> str:
51
+ if not next_run:
52
+ return "-"
77
53
  try:
78
- with urllib.request.urlopen(req, timeout=timeout) as resp:
79
- raw = resp.read().decode("utf-8")
80
- return json.loads(raw) if raw else {}
81
- except urllib.error.HTTPError as exc:
82
- detail = exc.read().decode("utf-8", errors="replace")
83
- typer.echo(f"Error: HTTP {exc.code} - {detail}", err=True)
84
- sys.exit(1)
85
- except urllib.error.URLError as exc:
86
- typer.echo(f"Error: Cannot reach daemon - {exc.reason}", err=True)
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)
87
89
  sys.exit(1)
88
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
+
89
109
 
90
110
  @app.command("list")
91
111
  def list_jobs(
@@ -106,18 +126,14 @@ def list_jobs(
106
126
  soothe cron list
107
127
  soothe cron list --status pending
108
128
  """
109
- ws_url, http_url = _require_daemon_http()
110
-
111
- params = {}
112
- if status:
113
- params["status"] = status
114
-
115
- query = "&".join(f"{k}={v}" for k, v in params.items())
116
- path = f"/api/v1/cron/jobs?{query}" if query else "/api/v1/cron/jobs"
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)
117
135
 
118
- result = _http_request(http_url, "GET", path)
119
136
  jobs = result.get("jobs", [])
120
-
121
137
  if not jobs:
122
138
  console.print("[info]No scheduled jobs found.[/info]")
123
139
  return
@@ -133,35 +149,11 @@ def list_jobs(
133
149
  desc = job.get("description", "")
134
150
  if len(desc) > 40:
135
151
  desc = desc[:37] + "..."
136
-
137
- next_run = job.get("next_run", "")
138
- if next_run:
139
- # Parse ISO datetime and format relative
140
- try:
141
- dt = datetime.fromisoformat(next_run.replace("Z", "+00:00"))
142
- now = datetime.now(dt.tzinfo)
143
- delta = dt - now
144
- if delta.total_seconds() < 0:
145
- next_run_str = "due now"
146
- elif delta.total_seconds() < 3600:
147
- mins = int(delta.total_seconds() / 60)
148
- next_run_str = f"in {mins}m"
149
- elif delta.total_seconds() < 86400:
150
- hrs = int(delta.total_seconds() / 3600)
151
- next_run_str = f"in {hrs}h"
152
- else:
153
- days = int(delta.total_seconds() / 86400)
154
- next_run_str = f"in {days}d"
155
- except ValueError:
156
- next_run_str = next_run[:19]
157
- else:
158
- next_run_str = "-"
159
-
160
152
  table.add_row(
161
153
  job.get("id", "?")[:12],
162
154
  desc,
163
155
  job.get("status", "pending"),
164
- next_run_str,
156
+ _format_next_run(job.get("next_run", "")),
165
157
  str(job.get("run_count", 0)),
166
158
  )
167
159
 
@@ -180,42 +172,34 @@ def show_job(
180
172
  Example:
181
173
  soothe cron show abc123def456
182
174
  """
183
- ws_url, http_url = _require_daemon_http()
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)
184
181
 
185
- result = _http_request(http_url, "GET", f"/api/v1/cron/jobs/{job_id}")
186
182
  job = result.get("job")
187
-
188
183
  if not job:
189
184
  typer.echo(f"Error: Job '{job_id}' not found.", err=True)
190
185
  sys.exit(1)
191
186
 
192
- # Build details panel
193
- details = []
194
- details.append(f"[bold]ID:[/bold] {job.get('id', '?')}")
195
- details.append(f"[bold]Description:[/bold] {job.get('description', '-')}")
196
- details.append(f"[bold]Status:[/bold] [green]{job.get('status', 'pending')}[/green]")
197
- details.append(f"[bold]Priority:[/bold] {job.get('priority', 50)}")
198
-
199
- # Schedule info
200
- schedule_kind = job.get("schedule_kind", "once")
201
- schedule_value = job.get("schedule_value", "")
202
- details.append(f"[bold]Schedule:[/bold] {schedule_kind} = {schedule_value}")
203
-
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
+ ]
204
194
  if job.get("end_condition"):
205
195
  details.append(f"[bold]End Condition:[/bold] {job['end_condition']}")
206
-
207
- # Timestamps
208
196
  next_run = job.get("next_run", "")
209
197
  if next_run:
210
198
  details.append(f"[bold]Next Run:[/bold] [yellow]{next_run[:19]}[/yellow]")
211
-
212
199
  last_run = job.get("last_run")
213
200
  if last_run:
214
201
  details.append(f"[bold]Last Run:[/bold] {last_run[:19]}")
215
-
216
202
  details.append(f"[bold]Run Count:[/bold] {job.get('run_count', 0)}")
217
-
218
- # Created/Updated
219
203
  created = job.get("created_at", "")
220
204
  if created:
221
205
  details.append(f"[bold]Created:[/bold] [dim]{created[:19]}[/dim]")
@@ -234,9 +218,12 @@ def cancel_job(
234
218
  Example:
235
219
  soothe cron cancel abc123def456
236
220
  """
237
- ws_url, http_url = _require_daemon_http()
238
-
239
- result = _http_request(http_url, "DELETE", f"/api/v1/cron/jobs/{job_id}")
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)
240
227
 
241
228
  if result.get("cancelled"):
242
229
  console.print(f"[success]Cancelled job: {job_id[:12]}[/success]")
@@ -247,6 +234,7 @@ def cancel_job(
247
234
 
248
235
  __all__ = [
249
236
  "app",
237
+ "add_job",
250
238
  "list_jobs",
251
239
  "show_job",
252
240
  "cancel_job",
@@ -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()
@@ -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":