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.
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/PKG-INFO +1 -1
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/autopilot_cmd.py +34 -42
- soothe_cli-0.6.13/src/soothe_cli/cli/commands/cron_cmd.py +241 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/status_cmd.py +34 -3
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/daemon.py +35 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/main.py +2 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/headless/processor.py +1 -1
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_execution.py +15 -2
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_model.py +59 -8
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/command_registry.py +6 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/commands/command_router.py +4 -4
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/commands/slash_commands.py +2 -2
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/theme.py +6 -6
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/messages.py +64 -72
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/.gitignore +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/README.md +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/pyproject.toml +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/_utils.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_startup.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/textual_adapter.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.6.11 → soothe_cli-0.6.13}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {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
|
"""Autopilot CLI subcommands for RFC-204.
|
|
2
2
|
|
|
3
|
-
Daemon-backed control surface: submit tasks and manage goals via
|
|
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
|
|
40
|
-
"""Return a live
|
|
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
|
-
|
|
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 =
|
|
74
|
+
client = _require_daemon_ws()
|
|
83
75
|
submit_workspace = _resolve_submit_workspace(workspace)
|
|
84
|
-
result = client.
|
|
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.
|
|
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 =
|
|
109
|
+
client = _require_daemon_ws()
|
|
118
110
|
submit_workspace = _resolve_submit_workspace(workspace)
|
|
119
|
-
result = client.
|
|
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 =
|
|
130
|
-
data = client.
|
|
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.
|
|
143
|
-
goals = client.
|
|
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 =
|
|
186
|
-
payload = client.
|
|
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 =
|
|
208
|
-
payload = client.
|
|
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 =
|
|
265
|
+
client = _require_daemon_ws()
|
|
274
266
|
try:
|
|
275
|
-
payload = client.
|
|
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 =
|
|
318
|
-
payload = client.
|
|
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 =
|
|
340
|
-
result = client.
|
|
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 =
|
|
343
|
+
client = _require_daemon_ws()
|
|
352
344
|
try:
|
|
353
|
-
result = client.
|
|
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 =
|
|
357
|
+
client = _require_daemon_ws()
|
|
366
358
|
try:
|
|
367
|
-
result = client.
|
|
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 =
|
|
375
|
+
client = _require_daemon_ws()
|
|
384
376
|
try:
|
|
385
|
-
result = client.
|
|
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 =
|
|
398
|
-
client.
|
|
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 =
|
|
406
|
-
client.
|
|
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,
|
|
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,
|
|
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-
|
|
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-
|
|
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>, /
|
|
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":
|