soothe-cli 0.1.0__py3-none-any.whl
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/__init__.py +5 -0
- soothe_cli/cli/__init__.py +1 -0
- soothe_cli/cli/commands/__init__.py +1 -0
- soothe_cli/cli/commands/autopilot_cmd.py +410 -0
- soothe_cli/cli/commands/config_cmd.py +277 -0
- soothe_cli/cli/commands/run_cmd.py +87 -0
- soothe_cli/cli/commands/status_cmd.py +121 -0
- soothe_cli/cli/commands/subagent_names.py +17 -0
- soothe_cli/cli/commands/thread_cmd.py +657 -0
- soothe_cli/cli/execution/__init__.py +6 -0
- soothe_cli/cli/execution/daemon.py +194 -0
- soothe_cli/cli/execution/headless.py +99 -0
- soothe_cli/cli/execution/launcher.py +31 -0
- soothe_cli/cli/main.py +509 -0
- soothe_cli/cli/renderer.py +444 -0
- soothe_cli/cli/stream/__init__.py +17 -0
- soothe_cli/cli/stream/context.py +138 -0
- soothe_cli/cli/stream/display_line.py +83 -0
- soothe_cli/cli/stream/formatter.py +412 -0
- soothe_cli/cli/stream/pipeline.py +521 -0
- soothe_cli/cli/utils.py +46 -0
- soothe_cli/config/__init__.py +5 -0
- soothe_cli/config/cli_config.py +155 -0
- soothe_cli/plan/__init__.py +5 -0
- soothe_cli/plan/rich_tree.py +54 -0
- soothe_cli/shared/__init__.py +107 -0
- soothe_cli/shared/command_router.py +246 -0
- soothe_cli/shared/config_loader.py +68 -0
- soothe_cli/shared/display_policy.py +413 -0
- soothe_cli/shared/essential_events.py +68 -0
- soothe_cli/shared/event_processor.py +823 -0
- soothe_cli/shared/message_processing.py +393 -0
- soothe_cli/shared/presentation_engine.py +173 -0
- soothe_cli/shared/processor_state.py +80 -0
- soothe_cli/shared/renderer_protocol.py +158 -0
- soothe_cli/shared/rendering.py +43 -0
- soothe_cli/shared/slash_commands.py +354 -0
- soothe_cli/shared/subagent_routing.py +63 -0
- soothe_cli/shared/suppression_state.py +188 -0
- soothe_cli/shared/tool_formatters/__init__.py +27 -0
- soothe_cli/shared/tool_formatters/base.py +109 -0
- soothe_cli/shared/tool_formatters/execution.py +297 -0
- soothe_cli/shared/tool_formatters/fallback.py +128 -0
- soothe_cli/shared/tool_formatters/file_ops.py +299 -0
- soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
- soothe_cli/shared/tool_formatters/media.py +291 -0
- soothe_cli/shared/tool_formatters/structured.py +202 -0
- soothe_cli/shared/tool_formatters/web.py +143 -0
- soothe_cli/shared/tool_output_formatter.py +227 -0
- soothe_cli/shared/tui_trace_log.py +40 -0
- soothe_cli/tui/__init__.py +5 -0
- soothe_cli/tui/_ask_user_types.py +50 -0
- soothe_cli/tui/_cli_context.py +27 -0
- soothe_cli/tui/_env_vars.py +56 -0
- soothe_cli/tui/_session_stats.py +114 -0
- soothe_cli/tui/_version.py +21 -0
- soothe_cli/tui/app.py +4992 -0
- soothe_cli/tui/app.tcss +302 -0
- soothe_cli/tui/command_registry.py +310 -0
- soothe_cli/tui/config.py +2381 -0
- soothe_cli/tui/daemon_session.py +233 -0
- soothe_cli/tui/file_ops.py +409 -0
- soothe_cli/tui/formatting.py +28 -0
- soothe_cli/tui/hooks.py +23 -0
- soothe_cli/tui/input.py +782 -0
- soothe_cli/tui/media_utils.py +471 -0
- soothe_cli/tui/model_config.py +518 -0
- soothe_cli/tui/output.py +69 -0
- soothe_cli/tui/project_utils.py +188 -0
- soothe_cli/tui/sessions.py +1248 -0
- soothe_cli/tui/skills/__init__.py +5 -0
- soothe_cli/tui/skills/invocation.py +74 -0
- soothe_cli/tui/skills/load.py +93 -0
- soothe_cli/tui/textual_adapter.py +1430 -0
- soothe_cli/tui/theme.py +838 -0
- soothe_cli/tui/tool_display.py +297 -0
- soothe_cli/tui/unicode_security.py +502 -0
- soothe_cli/tui/update_check.py +447 -0
- soothe_cli/tui/widgets/__init__.py +9 -0
- soothe_cli/tui/widgets/_links.py +63 -0
- soothe_cli/tui/widgets/approval.py +430 -0
- soothe_cli/tui/widgets/ask_user.py +392 -0
- soothe_cli/tui/widgets/autocomplete.py +666 -0
- soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
- soothe_cli/tui/widgets/autopilot_screen.py +64 -0
- soothe_cli/tui/widgets/chat_input.py +1834 -0
- soothe_cli/tui/widgets/clipboard.py +128 -0
- soothe_cli/tui/widgets/diff.py +240 -0
- soothe_cli/tui/widgets/editor.py +140 -0
- soothe_cli/tui/widgets/history.py +221 -0
- soothe_cli/tui/widgets/loading.py +194 -0
- soothe_cli/tui/widgets/mcp_viewer.py +352 -0
- soothe_cli/tui/widgets/message_store.py +693 -0
- soothe_cli/tui/widgets/messages.py +1720 -0
- soothe_cli/tui/widgets/model_selector.py +988 -0
- soothe_cli/tui/widgets/notification_settings.py +155 -0
- soothe_cli/tui/widgets/status.py +403 -0
- soothe_cli/tui/widgets/theme_selector.py +158 -0
- soothe_cli/tui/widgets/thread_selector.py +1865 -0
- soothe_cli/tui/widgets/tool_renderers.py +148 -0
- soothe_cli/tui/widgets/tool_widgets.py +254 -0
- soothe_cli/tui/widgets/tools.py +165 -0
- soothe_cli/tui/widgets/welcome.py +330 -0
- soothe_cli-0.1.0.dist-info/METADATA +100 -0
- soothe_cli-0.1.0.dist-info/RECORD +107 -0
- soothe_cli-0.1.0.dist-info/WHEEL +4 -0
- soothe_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Skills discovery and invocation helpers for the Soothe Textual TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from soothe_sdk.client import fetch_skills_catalog, websocket_url_from_config
|
|
11
|
+
|
|
12
|
+
from soothe_cli.tui.skills.load import ExtendedSkillMetadata
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from soothe_cli.config.cli_config import CLIConfig
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def discover_skills_async(
|
|
21
|
+
daemon_config: CLIConfig | None = None,
|
|
22
|
+
) -> list[ExtendedSkillMetadata]:
|
|
23
|
+
"""Discover skills from daemon RPC (IG-174 Phase 2).
|
|
24
|
+
|
|
25
|
+
Fetches wire-safe skill metadata from daemon via WebSocket RPC.
|
|
26
|
+
Daemon handles all skill discovery (built-in, user, project, etc.)
|
|
27
|
+
and returns wire-safe metadata. No local filesystem access.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
daemon_config: Daemon config for WebSocket URL construction.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of skill metadata dicts sorted by ascending precedence
|
|
34
|
+
(built-in first, winning entry last). Empty list if daemon
|
|
35
|
+
unavailable.
|
|
36
|
+
"""
|
|
37
|
+
from soothe_sdk.client import WebSocketClient
|
|
38
|
+
|
|
39
|
+
if daemon_config is None:
|
|
40
|
+
logger.warning("No daemon_config provided for skills discovery; returning empty catalog")
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
ws_url = websocket_url_from_config(daemon_config)
|
|
44
|
+
client = WebSocketClient(url=ws_url)
|
|
45
|
+
|
|
46
|
+
by_name: OrderedDict[str, ExtendedSkillMetadata] = OrderedDict()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
await client.connect()
|
|
50
|
+
skills_wire = await fetch_skills_catalog(client, timeout=15.0)
|
|
51
|
+
await client.close()
|
|
52
|
+
|
|
53
|
+
# Build by_name mapping from wire-safe metadata
|
|
54
|
+
for skill_meta in skills_wire:
|
|
55
|
+
name = skill_meta.get("name")
|
|
56
|
+
if name:
|
|
57
|
+
by_name[name] = skill_meta
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.warning(f"Failed to fetch skills from daemon: {e}")
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
return list(by_name.values())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def discover_skills(
|
|
66
|
+
daemon_config: CLIConfig | None = None,
|
|
67
|
+
) -> list[ExtendedSkillMetadata]:
|
|
68
|
+
"""Backward-compatible sync wrapper for async skills discovery."""
|
|
69
|
+
try:
|
|
70
|
+
asyncio.get_running_loop()
|
|
71
|
+
except RuntimeError:
|
|
72
|
+
return asyncio.run(discover_skills_async(daemon_config=daemon_config))
|
|
73
|
+
msg = "discover_skills() cannot be called from a running event loop; use discover_skills_async() instead."
|
|
74
|
+
raise RuntimeError(msg)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Skills loading utilities for the Soothe Textual TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TypedDict
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExtendedSkillMetadata(TypedDict):
|
|
14
|
+
"""Wire-safe skill metadata from daemon RPC (IG-174 Phase 2).
|
|
15
|
+
|
|
16
|
+
This TypedDict represents skill metadata fetched from daemon via WebSocket RPC.
|
|
17
|
+
All fields are wire-safe (no Path objects, use str representations).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
description: str
|
|
22
|
+
source: str # "builtin", "user", "project", "agents", "claude"
|
|
23
|
+
path: str | None # Wire-safe path representation (not Path object)
|
|
24
|
+
tags: list[str]
|
|
25
|
+
tools: list[str] | None
|
|
26
|
+
default_model: str | None
|
|
27
|
+
requires: list[str] | None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_under_allowed_roots(target: Path, roots: Sequence[Path]) -> bool:
|
|
31
|
+
"""Return True if `target` is equal to or nested under one of `roots`."""
|
|
32
|
+
t = target.resolve()
|
|
33
|
+
for root in roots:
|
|
34
|
+
try:
|
|
35
|
+
t.relative_to(root.resolve())
|
|
36
|
+
except ValueError:
|
|
37
|
+
continue
|
|
38
|
+
else:
|
|
39
|
+
return True
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_skill_content(
|
|
44
|
+
skill_path: str | Path, *, allowed_roots: Sequence[Path] | None = None
|
|
45
|
+
) -> str | None:
|
|
46
|
+
"""Read `SKILL.md` for a skill directory with optional path containment checks.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
skill_path: Path to the skill directory **or** to a `SKILL.md` file.
|
|
50
|
+
allowed_roots: Resolved directories that may contain the target file.
|
|
51
|
+
When empty or ``None``, any resolved path is accepted (tests only —
|
|
52
|
+
production callers should pass roots from daemon RPC).
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
File contents as a string, or ``None`` only when the file is missing.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
PermissionError: When ``allowed_roots`` is non-empty and the resolved
|
|
59
|
+
`SKILL.md` path lies outside every allowed root.
|
|
60
|
+
OSError: Propagated from the filesystem when the file cannot be read.
|
|
61
|
+
"""
|
|
62
|
+
raw = Path(skill_path)
|
|
63
|
+
skill_md = raw / "SKILL.md" if raw.is_dir() else raw
|
|
64
|
+
resolved_md = skill_md.resolve()
|
|
65
|
+
|
|
66
|
+
if allowed_roots:
|
|
67
|
+
roots = [Path(r).resolve() for r in allowed_roots]
|
|
68
|
+
if roots and not _is_under_allowed_roots(resolved_md, roots):
|
|
69
|
+
msg = f"Refusing to read skill file outside allowed directories: {resolved_md}"
|
|
70
|
+
raise PermissionError(msg)
|
|
71
|
+
|
|
72
|
+
if not resolved_md.is_file():
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
return resolved_md.read_text(encoding="utf-8")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def strip_skill_frontmatter(content: str) -> str:
|
|
79
|
+
"""Remove optional YAML frontmatter from SKILL markdown content."""
|
|
80
|
+
if not content.startswith("---\n"):
|
|
81
|
+
return content
|
|
82
|
+
end = content.find("\n---\n", 4)
|
|
83
|
+
if end == -1:
|
|
84
|
+
return content
|
|
85
|
+
return content[end + 5 :]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Re-export for callers that import from this module
|
|
89
|
+
__all__ = [
|
|
90
|
+
"ExtendedSkillMetadata",
|
|
91
|
+
"load_skill_content",
|
|
92
|
+
"strip_skill_frontmatter",
|
|
93
|
+
]
|