smart-linux-assistant 0.5.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.
- linux_assistant/__init__.py +11 -0
- linux_assistant/cli/__init__.py +0 -0
- linux_assistant/cli/main.py +277 -0
- linux_assistant/config/__init__.py +0 -0
- linux_assistant/config/settings.py +65 -0
- linux_assistant/core/__init__.py +0 -0
- linux_assistant/exceptions/__init__.py +31 -0
- linux_assistant/exceptions/base.py +98 -0
- linux_assistant/models/__init__.py +11 -0
- linux_assistant/models/command_result.py +51 -0
- linux_assistant/repositories/__init__.py +0 -0
- linux_assistant/services/__init__.py +2 -0
- linux_assistant/services/command_executor.py +109 -0
- linux_assistant/services/explainer.py +141 -0
- linux_assistant/services/search.py +77 -0
- linux_assistant/utils/__init__.py +0 -0
- linux_assistant/utils/groq_client.py +65 -0
- linux_assistant/utils/logger.py +54 -0
- linux_assistant/utils/shell.py +31 -0
- smart_linux_assistant-0.5.0.dist-info/METADATA +233 -0
- smart_linux_assistant-0.5.0.dist-info/RECORD +25 -0
- smart_linux_assistant-0.5.0.dist-info/WHEEL +5 -0
- smart_linux_assistant-0.5.0.dist-info/entry_points.txt +3 -0
- smart_linux_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
- smart_linux_assistant-0.5.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for the Smart Linux Assistant.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import sys
|
|
7
|
+
import typer
|
|
8
|
+
from linux_assistant.exceptions import CommandExecutionError, CommandFailedError, CommandTimeoutError, ValidationError, MissingAPIKeyError, ServiceError, RateLimitError
|
|
9
|
+
from linux_assistant.services.explainer import Explainer
|
|
10
|
+
from linux_assistant.services.command_executor import CommandExecutor
|
|
11
|
+
from linux_assistant.utils.logger import get_logger
|
|
12
|
+
from linux_assistant.utils.shell import command_exists
|
|
13
|
+
from linux_assistant.services.search import Searcher
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="smart-linux",
|
|
19
|
+
help="An AI-powered Linux productivity assistant.",
|
|
20
|
+
add_completion=False,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@app.callback()
|
|
24
|
+
def callback() -> None:
|
|
25
|
+
"""
|
|
26
|
+
Smart Linux Assistant — an AI-powered Linux productivity CLI.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def run(
|
|
31
|
+
command: str = typer.Argument(..., help="The shell command to execute."),
|
|
32
|
+
timeout: int = typer.Option(30, help="Timeout in seconds."),
|
|
33
|
+
check: bool = typer.Option(
|
|
34
|
+
False, "--check", help="Exit non-zero if the command itself fails."
|
|
35
|
+
),
|
|
36
|
+
suggest_fix: bool = typer.Option(
|
|
37
|
+
False, "--suggest-fix", help="If the command fails, suggest an AI-generated fix."
|
|
38
|
+
),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Execute a shell command and display structured results.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
if suggest_fix and not check:
|
|
45
|
+
typer.secho(
|
|
46
|
+
"Invalid usage: --suggest-fix requires --check (fix suggestions only apply to command failures detected via --check).",
|
|
47
|
+
fg=typer.colors.RED,
|
|
48
|
+
err=True,
|
|
49
|
+
)
|
|
50
|
+
raise typer.Exit(code=2)
|
|
51
|
+
|
|
52
|
+
executor = CommandExecutor()
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
if check:
|
|
56
|
+
result = executor.execute_checked(command, timeout=timeout)
|
|
57
|
+
else:
|
|
58
|
+
result = executor.execute(command, timeout=timeout)
|
|
59
|
+
|
|
60
|
+
except ValidationError as exc:
|
|
61
|
+
typer.secho(f"Invalid input: {exc}", fg=typer.colors.RED, err=True)
|
|
62
|
+
raise typer.Exit(code=2)
|
|
63
|
+
|
|
64
|
+
except CommandTimeoutError as exc:
|
|
65
|
+
typer.secho(f"Timed out: {exc}", fg=typer.colors.RED, err=True)
|
|
66
|
+
raise typer.Exit(code=124)
|
|
67
|
+
|
|
68
|
+
except CommandFailedError as exc:
|
|
69
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
70
|
+
if exc.result.stderr:
|
|
71
|
+
typer.secho(exc.result.stderr, fg=typer.colors.YELLOW, err=True)
|
|
72
|
+
|
|
73
|
+
if suggest_fix:
|
|
74
|
+
typer.echo()
|
|
75
|
+
try:
|
|
76
|
+
explainer = Explainer()
|
|
77
|
+
suggestion = explainer.suggest_fix(command, exc.result.stderr)
|
|
78
|
+
|
|
79
|
+
except MissingAPIKeyError as fix_exc:
|
|
80
|
+
typer.secho(str(fix_exc), fg=typer.colors.RED, err=True)
|
|
81
|
+
|
|
82
|
+
except RateLimitError as fix_exc:
|
|
83
|
+
typer.secho(str(fix_exc), fg=typer.colors.YELLOW, err=True)
|
|
84
|
+
|
|
85
|
+
except ServiceError as fix_exc:
|
|
86
|
+
typer.secho(f"Could not get a fix suggestion: {fix_exc}", fg=typer.colors.RED, err=True)
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
if suggestion is None:
|
|
90
|
+
typer.secho("No confident fix available.", fg=typer.colors.YELLOW)
|
|
91
|
+
else:
|
|
92
|
+
typer.secho("Suggested fix:", fg=typer.colors.CYAN)
|
|
93
|
+
typer.echo(f" {suggestion}")
|
|
94
|
+
|
|
95
|
+
raise typer.Exit(code=exc.result.exit_code)
|
|
96
|
+
|
|
97
|
+
except CommandExecutionError as exc:
|
|
98
|
+
typer.secho(f"Execution error: {exc}", fg=typer.colors.RED, err=True)
|
|
99
|
+
raise typer.Exit(code=1)
|
|
100
|
+
|
|
101
|
+
if result.stdout:
|
|
102
|
+
typer.echo(result.stdout)
|
|
103
|
+
if result.stderr:
|
|
104
|
+
typer.secho(result.stderr, fg=typer.colors.YELLOW, err=True)
|
|
105
|
+
|
|
106
|
+
raise typer.Exit(code=result.exit_code)
|
|
107
|
+
|
|
108
|
+
DOCTOR_CHECKS: tuple[str, ...] = (
|
|
109
|
+
"bash",
|
|
110
|
+
"git",
|
|
111
|
+
"python3",
|
|
112
|
+
"docker",
|
|
113
|
+
"curl",
|
|
114
|
+
"systemctl",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command()
|
|
119
|
+
def doctor() -> None:
|
|
120
|
+
"""
|
|
121
|
+
Check for the presence of common tools on this system.
|
|
122
|
+
"""
|
|
123
|
+
typer.echo("Running environment checks...\n")
|
|
124
|
+
|
|
125
|
+
missing: list[str] = []
|
|
126
|
+
|
|
127
|
+
for tool in DOCTOR_CHECKS:
|
|
128
|
+
exists = command_exists(tool)
|
|
129
|
+
symbol = "✔" if exists else "✘"
|
|
130
|
+
color = typer.colors.GREEN if exists else typer.colors.RED
|
|
131
|
+
typer.secho(f" {symbol} {tool}", fg=color)
|
|
132
|
+
|
|
133
|
+
if not exists:
|
|
134
|
+
missing.append(tool)
|
|
135
|
+
|
|
136
|
+
typer.echo()
|
|
137
|
+
|
|
138
|
+
if missing:
|
|
139
|
+
typer.secho(
|
|
140
|
+
f"{len(missing)} tool(s) missing: {', '.join(missing)}",
|
|
141
|
+
fg=typer.colors.YELLOW,
|
|
142
|
+
)
|
|
143
|
+
raise typer.Exit(code=1)
|
|
144
|
+
|
|
145
|
+
typer.secho("All checked tools are available.", fg=typer.colors.GREEN)
|
|
146
|
+
|
|
147
|
+
@app.command()
|
|
148
|
+
def explain(
|
|
149
|
+
text: str = typer.Argument(
|
|
150
|
+
..., help="The command, error message, or output to explain."
|
|
151
|
+
),
|
|
152
|
+
) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Get a plain-language explanation of a command or error message.
|
|
155
|
+
"""
|
|
156
|
+
if not text.strip():
|
|
157
|
+
typer.secho("Invalid input: Text to explain cannot be empty.", fg=typer.colors.RED, err=True)
|
|
158
|
+
raise typer.Exit(code=2)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
explainer = Explainer()
|
|
162
|
+
result = explainer.explain(text)
|
|
163
|
+
|
|
164
|
+
except MissingAPIKeyError as exc:
|
|
165
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
166
|
+
raise typer.Exit(code=1)
|
|
167
|
+
|
|
168
|
+
except RateLimitError as exc:
|
|
169
|
+
typer.secho(str(exc), fg=typer.colors.YELLOW, err=True)
|
|
170
|
+
raise typer.Exit(code=1)
|
|
171
|
+
|
|
172
|
+
except ServiceError as exc:
|
|
173
|
+
typer.secho(f"Explanation failed: {exc}", fg=typer.colors.RED, err=True)
|
|
174
|
+
raise typer.Exit(code=1)
|
|
175
|
+
|
|
176
|
+
typer.echo(result)
|
|
177
|
+
|
|
178
|
+
@app.command()
|
|
179
|
+
def fix(
|
|
180
|
+
command: str = typer.Argument(..., help="The failing command to fix."),
|
|
181
|
+
timeout: int = typer.Option(30, help="Timeout in seconds."),
|
|
182
|
+
) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Run a command, and if it fails, suggest a corrected version.
|
|
185
|
+
"""
|
|
186
|
+
executor = CommandExecutor()
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
result = executor.execute(command, timeout=timeout)
|
|
190
|
+
|
|
191
|
+
except ValidationError as exc:
|
|
192
|
+
typer.secho(f"Invalid input: {exc}", fg=typer.colors.RED, err=True)
|
|
193
|
+
raise typer.Exit(code=2)
|
|
194
|
+
|
|
195
|
+
except CommandTimeoutError as exc:
|
|
196
|
+
typer.secho(f"Timed out: {exc}", fg=typer.colors.RED, err=True)
|
|
197
|
+
raise typer.Exit(code=124)
|
|
198
|
+
|
|
199
|
+
except CommandExecutionError as exc:
|
|
200
|
+
typer.secho(f"Execution error: {exc}", fg=typer.colors.RED, err=True)
|
|
201
|
+
raise typer.Exit(code=1)
|
|
202
|
+
|
|
203
|
+
if result.succeeded:
|
|
204
|
+
typer.secho(f"Command succeeded, nothing to fix.", fg=typer.colors.GREEN)
|
|
205
|
+
if result.stdout:
|
|
206
|
+
typer.echo(result.stdout)
|
|
207
|
+
raise typer.Exit(code=0)
|
|
208
|
+
|
|
209
|
+
typer.secho(f"Command failed: {result.stderr or '(no error output)'}", fg=typer.colors.RED)
|
|
210
|
+
typer.echo()
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
explainer = Explainer()
|
|
214
|
+
suggestion = explainer.suggest_fix(command, result.stderr)
|
|
215
|
+
|
|
216
|
+
except MissingAPIKeyError as exc:
|
|
217
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
218
|
+
raise typer.Exit(code=1)
|
|
219
|
+
|
|
220
|
+
except RateLimitError as exc:
|
|
221
|
+
typer.secho(str(exc), fg=typer.colors.YELLOW, err=True)
|
|
222
|
+
raise typer.Exit(code=1)
|
|
223
|
+
|
|
224
|
+
except ServiceError as exc:
|
|
225
|
+
typer.secho(f"Could not get a fix suggestion: {exc}", fg=typer.colors.RED, err=True)
|
|
226
|
+
raise typer.Exit(code=1)
|
|
227
|
+
|
|
228
|
+
if suggestion is None:
|
|
229
|
+
typer.secho("No confident fix available.", fg=typer.colors.YELLOW)
|
|
230
|
+
raise typer.Exit(code=1)
|
|
231
|
+
|
|
232
|
+
typer.secho("Suggested fix:", fg=typer.colors.CYAN)
|
|
233
|
+
typer.echo(f" {suggestion}")
|
|
234
|
+
typer.echo()
|
|
235
|
+
typer.echo(f'Run it manually, or try: smart-linux run "{suggestion}"')
|
|
236
|
+
|
|
237
|
+
raise typer.Exit(code=1)
|
|
238
|
+
|
|
239
|
+
@app.command()
|
|
240
|
+
def search(
|
|
241
|
+
query: str = typer.Argument(
|
|
242
|
+
..., help="A natural-language question about a Linux task."
|
|
243
|
+
),
|
|
244
|
+
) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Search for how to accomplish a Linux task in plain language.
|
|
247
|
+
"""
|
|
248
|
+
if not query.strip():
|
|
249
|
+
typer.secho("Invalid input: Search query cannot be empty.", fg=typer.colors.RED, err=True)
|
|
250
|
+
raise typer.Exit(code=2)
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
searcher = Searcher()
|
|
254
|
+
result = searcher.search(query)
|
|
255
|
+
|
|
256
|
+
except MissingAPIKeyError as exc:
|
|
257
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
258
|
+
raise typer.Exit(code=1)
|
|
259
|
+
|
|
260
|
+
except RateLimitError as exc:
|
|
261
|
+
typer.secho(str(exc), fg=typer.colors.YELLOW, err=True)
|
|
262
|
+
raise typer.Exit(code=1)
|
|
263
|
+
|
|
264
|
+
except ServiceError as exc:
|
|
265
|
+
typer.secho(f"Search failed: {exc}", fg=typer.colors.RED, err=True)
|
|
266
|
+
raise typer.Exit(code=1)
|
|
267
|
+
|
|
268
|
+
typer.echo(result)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main() -> None:
|
|
272
|
+
"""Entry point wrapper, used by the packaged console script."""
|
|
273
|
+
app()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
centralised configuration to make the project easier to maintain, test, and extend as new features are added.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
def _find_project_root(marker: str = "pyproject.toml") -> Path:
|
|
11
|
+
"""
|
|
12
|
+
Dynamically locate the project root by searching upwards for a marker file.
|
|
13
|
+
This prevents breakage if the settings module is moved to a different depth.
|
|
14
|
+
"""
|
|
15
|
+
current_dir = Path(__file__).resolve().parent
|
|
16
|
+
for parent in [current_dir, *current_dir.parents]:
|
|
17
|
+
if (parent / marker).exists():
|
|
18
|
+
return parent
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
def _get_user_data_home() -> Path:
|
|
22
|
+
"""
|
|
23
|
+
Resolve the user's data directory following the XDG Base Directory
|
|
24
|
+
specification, with a Windows/macOS fallback. This is where the
|
|
25
|
+
application should write logs and runtime data regardless of where
|
|
26
|
+
the code itself is installed.
|
|
27
|
+
"""
|
|
28
|
+
xdg_data_home = os.environ.get("XDG_DATA_HOME")
|
|
29
|
+
if xdg_data_home:
|
|
30
|
+
return Path(xdg_data_home) / "smart-linux-assistant"
|
|
31
|
+
return Path.home() / ".local" / "share" / "smart-linux-assistant"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
PROJECT_ROOT = _find_project_root()
|
|
35
|
+
USER_DATA_HOME = _get_user_data_home()
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class Settings:
|
|
39
|
+
"""
|
|
40
|
+
Store shared project configuration.
|
|
41
|
+
"""
|
|
42
|
+
project_root: Path | None
|
|
43
|
+
logs_directory: Path
|
|
44
|
+
data_directory: Path
|
|
45
|
+
documentation_directory: Path | None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
settings = Settings(
|
|
49
|
+
project_root=PROJECT_ROOT,
|
|
50
|
+
logs_directory=USER_DATA_HOME / "logs",
|
|
51
|
+
data_directory=USER_DATA_HOME / "data",
|
|
52
|
+
documentation_directory=(PROJECT_ROOT / "docs") if PROJECT_ROOT else None,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def initialize_app_filesystem() -> None:
|
|
57
|
+
"""
|
|
58
|
+
Ensure required runtime directories exist with appropriate permissions.
|
|
59
|
+
|
|
60
|
+
must be called explicitly during application startup to prevent
|
|
61
|
+
unintended side-effects during standard imports or automated testing.
|
|
62
|
+
"""
|
|
63
|
+
for directory in (settings.logs_directory, settings.data_directory):
|
|
64
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
directory.chmod(0o755)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Expose the public exception hierarchy for the application.
|
|
3
|
+
Importing exceptions from this package keeps call sites concise and
|
|
4
|
+
avoids exposing the internal module structure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base import (
|
|
8
|
+
CommandExecutionError,
|
|
9
|
+
CommandFailedError,
|
|
10
|
+
CommandTimeoutError,
|
|
11
|
+
ConfigurationError,
|
|
12
|
+
MissingAPIKeyError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
RepositoryError,
|
|
15
|
+
ServiceError,
|
|
16
|
+
SmartLinuxAssistantError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SmartLinuxAssistantError",
|
|
22
|
+
"ConfigurationError",
|
|
23
|
+
"RepositoryError",
|
|
24
|
+
"ServiceError",
|
|
25
|
+
"ValidationError",
|
|
26
|
+
"CommandExecutionError",
|
|
27
|
+
"CommandTimeoutError",
|
|
28
|
+
"CommandFailedError",
|
|
29
|
+
"MissingAPIKeyError",
|
|
30
|
+
"RateLimitError",
|
|
31
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
exception hierarchy for the Smart Linux Assistant.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from linux_assistant.models import CommandResult
|
|
10
|
+
|
|
11
|
+
class SmartLinuxAssistantError(Exception):
|
|
12
|
+
"""
|
|
13
|
+
Base exception for the entire application.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigurationError(SmartLinuxAssistantError):
|
|
18
|
+
"""
|
|
19
|
+
Raised when the application configuration is missing, invalid,
|
|
20
|
+
or cannot be initialized correctly.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RepositoryError(SmartLinuxAssistantError):
|
|
25
|
+
"""
|
|
26
|
+
Raised when a repository cannot read from or write to its
|
|
27
|
+
underlying data source.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ServiceError(SmartLinuxAssistantError):
|
|
32
|
+
"""
|
|
33
|
+
Raised when a business service cannot complete the requested
|
|
34
|
+
operation due to an application-level failure.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ValidationError(SmartLinuxAssistantError):
|
|
39
|
+
"""
|
|
40
|
+
Raised when user-provided or internally generated data fails
|
|
41
|
+
validation before processing.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
class CommandExecutionError(ServiceError):
|
|
45
|
+
"""
|
|
46
|
+
Raised when a shell command cannot be executed at all, for
|
|
47
|
+
example because the shell could not be spawned or the system
|
|
48
|
+
refused to run it. This does NOT cover commands that ran but
|
|
49
|
+
returned a non-zero exit code; a non-zero exit code is a normal,
|
|
50
|
+
successfully-reported outcome captured in CommandResult.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CommandTimeoutError(CommandExecutionError):
|
|
55
|
+
"""
|
|
56
|
+
Raised when a shell command exceeds its allotted timeout and is
|
|
57
|
+
forcibly terminated before completion.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
class CommandFailedError(ServiceError):
|
|
61
|
+
"""
|
|
62
|
+
Raised by execute_checked() when a command runs to completion but
|
|
63
|
+
exits with a non-zero status code. This is distinct from
|
|
64
|
+
CommandExecutionError: the command DID execute successfully at
|
|
65
|
+
the OS level, it simply reported failure via its exit code.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, result: "CommandResult") -> None:
|
|
69
|
+
self.result = result
|
|
70
|
+
message = (
|
|
71
|
+
f"Command '{result.command}' failed with exit code "
|
|
72
|
+
f"{result.exit_code}."
|
|
73
|
+
)
|
|
74
|
+
super().__init__(message)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MissingAPIKeyError(ConfigurationError):
|
|
78
|
+
"""
|
|
79
|
+
Raised when an AI-powered feature is used but the required API
|
|
80
|
+
key environment variable is not set.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, env_var_name: str) -> None:
|
|
84
|
+
self.env_var_name = env_var_name
|
|
85
|
+
message = (
|
|
86
|
+
f"The '{env_var_name}' environment variable is not set. "
|
|
87
|
+
f"Get a free API key at https://console.groq.com and set it with:\n"
|
|
88
|
+
f" export {env_var_name}=\"your-key-here\""
|
|
89
|
+
)
|
|
90
|
+
super().__init__(message)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RateLimitError(ServiceError):
|
|
94
|
+
"""
|
|
95
|
+
Raised when an AI-powered feature hits the API provider's rate
|
|
96
|
+
limit. This is a transient condition — retrying after a short
|
|
97
|
+
delay will typically succeed.
|
|
98
|
+
"""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Domain model representing the result of a Linux command.
|
|
3
|
+
|
|
4
|
+
This model acts as the common language between different parts of
|
|
5
|
+
the application. Whether a command is executed by the CLI, collected
|
|
6
|
+
by a background daemon, or later exposed through a FastAPI endpoint,
|
|
7
|
+
its outcome should be represented using this object.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True, frozen=True)
|
|
17
|
+
class CommandResult:
|
|
18
|
+
"""
|
|
19
|
+
Store the outcome of a single command execution.
|
|
20
|
+
command:
|
|
21
|
+
The exact command that was executed.
|
|
22
|
+
|
|
23
|
+
exit_code:
|
|
24
|
+
Exit status returned by the operating system.
|
|
25
|
+
|
|
26
|
+
stdout:
|
|
27
|
+
Standard output produced by the command.
|
|
28
|
+
|
|
29
|
+
stderr:
|
|
30
|
+
Standard error produced by the command.
|
|
31
|
+
|
|
32
|
+
executed_at:
|
|
33
|
+
Timestamp indicating when the command finished executing.
|
|
34
|
+
duration_seconds:
|
|
35
|
+
Time taken to execute the command, in seconds.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
command: str
|
|
39
|
+
exit_code: int
|
|
40
|
+
stdout: str
|
|
41
|
+
stderr: str
|
|
42
|
+
executed_at: datetime
|
|
43
|
+
duration_seconds: float
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def succeeded(self) -> bool:
|
|
47
|
+
return self.exit_code == 0
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def failed(self) -> bool:
|
|
51
|
+
return not self.succeeded
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Execute Linux commands and return structured results.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from linux_assistant.exceptions import ValidationError, CommandExecutionError, CommandTimeoutError, CommandFailedError
|
|
10
|
+
from linux_assistant.models import CommandResult
|
|
11
|
+
from linux_assistant.utils.logger import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CommandExecutor:
|
|
17
|
+
"""
|
|
18
|
+
Execute Linux shell commands.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def execute(
|
|
22
|
+
self,
|
|
23
|
+
command: str,
|
|
24
|
+
timeout: int = 30,
|
|
25
|
+
) -> CommandResult:
|
|
26
|
+
"""
|
|
27
|
+
Execute a Linux command.
|
|
28
|
+
"""
|
|
29
|
+
command = command.strip()
|
|
30
|
+
|
|
31
|
+
if not command:
|
|
32
|
+
raise ValidationError("Command cannot be empty.")
|
|
33
|
+
|
|
34
|
+
if timeout <= 0:
|
|
35
|
+
raise ValidationError("Timeout must be greater than zero.")
|
|
36
|
+
logger.info("Executing command: %s", command)
|
|
37
|
+
|
|
38
|
+
start_time = time.perf_counter()
|
|
39
|
+
try:
|
|
40
|
+
completed_process = subprocess.run(
|
|
41
|
+
command,
|
|
42
|
+
shell=True,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=timeout,
|
|
46
|
+
check=False,
|
|
47
|
+
)
|
|
48
|
+
except subprocess.TimeoutExpired as exc:
|
|
49
|
+
duration = time.perf_counter() - start_time
|
|
50
|
+
logger.error(
|
|
51
|
+
"Command timed out after %.3f seconds: %s",
|
|
52
|
+
duration,
|
|
53
|
+
command,
|
|
54
|
+
)
|
|
55
|
+
raise CommandTimeoutError(
|
|
56
|
+
f"Command '{command}' timed out after {timeout} seconds."
|
|
57
|
+
) from exc
|
|
58
|
+
except OSError as exc:
|
|
59
|
+
duration = time.perf_counter() - start_time
|
|
60
|
+
logger.error(
|
|
61
|
+
"Command could not be executed: %s (%s)",
|
|
62
|
+
command,
|
|
63
|
+
exc,
|
|
64
|
+
)
|
|
65
|
+
raise CommandExecutionError(
|
|
66
|
+
f"Command '{command}' could not be executed: {exc}"
|
|
67
|
+
) from exc
|
|
68
|
+
|
|
69
|
+
duration = time.perf_counter() - start_time
|
|
70
|
+
|
|
71
|
+
logger.info(
|
|
72
|
+
"Command finished with exit code %d in %.3f seconds.",
|
|
73
|
+
completed_process.returncode,
|
|
74
|
+
duration,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return CommandResult(
|
|
78
|
+
command=command,
|
|
79
|
+
exit_code=completed_process.returncode,
|
|
80
|
+
stdout=completed_process.stdout.strip(),
|
|
81
|
+
stderr=completed_process.stderr.strip(),
|
|
82
|
+
executed_at=datetime.now(timezone.utc),
|
|
83
|
+
duration_seconds=duration,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def execute_checked(
|
|
87
|
+
self,
|
|
88
|
+
command: str,
|
|
89
|
+
timeout: int = 30,
|
|
90
|
+
) -> CommandResult:
|
|
91
|
+
"""
|
|
92
|
+
Execute a Linux command and raise if it fails.
|
|
93
|
+
Behaves exactly like execute(), except that a non-zero exit
|
|
94
|
+
code raises CommandFailedError instead of being returned
|
|
95
|
+
silently inside a CommandResult. Use this when a failed
|
|
96
|
+
command should be treated as an exceptional condition rather
|
|
97
|
+
than a normal outcome the caller must check manually.
|
|
98
|
+
"""
|
|
99
|
+
result = self.execute(command, timeout=timeout)
|
|
100
|
+
|
|
101
|
+
if result.failed:
|
|
102
|
+
logger.error(
|
|
103
|
+
"Command '%s' failed with exit code %d.",
|
|
104
|
+
result.command,
|
|
105
|
+
result.exit_code,
|
|
106
|
+
)
|
|
107
|
+
raise CommandFailedError(result)
|
|
108
|
+
|
|
109
|
+
return result
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI-powered explanations for Linux commands and error messages.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import groq
|
|
7
|
+
from linux_assistant.utils.groq_client import GROQ_MODEL, build_groq_client
|
|
8
|
+
from linux_assistant.exceptions import MissingAPIKeyError, ServiceError, ValidationError, RateLimitError
|
|
9
|
+
from linux_assistant.utils.logger import get_logger
|
|
10
|
+
from linux_assistant.utils.groq_client import GROQ_MODEL, build_groq_client, truncate_for_api
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
SYSTEM_PROMPT = """You are an elite Linux System Administrator and DevOps expert. Your primary role is to analyze, explain, and troubleshoot Linux shell commands, shell scripts, error messages, and terminal output.
|
|
15
|
+
|
|
16
|
+
Your tone should be professional, direct, and accessible, translating complex technical concepts into plain language without losing accuracy.
|
|
17
|
+
|
|
18
|
+
When responding, strictly adhere to the following guidelines:
|
|
19
|
+
|
|
20
|
+
1. STRUCTURED BREAKDOWNS: When explaining a command, break it down logically. Briefly explain what the entire command does, then use standard dashes (-) to list and explain each specific flag, option, and argument.
|
|
21
|
+
2. TROUBLESHOOTING PROTOCOL: If the user provides an error message or broken script, always include:
|
|
22
|
+
- Root Cause: A brief explanation of why it failed.
|
|
23
|
+
- The Fix: The exact, corrected command or action required.
|
|
24
|
+
- Verification: How the user can verify the fix worked.
|
|
25
|
+
3. SAFETY FIRST: If a command is destructive (e.g., involves rm -rf, dd, chmod 777, or partition changes), clearly prepend a [WARNING] to your response explaining the risk and how to execute it safely.
|
|
26
|
+
4. BEST PRACTICES: Where applicable, suggest modern or more efficient alternatives (e.g., using ip instead of ifconfig).
|
|
27
|
+
5. TERMINAL-SAFE FORMATTING: Do NOT use any Markdown formatting. Your output is being displayed in a raw text terminal. Do not use asterisks for bolding, hashes for headers, or backticks for code. Instead, use ALL CAPS for section headers. To highlight commands or file paths, simply indent them with spaces on a new line or wrap them in single quotes (' ').
|
|
28
|
+
|
|
29
|
+
Keep responses concise and scannable. Limit your response to the essential information needed to solve the user's problem or answer their question, avoiding unnecessary fluff.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
FIX_SYSTEM_PROMPT = """You are an automated Linux command correction tool operating in a raw terminal. You receive a failed shell command and its corresponding error message. Your single purpose is to output the exact, executable corrected command.
|
|
33
|
+
|
|
34
|
+
CRITICAL CONSTRAINTS:
|
|
35
|
+
1. Output ONLY the corrected command on a single line.
|
|
36
|
+
2. Provide zero conversational filler (do not start with "Here is the command" or "Try this").
|
|
37
|
+
3. Use zero Markdown formatting (no backticks or formatting blocks).
|
|
38
|
+
4. Do not include quotes or a leading '$' prompt.
|
|
39
|
+
5. Retain the user's original filenames, paths, and valid arguments exactly as provided; do not substitute them with generic placeholders.
|
|
40
|
+
6. If you cannot determine a highly confident fix, output exactly: NO_FIX_AVAILABLE
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Explainer:
|
|
45
|
+
"""
|
|
46
|
+
Generate plain-language explanations of Linux commands or errors
|
|
47
|
+
using an LLM.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self._client = build_groq_client()
|
|
52
|
+
|
|
53
|
+
def explain(self, text: str) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Explain a command or error message in plain language.
|
|
56
|
+
ValidationError: If text is empty or whitespace-only.
|
|
57
|
+
ServiceError: If the underlying API call fails.
|
|
58
|
+
"""
|
|
59
|
+
text = text.strip()
|
|
60
|
+
|
|
61
|
+
if not text:
|
|
62
|
+
from linux_assistant.exceptions import ValidationError
|
|
63
|
+
|
|
64
|
+
raise ValidationError("Text to explain cannot be empty.")
|
|
65
|
+
|
|
66
|
+
text = truncate_for_api(text)
|
|
67
|
+
|
|
68
|
+
logger.info("Requesting explanation for: %s", text)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
response = self._client.chat.completions.create(
|
|
72
|
+
model=GROQ_MODEL,
|
|
73
|
+
messages=[
|
|
74
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
75
|
+
{"role": "user", "content": text},
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
except groq.RateLimitError as exc:
|
|
79
|
+
logger.error("Rate limit hit: %s", exc)
|
|
80
|
+
raise RateLimitError(
|
|
81
|
+
"Groq API rate limit reached. Please wait a moment and try again."
|
|
82
|
+
) from exc
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
logger.error("Explanation request failed: %s", exc)
|
|
85
|
+
raise ServiceError(f"Failed to get explanation: {exc}") from exc
|
|
86
|
+
|
|
87
|
+
explanation = response.choices[0].message.content
|
|
88
|
+
if explanation is None:
|
|
89
|
+
raise ServiceError("Received an empty explanation from the API.")
|
|
90
|
+
logger.info("Explanation received (%d characters).", len(explanation))
|
|
91
|
+
|
|
92
|
+
return explanation.strip()
|
|
93
|
+
|
|
94
|
+
def suggest_fix(self, command: str, error: str) -> str | None:
|
|
95
|
+
"""
|
|
96
|
+
Suggest a corrected version of a failed shell command.
|
|
97
|
+
"""
|
|
98
|
+
command = command.strip()
|
|
99
|
+
|
|
100
|
+
if not command:
|
|
101
|
+
from linux_assistant.exceptions import ValidationError
|
|
102
|
+
|
|
103
|
+
raise ValidationError("Command to fix cannot be empty.")
|
|
104
|
+
|
|
105
|
+
error = truncate_for_api(error, keep_end=True)
|
|
106
|
+
|
|
107
|
+
user_content = f"Command: {command}\nError: {error.strip()}"
|
|
108
|
+
|
|
109
|
+
logger.info("Requesting fix suggestion for: %s", command)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
response = self._client.chat.completions.create(
|
|
113
|
+
model=GROQ_MODEL,
|
|
114
|
+
messages=[
|
|
115
|
+
{"role": "system", "content": FIX_SYSTEM_PROMPT},
|
|
116
|
+
{"role": "user", "content": user_content},
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
except groq.RateLimitError as exc:
|
|
120
|
+
logger.error("Rate limit hit: %s", exc)
|
|
121
|
+
raise RateLimitError(
|
|
122
|
+
"Groq API rate limit reached. Please wait a moment and try again."
|
|
123
|
+
) from exc
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
logger.error("Explanation request failed: %s", exc)
|
|
126
|
+
raise ServiceError(f"Failed to get explanation: {exc}") from exc
|
|
127
|
+
|
|
128
|
+
suggestion = response.choices[0].message.content
|
|
129
|
+
|
|
130
|
+
if suggestion is None:
|
|
131
|
+
raise ServiceError("Received an empty fix suggestion from the API.")
|
|
132
|
+
|
|
133
|
+
suggestion = suggestion.strip()
|
|
134
|
+
|
|
135
|
+
if suggestion == "NO_FIX_AVAILABLE":
|
|
136
|
+
logger.info("No confident fix available for: %s", command)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
logger.info("Fix suggestion received: %s", suggestion)
|
|
140
|
+
|
|
141
|
+
return suggestion
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI-powered natural-language search for Linux commands and tasks.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import groq
|
|
7
|
+
from linux_assistant.exceptions import ServiceError, ValidationError, RateLimitError
|
|
8
|
+
from linux_assistant.utils.groq_client import GROQ_MODEL, build_groq_client
|
|
9
|
+
from linux_assistant.utils.logger import get_logger
|
|
10
|
+
from linux_assistant.utils.groq_client import GROQ_MODEL, build_groq_client, truncate_for_api
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
SEARCH_SYSTEM_PROMPT = """You are a Linux command lookup tool operating in a raw terminal. The user will describe a desired action in plain language. Your objective is to provide a concrete, ready-to-run command and a brief explanation.
|
|
15
|
+
|
|
16
|
+
CRITICAL CONSTRAINTS:
|
|
17
|
+
1. COMMAND FORMAT: Place the suggested command on its own line, prefixed with exactly "$ ".
|
|
18
|
+
2. EXPLANATION: Provide a 1-2 sentence practical explanation on the line immediately below the command.
|
|
19
|
+
3. ZERO MARKDOWN: Do NOT use backticks (`), asterisks (*), or hashes (#). Output pure raw text.
|
|
20
|
+
4. ZERO FILLER: Do not use conversational openings like "Sure" or "Here is the command". Start immediately with the "$ " command line.
|
|
21
|
+
5. READY-TO-RUN (NO PLACEHOLDERS): Always use realistic, runnable values in the command itself. Use standard defaults (like '.' for the current directory) or plausible example filenames (like 'example.txt'). NEVER use angle-bracket placeholders like <directory_path> or <filename>. The user should be able to copy and run the command directly.
|
|
22
|
+
6. BREVITY: Keep the entire response strictly under 120 words.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Searcher:
|
|
27
|
+
"""
|
|
28
|
+
Answer natural-language questions about Linux commands and tasks.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self._client = build_groq_client()
|
|
33
|
+
|
|
34
|
+
def search(self, query: str) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Answer a natural-language question about a Linux task.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
query: The user's question, e.g. "how do I find large files".
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A short, practical answer.
|
|
43
|
+
"""
|
|
44
|
+
query = query.strip()
|
|
45
|
+
|
|
46
|
+
if not query:
|
|
47
|
+
raise ValidationError("Search query cannot be empty.")
|
|
48
|
+
|
|
49
|
+
query = truncate_for_api(query)
|
|
50
|
+
|
|
51
|
+
logger.info("Searching for: %s", query)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
response = self._client.chat.completions.create(
|
|
55
|
+
model=GROQ_MODEL,
|
|
56
|
+
messages=[
|
|
57
|
+
{"role": "system", "content": SEARCH_SYSTEM_PROMPT},
|
|
58
|
+
{"role": "user", "content": query},
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
except groq.RateLimitError as exc:
|
|
62
|
+
logger.error("Rate limit hit: %s", exc)
|
|
63
|
+
raise RateLimitError(
|
|
64
|
+
"Groq API rate limit reached. Please wait a moment and try again."
|
|
65
|
+
) from exc
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
logger.error("Explanation request failed: %s", exc)
|
|
68
|
+
raise ServiceError(f"Failed to get explanation: {exc}") from exc
|
|
69
|
+
|
|
70
|
+
answer = response.choices[0].message.content
|
|
71
|
+
|
|
72
|
+
if answer is None:
|
|
73
|
+
raise ServiceError("Received an empty answer from the API.")
|
|
74
|
+
|
|
75
|
+
logger.info("Search answer received (%d characters).", len(answer))
|
|
76
|
+
|
|
77
|
+
return answer.strip()
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared Groq client construction for AI-powered services.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import os
|
|
7
|
+
from groq import Groq
|
|
8
|
+
from linux_assistant.exceptions import MissingAPIKeyError
|
|
9
|
+
|
|
10
|
+
GROQ_API_KEY = "GROQ_API_KEY"
|
|
11
|
+
GROQ_MODEL = "llama-3.3-70b-versatile"
|
|
12
|
+
|
|
13
|
+
MAX_INPUT_CHARACTERS = 4000
|
|
14
|
+
REQUEST_TIMEOUT_SECONDS = 30.0
|
|
15
|
+
MAX_RETRIES = 2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_groq_client() -> Groq:
|
|
19
|
+
"""
|
|
20
|
+
Construct an authenticated Groq client using the GROQ_API_KEY
|
|
21
|
+
environment variable.
|
|
22
|
+
|
|
23
|
+
The client is configured with a request timeout and automatic
|
|
24
|
+
retries for transient failures (connection errors, timeouts, and
|
|
25
|
+
5xx server errors), handled internally by the Groq SDK.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
MissingAPIKeyError: If the environment variable is not set.
|
|
29
|
+
"""
|
|
30
|
+
api_key = os.environ.get(GROQ_API_KEY)
|
|
31
|
+
|
|
32
|
+
if not api_key:
|
|
33
|
+
raise MissingAPIKeyError(GROQ_API_KEY)
|
|
34
|
+
|
|
35
|
+
return Groq(
|
|
36
|
+
api_key=api_key,
|
|
37
|
+
timeout=REQUEST_TIMEOUT_SECONDS,
|
|
38
|
+
max_retries=MAX_RETRIES,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def truncate_for_api(text: str, *, keep_end: bool = False) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Truncate text to a safe length before sending it to the API,
|
|
44
|
+
avoiding excessive token usage or provider-side length limits.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
text: The text to truncate.
|
|
48
|
+
keep_end: If True, keep the end of the text and truncate from
|
|
49
|
+
the start (useful for error output, where the final lines
|
|
50
|
+
are usually most relevant). If False, keep the beginning
|
|
51
|
+
and truncate from the end.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The original text if already within the limit, otherwise a
|
|
55
|
+
truncated version with a marker indicating truncation occurred.
|
|
56
|
+
"""
|
|
57
|
+
if len(text) <= MAX_INPUT_CHARACTERS:
|
|
58
|
+
return text
|
|
59
|
+
|
|
60
|
+
marker = "...[truncated]..."
|
|
61
|
+
available = MAX_INPUT_CHARACTERS - len(marker)
|
|
62
|
+
|
|
63
|
+
if keep_end:
|
|
64
|
+
return marker + text[-available:]
|
|
65
|
+
return text[:available] + marker
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized logging configuration for the Smart Linux Assistant.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from logging.handlers import RotatingFileHandler
|
|
9
|
+
from linux_assistant.config.settings import settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LOG_FILE = settings.logs_directory / "smart_linux_assistant.log"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_logger(name: str) -> logging.Logger:
|
|
16
|
+
"""
|
|
17
|
+
Return a configured logger for the given module.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(name)
|
|
21
|
+
|
|
22
|
+
if logger.handlers:
|
|
23
|
+
return logger
|
|
24
|
+
|
|
25
|
+
logger.setLevel(logging.INFO)
|
|
26
|
+
|
|
27
|
+
formatter = logging.Formatter(
|
|
28
|
+
fmt=(
|
|
29
|
+
"%(asctime)s | "
|
|
30
|
+
"%(levelname)-8s | "
|
|
31
|
+
"%(name)s | "
|
|
32
|
+
"%(message)s"
|
|
33
|
+
),
|
|
34
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
console_handler = logging.StreamHandler()
|
|
38
|
+
console_handler.setFormatter(formatter)
|
|
39
|
+
|
|
40
|
+
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
file_handler = RotatingFileHandler(
|
|
42
|
+
filename=LOG_FILE,
|
|
43
|
+
maxBytes=5 * 1024 * 1024,
|
|
44
|
+
backupCount=5,
|
|
45
|
+
encoding="utf-8",
|
|
46
|
+
)
|
|
47
|
+
file_handler.setFormatter(formatter)
|
|
48
|
+
|
|
49
|
+
logger.addHandler(console_handler)
|
|
50
|
+
logger.addHandler(file_handler)
|
|
51
|
+
|
|
52
|
+
logger.propagate = False
|
|
53
|
+
|
|
54
|
+
return logger
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shell-level utilities for checking the system environment.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
from linux_assistant.exceptions import ValidationError
|
|
10
|
+
from linux_assistant.utils.logger import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def command_exists(name: str) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Check whether a command is available on the system's PATH.
|
|
18
|
+
"""
|
|
19
|
+
name = name.strip()
|
|
20
|
+
|
|
21
|
+
if not name:
|
|
22
|
+
raise ValidationError("Command name cannot be empty.")
|
|
23
|
+
|
|
24
|
+
found_path = shutil.which(name)
|
|
25
|
+
|
|
26
|
+
if found_path is None:
|
|
27
|
+
logger.info("Command '%s' was not found on PATH.", name)
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
logger.info("Command '%s' found at '%s'.", name, found_path)
|
|
31
|
+
return True
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smart-linux-assistant
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: An AI-powered Linux productivity assistant for command analysis, troubleshooting, and knowledge management.
|
|
5
|
+
Author: Shubham Kumar Jha
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/shubham-k-jha-dev/smart-linux-assistant
|
|
8
|
+
Project-URL: Repository, https://github.com/shubham-k-jha-dev/smart-linux-assistant
|
|
9
|
+
Project-URL: Issues, https://github.com/shubham-k-jha-dev/smart-linux-assistant/issues
|
|
10
|
+
Keywords: linux,cli,automation,terminal,devops,shell,productivity
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Environment :: Console
|
|
18
|
+
Classifier: Topic :: System :: Systems Administration
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Classifier: Development Status :: 3 - Alpha
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: typer>=0.12.0
|
|
25
|
+
Requires-Dist: groq>=0.9.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# Smart Linux Assistant
|
|
29
|
+
|
|
30
|
+
Smart Linux Assistant is an AI-powered Linux operations assistant that understands natural language, safely executes shell commands, retrieves Linux knowledge, explains errors, and assists users with troubleshooting. The current version implements the core command execution engine and foundational architecture for future AI capabilities.
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+

|
|
34
|
+
[](https://github.com/shubham-k-jha-dev/smart-linux-assistant/actions/workflows/ci.yml)
|
|
35
|
+
|
|
36
|
+
## Project Overview
|
|
37
|
+
|
|
38
|
+
Smart Linux Assistant is a command-line utility that executes shell commands and returns structured outcomes. The tool captures the command, exit code, stdout, stderr, execution timestamp, and duration to make downstream automation and logging straightforward.
|
|
39
|
+
|
|
40
|
+
## System Architecture
|
|
41
|
+
|
|
42
|
+
- CLI (`linux_assistant.cli.main`) accepts user commands and options and delegates execution to `CommandExecutor`.
|
|
43
|
+
- `CommandExecutor` runs shell commands using `subprocess.run` and returns a `CommandResult` dataclass describing the outcome.
|
|
44
|
+
- Centralized logging is provided by `linux_assistant.utils.logger`, writing to `logs/smart_linux_assistant.log` with rotation.
|
|
45
|
+
- Runtime paths and directories are managed by `linux_assistant.config.settings` and can be initialized with `initialize_app_filesystem()`.
|
|
46
|
+
|
|
47
|
+
## Tech Stack
|
|
48
|
+
|
|
49
|
+
- Python 3.11+
|
|
50
|
+
- Typer (CLI)
|
|
51
|
+
- Standard library: `subprocess`, `logging`, `shutil`, `dataclasses`, `pathlib`, `datetime`
|
|
52
|
+
|
|
53
|
+
## Prerequisites
|
|
54
|
+
|
|
55
|
+
1. Python 3.11 or newer.
|
|
56
|
+
2. Optional: a virtual environment tool (`venv`).
|
|
57
|
+
3. No Dockerfile or docker-compose are included in this repository.
|
|
58
|
+
|
|
59
|
+
## Local Setup & Installation
|
|
60
|
+
|
|
61
|
+
1. Clone the repository:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/shubham-k-jha-dev/smart-linux-assistant
|
|
65
|
+
cd smart-linux-assistant
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. Create and activate a virtual environment:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Linux/macOS
|
|
72
|
+
python3 -m venv .venv
|
|
73
|
+
source .venv/bin/activate
|
|
74
|
+
|
|
75
|
+
# Windows (PowerShell)
|
|
76
|
+
python -m venv .venv
|
|
77
|
+
.venv\\Scripts\\Activate.ps1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
3. Install development dependencies:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install -r requirements-dev.txt
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
4. (Optional) Install the package in editable mode to enable the `smart-linux` CLI entrypoint:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install -e .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
5. (Optional) Ensure runtime directories exist from Python:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from linux_assistant.config.settings import initialize_app_filesystem
|
|
96
|
+
initialize_app_filesystem()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Environment Variables
|
|
100
|
+
|
|
101
|
+
This project does not require any environment variables for its core CLI functionality. The repository includes an empty `.env.example` placeholder.
|
|
102
|
+
|
|
103
|
+
| Variable | Description | Example |
|
|
104
|
+
|----------|-------------|---------|
|
|
105
|
+
| (none) | No required environment variables for CLI execution | - |
|
|
106
|
+
|
|
107
|
+
## Usage / API Reference
|
|
108
|
+
|
|
109
|
+
The project exposes the console scripts `smart-linux` and `sla` (configured in `pyproject.toml`).
|
|
110
|
+
|
|
111
|
+
- Run a shell command:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
smart-linux run "echo hello"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- Options:
|
|
118
|
+
- `--timeout <seconds>` — maximum seconds to allow command to run (default: 30)
|
|
119
|
+
- `--check` — treat non-zero exit codes as errors and exit with that code
|
|
120
|
+
- `--suggest-fix` — if the command fails, use AI to suggest a corrected version (requires `--check`; requires `GROQ_API_KEY`, same as `explain`/`fix`/`search`)
|
|
121
|
+
|
|
122
|
+
- Doctor command (checks common tools):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
smart-linux doctor
|
|
126
|
+
```
|
|
127
|
+
- Get an AI-powered explanation of a command or error message:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
smart-linux explain "permission denied when running ./script.sh"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Requires a free Groq API key set as an environment variable:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
export GROQ_API_KEY="your-key-here"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Get a free key at [console.groq.com](https://console.groq.com).
|
|
140
|
+
|
|
141
|
+
- Fix a failing command:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
smart-linux fix "ls /nonexistent"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- Options:
|
|
148
|
+
- `--timeout <seconds>` — maximum seconds to allow the command to run (default: 30)
|
|
149
|
+
|
|
150
|
+
- This runs the command and, if it fails, uses the AI to suggest a corrected version. Requires the same `GROQ_API_KEY` environment variable as the `explain` command.
|
|
151
|
+
|
|
152
|
+
- Search for a Linux task in plain language:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
smart-linux search "find the 10 largest files in the current directory"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- This returns a concrete command and brief explanation for the requested task. Requires the same `GROQ_API_KEY` environment variable as the `explain` command.
|
|
159
|
+
|
|
160
|
+
### Example output
|
|
161
|
+
|
|
162
|
+
Successful command:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
$ smart-linux run "echo hello"
|
|
166
|
+
hello
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Failed command (example):
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
$ smart-linux run "ls nonexistent" --check
|
|
173
|
+
ls: cannot access 'nonexistent': No such file or directory
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
These outputs reflect the CLI behaviour: standard output is printed for successful commands; standard error is printed for failures and, when `--check` is used, the CLI exits with the command's exit code.
|
|
177
|
+
|
|
178
|
+
Failed command with an AI-suggested fix:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
$ smart-linux run "gti status" --check --suggest-fix
|
|
182
|
+
gti: command not found
|
|
183
|
+
|
|
184
|
+
Suggested fix:
|
|
185
|
+
git status
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`--suggest-fix` requires `--check` (fix suggestions only apply to command failures detected via `--check`); calling it without `--check` exits immediately with an error.
|
|
189
|
+
|
|
190
|
+
## Roadmap / Current Status
|
|
191
|
+
|
|
192
|
+
- Core CLI: implemented — `run` and `doctor` commands are provided in `linux_assistant.cli.main`.
|
|
193
|
+
- Command execution: implemented using `linux_assistant.services.command_executor.CommandExecutor` which returns `CommandResult` instances.
|
|
194
|
+
- Logging & configuration: implemented via `linux_assistant.utils.logger` and `linux_assistant.config.settings`.
|
|
195
|
+
- Packaging: console script entry points are declared in `pyproject.toml`.
|
|
196
|
+
- AI-powered explanations: implemented — `smart-linux explain` uses the Groq API (`llama-3.3-70b-versatile`) to generate plain-language explanations of commands and error messages, via `linux_assistant.services.explainer.Explainer`. Requires a user-supplied `GROQ_API_KEY` environment variable.
|
|
197
|
+
- AI-powered fix suggestions: implemented — `smart-linux fix` runs a failing command and suggests a corrected version; `smart-linux run --check --suggest-fix` offers the same suggestion inline as part of normal command execution. Both use `linux_assistant.services.explainer.Explainer.suggest_fix()`.
|
|
198
|
+
- AI-powered search: implemented — `smart-linux search` answers natural-language questions about Linux tasks via `linux_assistant.services.search.Searcher`.
|
|
199
|
+
- Production hardening: implemented — API timeouts, retry logic, rate-limit-specific handling, input truncation, and documented OS/privacy limitations across all AI-backed commands.
|
|
200
|
+
- Additional AI features (command history awareness, documentation lookup) are planned but not yet implemented.
|
|
201
|
+
|
|
202
|
+
## Known Limitations
|
|
203
|
+
|
|
204
|
+
- Tested and verified on Linux (native and WSL). Not yet tested on macOS or native Windows Python — behavior on those platforms is currently unverified, though the codebase avoids Linux-only APIs where possible.
|
|
205
|
+
|
|
206
|
+
## Privacy Note
|
|
207
|
+
|
|
208
|
+
The `explain`, `fix`, and `search` commands send the command text, error output, or your query to Groq's API for processing. Avoid running these commands on text that contains secrets, passwords, or sensitive data, since that content leaves your machine.
|
|
209
|
+
|
|
210
|
+
## Install from PyPI
|
|
211
|
+
|
|
212
|
+
If this package is published to PyPI, it can be installed with:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pip install smart-linux-assistant
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Testing
|
|
219
|
+
|
|
220
|
+
Run the test suite with `pytest`:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
pytest
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT License — see `LICENSE`.
|
|
229
|
+
|
|
230
|
+
## Contributing
|
|
231
|
+
|
|
232
|
+
- Run tests with `pytest` before opening a pull request.
|
|
233
|
+
- Follow standard Python packaging best practices.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
linux_assistant/__init__.py,sha256=ZtxUCkFFzbqtXCAYGt_FhBSiAsWhJkcecOv9e6tbnUs,233
|
|
2
|
+
linux_assistant/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
linux_assistant/cli/main.py,sha256=9Pupxu5ppg6j3AAdsUjQEUZmi4rvSAXT-GIuu8Qr5wQ,8783
|
|
4
|
+
linux_assistant/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
linux_assistant/config/settings.py,sha256=Ll1QLfgxwON6tst2-uRMHPTx2ACL5CypkjBm0CXsd_w,2187
|
|
6
|
+
linux_assistant/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
linux_assistant/exceptions/__init__.py,sha256=Nw2jvBHkML5Wg02zr-gR7G0XCwxmJId9a1eE8jy77eM,737
|
|
8
|
+
linux_assistant/exceptions/base.py,sha256=85oWenBiosghSjzuz95Tzh7APE6O34wkH587Obz2-FU,3015
|
|
9
|
+
linux_assistant/models/__init__.py,sha256=GXMAMk8W_s2XzQ7KoBWS2G8l_tOa8DvOpwopADbBLN0,251
|
|
10
|
+
linux_assistant/models/command_result.py,sha256=E7Ik3idYMI7-5eEjliriUu8QCxd3ndhcb_aH0Va8fHs,1346
|
|
11
|
+
linux_assistant/repositories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
linux_assistant/services/__init__.py,sha256=XZOAN1X6KMgVQQ0gcvsgJ2-VSLlDvBrrGFFKCW2g5Uc,71
|
|
13
|
+
linux_assistant/services/command_executor.py,sha256=NKVka5u6N64vJ3REDIcK0WaGIVIu8kYgLker1VClwuI,3489
|
|
14
|
+
linux_assistant/services/explainer.py,sha256=3oO0wu_cgMn1WI1BE-jgLRjbVZ7kwl9yTOKRmTUghOg,6592
|
|
15
|
+
linux_assistant/services/search.py,sha256=kHVERaIycVTFz9y-Z27SRLks94EXOScj6tkRm8KVCAY,3269
|
|
16
|
+
linux_assistant/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
linux_assistant/utils/groq_client.py,sha256=lLiV6nJpSQxuZ1SKDfZriVgVKvNUb4-aYNZYS8DGvcI,1966
|
|
18
|
+
linux_assistant/utils/logger.py,sha256=wBMPv6VcqkKPapXDyAA6rhMTtoSCzTJ8C1VAIJAPYFA,1273
|
|
19
|
+
linux_assistant/utils/shell.py,sha256=in4zz7uQiM4w7nqEdFz6Vem_zMqc0dCDu8fNRMieVS0,744
|
|
20
|
+
smart_linux_assistant-0.5.0.dist-info/licenses/LICENSE,sha256=jo3I7AiH6gbrwm977FhHWRGmpkgpo4K3sPFhLg2R9kg,1074
|
|
21
|
+
smart_linux_assistant-0.5.0.dist-info/METADATA,sha256=2v9kYvocOq8Op9eN69ekD2_AvMbHJEN6hMFOxWPIP9M,8748
|
|
22
|
+
smart_linux_assistant-0.5.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
23
|
+
smart_linux_assistant-0.5.0.dist-info/entry_points.txt,sha256=bIeHREzxAeh5t4rP4ESLqwUBStKYdlHVhUdYlr0SEok,96
|
|
24
|
+
smart_linux_assistant-0.5.0.dist-info/top_level.txt,sha256=ClGTbRQlqUYxbbL_9Y_cM4nAAObdue-YdB8TbACfZeo,16
|
|
25
|
+
smart_linux_assistant-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shubham Kumar Jha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
linux_assistant
|