soothe-cli 0.3.6__tar.gz → 0.3.7__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.3.6 → soothe_cli-0.3.7}/PKG-INFO +4 -4
  2. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/README.md +3 -3
  3. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/commands/config_cmd.py +1 -1
  4. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/commands/thread_cmd.py +23 -282
  5. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/main.py +35 -132
  6. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/stream/formatter.py +7 -5
  7. soothe_cli-0.3.7/src/soothe_cli/loop_commands.py +869 -0
  8. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/app.py +170 -70
  9. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/command_registry.py +2 -2
  10. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/config.py +2 -2
  11. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/model_config.py +66 -0
  12. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/sessions.py +79 -108
  13. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/textual_adapter.py +7 -7
  14. soothe_cli-0.3.7/src/soothe_cli/tui/widgets/loop_selector.py +1550 -0
  15. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/messages.py +10 -42
  16. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/thread_selector.py +5 -6
  17. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/welcome.py +12 -10
  18. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/.gitignore +0 -0
  19. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/pyproject.toml +0 -0
  20. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/__init__.py +0 -0
  21. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/__init__.py +0 -0
  22. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/commands/__init__.py +0 -0
  23. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/commands/autopilot_cmd.py +0 -0
  24. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/commands/run_cmd.py +0 -0
  25. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/commands/status_cmd.py +0 -0
  26. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/commands/subagent_names.py +0 -0
  27. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/execution/__init__.py +0 -0
  28. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/execution/daemon.py +0 -0
  29. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/execution/headless.py +0 -0
  30. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/execution/launcher.py +0 -0
  31. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/renderer.py +0 -0
  32. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/stream/__init__.py +0 -0
  33. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/stream/context.py +0 -0
  34. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/stream/display_line.py +0 -0
  35. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/stream/pipeline.py +0 -0
  36. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/cli/utils.py +0 -0
  37. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/config/__init__.py +0 -0
  38. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/config/cli_config.py +0 -0
  39. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/plan/__init__.py +0 -0
  40. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/plan/rich_tree.py +0 -0
  41. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/__init__.py +0 -0
  42. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/command_router.py +0 -0
  43. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/config_loader.py +0 -0
  44. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/display_policy.py +0 -0
  45. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/essential_events.py +0 -0
  46. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/event_processor.py +0 -0
  47. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/message_processing.py +0 -0
  48. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/presentation_engine.py +0 -0
  49. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/processor_state.py +0 -0
  50. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/renderer_protocol.py +0 -0
  51. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/rendering.py +0 -0
  52. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/slash_commands.py +0 -0
  53. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/subagent_routing.py +0 -0
  54. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/suppression_state.py +0 -0
  55. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_call_resolution.py +0 -0
  56. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_card_payload.py +0 -0
  57. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_formatters/__init__.py +0 -0
  58. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_formatters/base.py +0 -0
  59. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_formatters/execution.py +0 -0
  60. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_formatters/fallback.py +0 -0
  61. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_formatters/file_ops.py +0 -0
  62. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_formatters/goal_formatter.py +0 -0
  63. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_formatters/media.py +0 -0
  64. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_formatters/structured.py +0 -0
  65. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_formatters/web.py +0 -0
  66. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_message_format.py +0 -0
  67. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tool_output_formatter.py +0 -0
  68. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/shared/tui_trace_log.py +0 -0
  69. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/__init__.py +0 -0
  70. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/_ask_user_types.py +0 -0
  71. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/_cli_context.py +0 -0
  72. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/_env_vars.py +0 -0
  73. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/_session_stats.py +0 -0
  74. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/_version.py +0 -0
  75. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/app.tcss +0 -0
  76. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/daemon_session.py +0 -0
  77. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/file_ops.py +0 -0
  78. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/formatting.py +0 -0
  79. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/hooks.py +0 -0
  80. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/input.py +0 -0
  81. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/media_utils.py +0 -0
  82. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/message_display_filter.py +0 -0
  83. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/output.py +0 -0
  84. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/project_utils.py +0 -0
  85. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/skills/__init__.py +0 -0
  86. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/skills/invocation.py +0 -0
  87. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/skills/load.py +0 -0
  88. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/theme.py +0 -0
  89. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/tool_display.py +0 -0
  90. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/unicode_security.py +0 -0
  91. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/update_check.py +0 -0
  92. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/__init__.py +0 -0
  93. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/_links.py +0 -0
  94. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/approval.py +0 -0
  95. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/ask_user.py +0 -0
  96. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/autocomplete.py +0 -0
  97. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/autopilot_dashboard.py +0 -0
  98. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/autopilot_screen.py +0 -0
  99. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/chat_input.py +0 -0
  100. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/clipboard.py +0 -0
  101. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/diff.py +0 -0
  102. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/editor.py +0 -0
  103. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/history.py +0 -0
  104. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/loading.py +0 -0
  105. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/mcp_viewer.py +0 -0
  106. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/message_store.py +0 -0
  107. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/model_selector.py +0 -0
  108. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/notification_settings.py +0 -0
  109. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/status.py +0 -0
  110. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/theme_selector.py +0 -0
  111. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/tool_renderers.py +0 -0
  112. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/tool_widgets.py +0 -0
  113. {soothe_cli-0.3.6 → soothe_cli-0.3.7}/src/soothe_cli/tui/widgets/tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soothe-cli
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Soothe CLI client - communicates with daemon via WebSocket
5
5
  Project-URL: Homepage, https://github.com/OpenSoothe/soothe
