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,188 @@
|
|
|
1
|
+
"""Utilities for project root detection and project-specific configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from soothe_cli.tui._env_vars import SERVER_ENV_PREFIX
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ProjectContext:
|
|
22
|
+
"""Explicit user/project path context for project-sensitive behavior.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
user_cwd: Authoritative working directory from the CLI invocation.
|
|
26
|
+
project_root: Resolved project root for `user_cwd`, if one exists.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
user_cwd: Path
|
|
30
|
+
project_root: Path | None = None
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
"""Validate that path fields are absolute.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If `user_cwd` or `project_root` is not absolute.
|
|
37
|
+
"""
|
|
38
|
+
if not self.user_cwd.is_absolute():
|
|
39
|
+
msg = f"user_cwd must be absolute, got {self.user_cwd!r}"
|
|
40
|
+
raise ValueError(msg)
|
|
41
|
+
if self.project_root is not None and not self.project_root.is_absolute():
|
|
42
|
+
msg = f"project_root must be absolute, got {self.project_root!r}"
|
|
43
|
+
raise ValueError(msg)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_user_cwd(cls, user_cwd: str | Path) -> ProjectContext:
|
|
47
|
+
"""Build a project context from an explicit user working directory.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
user_cwd: User invocation directory.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Resolved project context.
|
|
54
|
+
"""
|
|
55
|
+
resolved_cwd = Path(user_cwd).expanduser().resolve()
|
|
56
|
+
return cls(
|
|
57
|
+
user_cwd=resolved_cwd,
|
|
58
|
+
project_root=find_project_root(resolved_cwd),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def resolve_user_path(self, path: str | Path) -> Path:
|
|
62
|
+
"""Resolve a path relative to the explicit user working directory.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
path: Absolute or relative user-facing path.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Absolute resolved path.
|
|
69
|
+
"""
|
|
70
|
+
candidate = Path(path).expanduser()
|
|
71
|
+
if candidate.is_absolute():
|
|
72
|
+
return candidate.resolve()
|
|
73
|
+
return (self.user_cwd / candidate).resolve()
|
|
74
|
+
|
|
75
|
+
def project_agent_md_paths(self) -> list[Path]:
|
|
76
|
+
"""Return project-level `AGENTS.md` files for this context."""
|
|
77
|
+
if self.project_root is None:
|
|
78
|
+
return []
|
|
79
|
+
return find_project_agent_md(self.project_root)
|
|
80
|
+
|
|
81
|
+
def project_skills_dir(self) -> Path | None:
|
|
82
|
+
"""Return the project `SOOTHE_HOME/skills` directory, if any."""
|
|
83
|
+
if self.project_root is None:
|
|
84
|
+
return None
|
|
85
|
+
return self.project_root / ".soothe" / "skills"
|
|
86
|
+
|
|
87
|
+
def project_agents_dir(self) -> Path | None:
|
|
88
|
+
"""Return the project `SOOTHE_HOME/agents` directory, if any."""
|
|
89
|
+
if self.project_root is None:
|
|
90
|
+
return None
|
|
91
|
+
return self.project_root / ".soothe" / "agents"
|
|
92
|
+
|
|
93
|
+
def project_agent_skills_dir(self) -> Path | None:
|
|
94
|
+
"""Return the project `.agents/skills` directory, if any."""
|
|
95
|
+
if self.project_root is None:
|
|
96
|
+
return None
|
|
97
|
+
return self.project_root / ".agents" / "skills"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_server_project_context(
|
|
101
|
+
env: Mapping[str, str] | None = None,
|
|
102
|
+
) -> ProjectContext | None:
|
|
103
|
+
"""Read the server project context from environment transport data.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
env: Environment mapping to read from.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Reconstructed project context, or `None` if no server context exists.
|
|
110
|
+
"""
|
|
111
|
+
environment = os.environ if env is None else env
|
|
112
|
+
raw_cwd = environment.get(f"{SERVER_ENV_PREFIX}CWD")
|
|
113
|
+
if not raw_cwd:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
user_cwd = Path(raw_cwd).expanduser().resolve()
|
|
118
|
+
raw_project_root = environment.get(f"{SERVER_ENV_PREFIX}PROJECT_ROOT")
|
|
119
|
+
project_root = (
|
|
120
|
+
Path(raw_project_root).expanduser().resolve()
|
|
121
|
+
if raw_project_root
|
|
122
|
+
else find_project_root(user_cwd)
|
|
123
|
+
)
|
|
124
|
+
except OSError:
|
|
125
|
+
logger.warning(
|
|
126
|
+
"Could not resolve server project context from CWD=%s",
|
|
127
|
+
raw_cwd,
|
|
128
|
+
exc_info=True,
|
|
129
|
+
)
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
return ProjectContext(user_cwd=user_cwd, project_root=project_root)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def find_project_root(start_path: str | Path | None = None) -> Path | None:
|
|
136
|
+
"""Find the project root by looking for .git directory.
|
|
137
|
+
|
|
138
|
+
Walks up the directory tree from start_path (or cwd) looking for a .git
|
|
139
|
+
directory, which indicates the project root.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
start_path: Directory to start searching from.
|
|
143
|
+
Defaults to current working directory.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Path to the project root if found, None otherwise.
|
|
147
|
+
"""
|
|
148
|
+
current = Path(start_path or Path.cwd()).expanduser().resolve()
|
|
149
|
+
|
|
150
|
+
# Walk up the directory tree
|
|
151
|
+
for parent in [current, *list(current.parents)]:
|
|
152
|
+
git_dir = parent / ".git"
|
|
153
|
+
if git_dir.exists():
|
|
154
|
+
return parent
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def find_project_agent_md(project_root: Path) -> list[Path]:
|
|
160
|
+
"""Find project-specific AGENTS.md file(s).
|
|
161
|
+
|
|
162
|
+
Checks two locations and returns ALL that exist:
|
|
163
|
+
1. project_root/SOOTHE_HOME/AGENTS.md
|
|
164
|
+
2. project_root/AGENTS.md
|
|
165
|
+
|
|
166
|
+
Both files will be loaded and combined if both exist.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
project_root: Path to the project root directory.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Existing AGENTS.md paths.
|
|
173
|
+
|
|
174
|
+
Empty if neither file exists, one entry if only one is present, or
|
|
175
|
+
two entries if both locations have the file.
|
|
176
|
+
"""
|
|
177
|
+
candidates = [
|
|
178
|
+
project_root / "SOOTHE_HOME" / "AGENTS.md",
|
|
179
|
+
project_root / "AGENTS.md",
|
|
180
|
+
]
|
|
181
|
+
paths: list[Path] = []
|
|
182
|
+
for candidate in candidates:
|
|
183
|
+
try:
|
|
184
|
+
if candidate.exists():
|
|
185
|
+
paths.append(candidate)
|
|
186
|
+
except OSError:
|
|
187
|
+
pass
|
|
188
|
+
return paths
|