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.
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/PKG-INFO +1 -1
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/autopilot_cmd.py +34 -42
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/cron_cmd.py +90 -102
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/daemon.py +35 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_execution.py +14 -1
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_model.py +59 -8
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/textual_adapter.py +23 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/messages.py +70 -18
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/.gitignore +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/README.md +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/pyproject.toml +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/loop_cmd.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/daemon_errors.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/headless.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/headless_renderer.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/execution/launcher.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/cli/main.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/config/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/config/cli_config.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/config/loader.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/config/logging_setup.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/headless/processor.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/headless/processor_state.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/_utils.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/message_processing.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/tool_call_resolution.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/tool_message_format.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/parse/tool_result.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/policy/display_policy.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/policy/essential_events.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/policy/tui_trace_log.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/duration_format.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/engine.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/explore_task_display.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/renderer_base.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/presentation/renderer_protocol.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/file_tracker.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/session_stats.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/step_router.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/stream_accumulator.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/state/transcript.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/transport/session.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/turn/pipeline.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/turn/prepare.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/wire/chunk_filter.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/wire/display_text.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/wire/message_text.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/runtime/wire/messages.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/_cli_context.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/_env_vars.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/_version.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_app.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_commands.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_history.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_messages_mixin.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_module_init.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_startup.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/_ui.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/app/app.tcss +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/binding.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/command_registry.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/commands/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/commands/command_router.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/commands/slash_commands.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/commands/subagent_routing.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/config.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/file_change_notify.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/file_change_renderers.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/hooks.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/input.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/media_utils.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/model_config.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/path_utils.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/preview_limits.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/project_utils.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/sessions.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/skills/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/skills/invocation.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/skills/load.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/theme.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/tips.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/tool_display.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/unicode_security.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/update_check.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/__init__.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/_links.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/diff.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/editor.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/file_change_preview.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/history.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/loading.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/loop_selector.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/message_store.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/status.py +0 -0
- {soothe_cli-0.6.12 → soothe_cli-0.6.14}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
- {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
|
"""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
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Cron CLI subcommands for RFC-229.
|
|
2
2
|
|
|
3
|
-
Daemon-backed control surface: manage scheduled jobs via
|
|
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
|
|
36
|
-
"""Return
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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>, /
|
|
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":
|