6
6
  Project-URL: Documentation, https://soothe.readthedocs.io
@@ -54,9 +54,9 @@ soothe
54
54
  # Headless single-prompt mode
55
55
  soothe -p "Research AI advances"
56
56
 
57
- # Thread management
58
- soothe thread list
59
- soothe thread continue abc123
57
+ # Loop management
58
+ soothe loop list
59
+ soothe loop continue loop_abc123
60
60
 
61
61
  # Configuration
62
62
  soothe config show
@@ -19,9 +19,9 @@ soothe
19
19
  # Headless single-prompt mode
20
20
  soothe -p "Research AI advances"
21
21
 
22
- # Thread management
23
- soothe thread list
24
- soothe thread continue abc123
22
+ # Loop management
23
+ soothe loop list
24
+ soothe loop continue loop_abc123
25
25
 
26
26
  # Configuration
27
27
  soothe config show
@@ -189,7 +189,7 @@ def config_init(
189
189
  target.write_text("# Soothe configuration\n# See docs/user_guide.md for options\n")
190
190
  typer.echo(f"Created minimal {target}")
191
191
 
192
- for subdir in ("runs", "generated_agents", "logs", "data"):
192
+ for subdir in ("generated_agents", "logs", "data"):
193
193
  (home / subdir).mkdir(parents=True, exist_ok=True)
194
194
 
195
195
  # Migrate runtime data files from root to data/ subdirectory
@@ -1,18 +1,21 @@
1
- """Thread commands for Soothe CLI.
1
+ """Thread commands for Soothe CLI (read-only diagnostics).
2
2
 
3
3
  All thread operations communicate exclusively via daemon WebSocket RPC.
4
4
  The daemon must be running for thread commands to work.
5
+
6
+ Note: Thread commands are read-only diagnostics per RFC-503 (Loop-First UX).
7
+ Users manage loops (primary entity), not threads (internal execution contexts).
8
+ For thread lifecycle management, use loop commands: soothe loop <subcommand>
5
9
  """
6
10
 
7
11
  import asyncio
12
+ import json
8
13
  import sys
9
14
  from pathlib import Path
10
15
  from typing import Annotated, Any
11
16
 
12
17
  import typer
13
18
  from soothe_sdk.client import WebSocketClient, is_daemon_live, websocket_url_from_config
14
- from soothe_sdk.client.config import SOOTHE_HOME
15
- from soothe_sdk.utils.logging import resolve_cli_log_level
16
19
 
17
20
  from soothe_cli.shared import load_config
18
21
 
@@ -92,7 +95,7 @@ def _echo_thread_table(rows: list[dict[str, object]]) -> None:
92
95
  typer.echo("No threads.")
93
96
  return
94
97
  typer.echo(f"{'ID':<20} {'Status':<10} {'Created':<19} {'Last Message':<19} {'Topic':<30}")
95
- typer.echo("\u2500" * 104)
98
+ typer.echo("" * 104)
96
99
  for raw in rows:
97
100
  tid_raw = str(raw.get("thread_id", ""))
98
101
  tid = (
@@ -127,13 +130,15 @@ def thread_list(
127
130
  typer.Option("--limit", "-l", help="Limit number of threads shown."),
128
131
  ] = None,
129
132
  ) -> None:
130
- """List all agent threads.
133
+ """List all agent threads (read-only diagnostics).
131
134
 
132
135
  Examples:
133
136
  soothe thread list
134
137
  soothe thread list --status active
135
138
  soothe thread list --limit 10
136
139
  soothe thread list --limit 20 --status idle
140
+
141
+ Note: For thread lifecycle management, use loop commands (RFC-503).
137
142
  """
138
143
  cfg = load_config(config)
139
144
  ws_url = websocket_url_from_config(cfg)
@@ -181,105 +186,6 @@ def thread_list(
181
186
  asyncio.run(_list())
182
187
 
183
188
 
184
- def thread_continue(
185
- thread_id: Annotated[
186
- str | None,
187
- typer.Argument(help="Thread ID to continue. Omit to continue last active thread."),
188
- ] = None,
189
- config: Annotated[
190
- str | None,
191
- typer.Option("--config", "-c", help="Path to configuration file."),
192
- ] = None,
193
- *,
194
- new: Annotated[
195
- bool,
196
- typer.Option("--new", help="Create a new thread instead of continuing."),
197
- ] = False,
198
- ) -> None:
199
- """Continue a conversation thread in the TUI.
200
-
201
- Requires a running daemon. Start daemon with 'soothe daemon start' first.
202
-
203
- Examples:
204
- soothe thread continue abc123
205
- soothe thread continue --new
206
- soothe thread continue
207
- """
208
- from soothe_cli.cli.execution import run_tui
209
- from soothe_cli.shared import setup_logging
210
-
211
- cfg = load_config(config)
212
- log_level = resolve_cli_log_level(
213
- cfg.logging.verbosity,
214
- logging_level=cfg.logging.level,
215
- )
216
- log_file = Path(SOOTHE_HOME) / "logs" / "soothe-cli.log"
217
- setup_logging(log_level, log_file=log_file)
218
- ws_url = websocket_url_from_config(cfg)
219
- _require_daemon(ws_url)
220
-
221
- # Handle --new flag
222
- if new:
223
- thread_id = None
224
- elif not thread_id:
225
- # Find the most recently updated active thread through the daemon
226
- async def get_last_thread_via_daemon() -> str | None:
227
- client = WebSocketClient(url=ws_url)
228
- try:
229
- await client.connect()
230
- await client.send_thread_list()
231
- while True:
232
- event = await client.read_event()
233
- if not event:
234
- break
235
- if event.get("type") != "thread_list_response":
236
- continue
237
- threads = event.get("threads", [])
238
- active_threads = [t for t in threads if t.get("status") in ("active", "idle")]
239
- if not active_threads:
240
- typer.echo("No active threads found.", err=True)
241
- sys.exit(1)
242
- active_threads.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
243
- return active_threads[0].get("thread_id")
244
- finally:
245
- await client.close()
246
-
247
- typer.echo("No active threads found.", err=True)
248
- sys.exit(1)
249
-
250
- thread_id = asyncio.run(get_last_thread_via_daemon())
251
-
252
- run_tui(cfg, thread_id=thread_id)
253
-
254
-
255
- def thread_archive(
256
- thread_id: Annotated[str, typer.Argument(help="Thread ID to archive.")],
257
- config: Annotated[
258
- str | None,
259
- typer.Option("--config", "-c", help="Path to configuration file."),
260
- ] = None,
261
- ) -> None:
262
- """Archive a thread.
263
-
264
- Example:
265
- soothe thread archive abc123
266
- """
267
- cfg = load_config(config)
268
- ws_url = websocket_url_from_config(cfg)
269
- _require_daemon(ws_url)
270
-
271
- resp = asyncio.run(
272
- _rpc(ws_url, "send_thread_archive", {"thread_id": thread_id}, "thread_operation_ack")
273
- )
274
- if resp.get("success"):
275
- typer.echo(f"Archived thread {thread_id}.")
276
- else:
277
- typer.echo(
278
- f"Failed to archive thread: {resp.get('message', resp.get('error', 'unknown'))}",
279
- err=True,
280
- )
281
-
282
-
283
189
  def thread_show(
284
190
  thread_id: Annotated[str, typer.Argument(help="Thread ID to show.")],
285
191
  config: Annotated[
@@ -287,10 +193,12 @@ def thread_show(
287
193
  typer.Option("--config", "-c", help="Path to configuration file."),
288
194
  ] = None,
289
195
  ) -> None:
290
- """Show thread details.
196
+ """Show thread details (read-only diagnostics).
291
197
 
292
198
  Example:
293
199
  soothe thread show abc123
200
+
201
+ Note: For thread lifecycle management, use loop commands (RFC-503).
294
202
  """
295
203
  cfg = load_config(config)
296
204
  ws_url = websocket_url_from_config(cfg)
@@ -331,45 +239,6 @@ def thread_show(
331
239
  asyncio.run(_show())
332
240
 
333
241
 
334
- def thread_delete(
335
- thread_id: Annotated[str, typer.Argument(help="Thread ID to delete.")],
336
- config: Annotated[
337
- str | None,
338
- typer.Option("--config", "-c", help="Path to configuration file."),
339
- ] = None,
340
- *,
341
- yes: Annotated[
342
- bool,
343
- typer.Option("--yes", "-y", help="Skip confirmation."),
344
- ] = False,
345
- ) -> None:
346
- """Permanently delete a thread.
347
-
348
- Example:
349
- soothe thread delete abc123
350
- """
351
- if not yes:
352
- confirm = typer.confirm(f"Permanently delete thread {thread_id}?")
353
- if not confirm:
354
- typer.echo("Cancelled.")
355
- return
356
-
357
- cfg = load_config(config)
358
- ws_url = websocket_url_from_config(cfg)
359
- _require_daemon(ws_url)
360
-
361
- resp = asyncio.run(
362
- _rpc(ws_url, "send_thread_delete", {"thread_id": thread_id}, "thread_operation_ack")
363
- )
364
- if resp.get("success"):
365
- typer.echo(f"Deleted thread {thread_id}.")
366
- else:
367
- typer.echo(
368
- f"Failed to delete thread: {resp.get('message', resp.get('error', 'unknown'))}",
369
- err=True,
370
- )
371
-
372
-
373
242
  def thread_export(
374
243
  thread_id: Annotated[str, typer.Argument(help="Thread ID to export.")],
375
244
  output: Annotated[
@@ -381,15 +250,14 @@ def thread_export(
381
250
  typer.Option("--format", "-f", help="Export format: jsonl or md."),
382
251
  ] = "jsonl",
383
252
  ) -> None:
384
- """Export thread conversation to a file.
253
+ """Export thread conversation to a file (read-only diagnostics).
385
254
 
386
255
  Example:
387
256
  soothe thread export abc123 --output out.jsonl
388
257
  soothe thread export abc123 --format md --output out.md
389
- """
390
- import json
391
- from pathlib import Path
392
258
 
259
+ Note: For thread lifecycle management, use loop commands (RFC-503).
260
+ """
393
261
  cfg = load_config(config=None)
394
262
  ws_url = websocket_url_from_config(cfg)
395
263
  _require_daemon(ws_url)
@@ -448,10 +316,12 @@ def thread_stats(
448
316
  typer.Option("--config", "-c", help="Path to configuration file."),
449
317
  ] = None,
450
318
  ) -> None:
451
- """Show thread execution statistics.
319
+ """Show thread execution statistics (read-only diagnostics).
452
320
 
453
321
  Example:
454
322
  soothe thread stats abc123
323
+
324
+ Note: For thread lifecycle management, use loop commands (RFC-503).
455
325
  """
456
326
  cfg = load_config(config)
457
327
  ws_url = websocket_url_from_config(cfg)
@@ -498,137 +368,6 @@ def thread_stats(
498
368
  asyncio.run(_stats())
499
369
 
500
370
 
501
- def thread_tag(
502
- thread_id: Annotated[str, typer.Argument(help="Thread ID.")],
503
- tags: Annotated[
504
- list[str],
505
- typer.Argument(help="Tags to add/remove."),
506
- ],
507
- config: Annotated[
508
- str | None,
509
- typer.Option("--config", "-c", help="Path to configuration file."),
510
- ] = None,
511
- *,
512
- remove: Annotated[
513
- bool,
514
- typer.Option("--remove", help="Remove tags instead of adding."),
515
- ] = False,
516
- ) -> None:
517
- """Add or remove tags from a thread.
518
-
519
- Examples:
520
- soothe thread tag abc123 research analysis
521
- soothe thread tag abc123 research --remove
522
- """
523
- cfg = load_config(config)
524
- ws_url = websocket_url_from_config(cfg)
525
- _require_daemon(ws_url)
526
-
527
- async def _tag() -> None:
528
- client = WebSocketClient(url=ws_url)
529
- try:
530
- await client.connect()
531
-
532
- # Get current thread state to read existing tags
533
- await client.send_thread_get(thread_id)
534
- thread_data: dict[str, Any] = {}
535
- async with asyncio.timeout(30.0):
536
- while True:
537
- event = await client.read_event()
538
- if not event:
539
- typer.echo("No response from daemon.", err=True)
540
- return
541
- etype = event.get("type", "")
542
- if etype == "thread_get_response":
543
- thread_data = event.get("thread", {})
544
- break
545
- if etype == "error":
546
- typer.echo(f"Error: {event.get('message', 'unknown')}", err=True)
547
- return
548
-
549
- # Update tags
550
- metadata = dict(thread_data.get("metadata", {}))
551
- current_tags = set(metadata.get("tags", []))
552
-
553
- if remove:
554
- current_tags -= set(tags)
555
- else:
556
- current_tags |= set(tags)
557
-
558
- metadata["tags"] = sorted(current_tags)
559
-
560
- # Update state via thread_update_state
561
- await client.send_thread_update_state(thread_id, {"metadata": metadata})
562
-
563
- # Wait for ack
564
- async with asyncio.timeout(10.0):
565
- while True:
566
- event = await client.read_event()
567
- if not event:
568
- break
569
- if event.get("type") in (
570
- "thread_update_state_response",
571
- "thread_operation_ack",
572
- ):
573
- break
574
-
575
- tag_list = ", ".join(metadata["tags"]) if metadata["tags"] else "(none)"
576
- typer.echo(f"Tags: {tag_list}")
577
- except TimeoutError:
578
- typer.echo("Timed out waiting for response.", err=True)
579
- finally:
580
- await client.close()
581
-
582
- asyncio.run(_tag())
583
-
584
-
585
- def thread_create(
586
- config: Annotated[
587
- str | None,
588
- typer.Option("--config", "-c", help="Path to configuration file."),
589
- ] = None,
590
- *,
591
- message: Annotated[
592
- str | None,
593
- typer.Option("--message", "-m", help="Initial message to seed the thread."),
594
- ] = None,
595
- tag: Annotated[
596
- list[str] | None,
597
- typer.Option("--tag", "-t", help="Tags for the thread (repeatable)."),
598
- ] = None,
599
- ) -> None:
600
- """Create a new persisted thread.
601
-
602
- Examples:
603
- soothe thread create
604
- soothe thread create --message "Hello world"
605
- soothe thread create --tag research --tag analysis
606
- """
607
- cfg = load_config(config)
608
- ws_url = websocket_url_from_config(cfg)
609
- _require_daemon(ws_url)
610
-
611
- metadata: dict[str, Any] | None = None
612
- if tag:
613
- metadata = {"tags": sorted(tag)}
614
-
615
- resp = asyncio.run(
616
- _rpc(
617
- ws_url,
618
- "send_thread_create",
619
- {"initial_message": message, "metadata": metadata},
620
- "thread_created",
621
- )
622
- )
623
- if resp.get("thread_id"):
624
- typer.echo(f"Created thread {resp['thread_id']}")
625
- else:
626
- typer.echo(
627
- f"Failed to create thread: {resp.get('message', resp.get('error', 'unknown'))}",
628
- err=True,
629
- )
630
-
631
-
632
371
  def thread_artifacts(
633
372
  thread_id: Annotated[str, typer.Argument(help="Thread ID to list artifacts for.")],
634
373
  config: Annotated[
@@ -636,10 +375,12 @@ def thread_artifacts(
636
375
  typer.Option("--config", "-c", help="Path to configuration file."),
637
376
  ] = None,
638
377
  ) -> None:
639
- """List artifacts for a thread.
378
+ """List artifacts for a thread (read-only diagnostics).
640
379
 
641
380
  Example:
642
381
  soothe thread artifacts abc123
382
+
383
+ Note: For thread lifecycle management, use loop commands (RFC-503).
643
384
  """
644
385
  cfg = load_config(config)
645
386
  ws_url = websocket_url_from_config(cfg)
@@ -653,7 +394,7 @@ def thread_artifacts(
653
394
  typer.echo("No artifacts found.")
654
395
  return
655
396
  typer.echo(f"{'Name':<30} {'Type':<15} {'Summary':<40}")
656
- typer.echo("\u2500" * 90)
397
+ typer.echo("" * 90)
657
398
  for a in artifacts:
658
399
  name = str(a.get("name", ""))[:30]
659
400
  a_type = str(a.get("type", ""))[:15]
@@ -93,7 +93,7 @@ def main(
93
93
  soothe # Interactive TUI mode
94
94
  soothe -p "Research AI advances" # Headless single-prompt mode
95
95
  soothe --config custom.yml # Ignored for client settings; use ~/.soothe/config/cli_config.yml
96
- soothe thread list # List conversation threads
96
+ soothe loop list # List AgentLoop instances
97
97
  """
98
98
  # Handle -h/--help flag
99
99
  if show_help:
@@ -121,10 +121,16 @@ def main(
121
121
 
122
122
 
123
123
  # ---------------------------------------------------------------------------
124
- # Thread Command (Nested Subcommands)
124
+ # Thread Command (Nested Subcommands) - Read-Only Diagnostics
125
125
  # ---------------------------------------------------------------------------
126
+ # NOTE: Thread commands are read-only diagnostics per RFC-503 (Loop-First UX).
127
+ # Users manage loops (primary entity), not threads (internal execution contexts).
128
+ # For thread lifecycle management, use loop commands: soothe loop <subcommand>
126
129
 
127
- thread_app = typer.Typer(name="thread", help="Manage conversation threads")
130
+ thread_app = typer.Typer(
131
+ name="thread",
132
+ help="Inspect conversation threads (read-only diagnostics)",
133
+ )
128
134
  add_help_alias(thread_app)
129
135
  app.add_typer(thread_app)
130
136
 
@@ -140,11 +146,13 @@ def _thread_list(
140
146
  typer.Option("--status", "-s", help="Filter by status (active, archived)."),
141
147
  ] = None,
142
148
  ) -> None:
143
- """List all conversation threads.
149
+ """List all agent threads (read-only diagnostics).
144
150
 
145
151
  Examples:
146
152
  soothe thread list
147
153
  soothe thread list --status active
154
+
155
+ Note: For thread lifecycle management, use loop commands (RFC-503).
148
156
  """
149
157
  from soothe_cli.cli.commands.thread_cmd import thread_list
150
158
 
@@ -159,85 +167,18 @@ def _thread_show(
159
167
  typer.Option("--config", "-c", help="Path to configuration file."),
160
168
  ] = None,
161
169
  ) -> None:
162
- """Show thread details.
170
+ """Show thread details (read-only diagnostics).
163
171
 
164
172
  Example:
165
173
  soothe thread show abc123
174
+
175
+ Note: For thread lifecycle management, use loop commands (RFC-503).
166
176
  """
167
177
  from soothe_cli.cli.commands.thread_cmd import thread_show
168
178
 
169
179
  thread_show(thread_id=thread_id, config=config)
170
180
 
171
181
 
172
- @thread_app.command("continue")
173
- def _thread_continue(
174
- thread_id: Annotated[
175
- str | None,
176
- typer.Argument(help="Thread ID to continue. Omit to continue last active thread."),
177
- ] = None,
178
- config: Annotated[
179
- str | None,
180
- typer.Option("--config", "-c", help="Path to configuration file."),
181
- ] = None,
182
- new: Annotated[ # noqa: FBT002
183
- bool,
184
- typer.Option("--new", help="Create a new thread instead of continuing."),
185
- ] = False,
186
- ) -> None:
187
- """Continue a conversation thread in the TUI.
188
-
189
- Requires a running daemon. Start daemon with 'soothe-daemon start' first.
190
-
191
- Examples:
192
- soothe thread continue abc123
193
- soothe thread continue --new
194
- soothe thread continue
195
- """
196
- from soothe_cli.cli.commands.thread_cmd import thread_continue
197
-
198
- thread_continue(thread_id=thread_id, config=config, new=new)
199
-
200
-
201
- @thread_app.command("archive")
202
- def _thread_archive(
203
- thread_id: Annotated[str, typer.Argument(help="Thread ID to archive.")],
204
- config: Annotated[
205
- str | None,
206
- typer.Option("--config", "-c", help="Path to configuration file."),
207
- ] = None,
208
- ) -> None:
209
- """Archive a thread.
210
-
211
- Example:
212
- soothe thread archive abc123
213
- """
214
- from soothe_cli.cli.commands.thread_cmd import thread_archive
215
-
216
- thread_archive(thread_id=thread_id, config=config)
217
-
218
-
219
- @thread_app.command("delete")
220
- def _thread_delete(
221
- thread_id: Annotated[str, typer.Argument(help="Thread ID to delete.")],
222
- config: Annotated[
223
- str | None,
224
- typer.Option("--config", "-c", help="Path to configuration file."),
225
- ] = None,
226
- yes: Annotated[ # noqa: FBT002
227
- bool,
228
- typer.Option("--yes", "-y", help="Skip confirmation."),
229
- ] = False,
230
- ) -> None:
231
- """Permanently delete a thread.
232
-
233
- Example:
234
- soothe thread delete abc123
235
- """
236
- from soothe_cli.cli.commands.thread_cmd import thread_delete
237
-
238
- thread_delete(thread_id=thread_id, config=config, yes=yes)
239
-
240
-
241
182
  @thread_app.command("export")
242
183
  def _thread_export(
243
184
  thread_id: Annotated[str, typer.Argument(help="Thread ID to export.")],
@@ -250,10 +191,12 @@ def _thread_export(
250
191
  typer.Option("--format", "-f", help="Export format: jsonl or md."),
251
192
  ] = "jsonl",
252
193
  ) -> None:
253
- """Export thread conversation to a file.
194
+ """Export thread conversation to a file (read-only diagnostics).
254
195
 
255
196
  Example:
256
- soothe thread export abc123 --output out.json
197
+ soothe thread export abc123 --output out.jsonl
198
+
199
+ Note: For thread lifecycle management, use loop commands (RFC-503).
257
200
  """
258
201
  from soothe_cli.cli.commands.thread_cmd import thread_export
259
202
 
@@ -268,70 +211,18 @@ def _thread_stats(
268
211
  typer.Option("--config", "-c", help="Path to configuration file."),
269
212
  ] = None,
270
213
  ) -> None:
271
- """Show thread execution statistics.
214
+ """Show thread execution statistics (read-only diagnostics).
272
215
 
273
216
  Example:
274
217
  soothe thread stats abc123
218
+
219
+ Note: For thread lifecycle management, use loop commands (RFC-503).
275
220
  """
276
221
  from soothe_cli.cli.commands.thread_cmd import thread_stats
277
222
 
278
223
  thread_stats(thread_id=thread_id, config=config)
279
224
 
280
225
 
281
- @thread_app.command("tag")
282
- def _thread_tag(
283
- thread_id: Annotated[str, typer.Argument(help="Thread ID.")],
284
- tags: Annotated[
285
- list[str],
286
- typer.Argument(help="Tags to add/remove."),
287
- ],
288
- config: Annotated[
289
- str | None,
290
- typer.Option("--config", "-c", help="Path to configuration file."),
291
- ] = None,
292
- remove: Annotated[ # noqa: FBT002
293
- bool,
294
- typer.Option("--remove", help="Remove tags instead of adding."),
295
- ] = False,
296
- ) -> None:
297
- """Add or remove tags from a thread.
298
-
299
- Examples:
300
- soothe thread tag abc123 research analysis
301
- soothe thread tag abc123 research --remove
302
- """
303
- from soothe_cli.cli.commands.thread_cmd import thread_tag
304
-
305
- thread_tag(thread_id=thread_id, tags=tags, config=config, remove=remove)
306
-
307
-
308
- @thread_app.command("create")
309
- def _thread_create(
310
- config: Annotated[
311
- str | None,
312
- typer.Option("--config", "-c", help="Path to configuration file."),
313
- ] = None,
314
- message: Annotated[
315
- str | None,
316
- typer.Option("--message", "-m", help="Initial message to seed the thread."),
317
- ] = None,
318
- tag: Annotated[
319
- list[str] | None,
320
- typer.Option("--tag", "-t", help="Tags for the thread (repeatable)."),
321
- ] = None,
322
- ) -> None:
323
- """Create a new persisted thread.
324
-
325
- Examples:
326
- soothe thread create
327
- soothe thread create --message "Hello world"
328
- soothe thread create --tag research
329
- """
330
- from soothe_cli.cli.commands.thread_cmd import thread_create
331
-
332
- thread_create(config=config, message=message, tag=tag)
333
-
334
-
335
226
  @thread_app.command("artifacts")
336
227
  def _thread_artifacts(
337
228
  thread_id: Annotated[str, typer.Argument(help="Thread ID to list artifacts for.")],
@@ -340,16 +231,28 @@ def _thread_artifacts(
340
231
  typer.Option("--config", "-c", help="Path to configuration file."),
341
232
  ] = None,
342
233
  ) -> None:
343
- """List artifacts for a thread.
234
+ """List artifacts for a thread (read-only diagnostics).
344
235
 
345
236
  Example:
346
237
  soothe thread artifacts abc123
238
+
239
+ Note: For thread lifecycle management, use loop commands (RFC-503).
347
240
  """
348
241
  from soothe_cli.cli.commands.thread_cmd import thread_artifacts
349
242
 
350
243
  thread_artifacts(thread_id=thread_id, config=config)
351
244
 
352
245
 
246
+ # ---------------------------------------------------------------------------
247
+ # Loop Command (Nested Subcommands)
248
+ # ---------------------------------------------------------------------------
249
+
250
+ from soothe_cli.loop_commands import loop_app as _loop_app # noqa: E402
251
+
252
+ add_help_alias(_loop_app)
253
+ app.add_typer(_loop_app, name="loop")
254
+
255
+
353
256
  # ---------------------------------------------------------------------------
354
257
  # Config Command (Nested Subcommands)
355
258
  # ---------------------------------------------------------------------------